1、万恶的擦除html
我在本身总结的【Java心得总结三】Java泛型上——初识泛型这篇博文中提到了Java中对泛型擦除的问题,考虑下面代码:java
1 import java.util.*; 2 public class ErasedTypeEquivalence { 3 public static void main(String[] args) { 4 Class c1 = new ArrayList<String>().getClass(); 5 Class c2 = new ArrayList<Integer>().getClass(); 6 System.out.println(c1 == c2); 7 } 8 }/* Output: 9 true 10 *///:~
在代码的第4行和第5行,咱们分别定义了一个接受String类型的List和一个接受Integer类型的List,按照咱们正常的理解,泛型ArrayList<T>虽然是相同的,可是咱们给它传了不一样的类型参数,那么c1和2的类型应该是不一样的。可是结果偏偏想法,运行程序发现两者的类型时相同的。这是为何呢?这里就要说到Java语言实现泛型所独有的——擦除(万恶啊)python
即当咱们声明List<String>和List<Integer>时,在运行时其实是相同的,都是List,而具体的类型参数信息String和Integer被擦除了。这就致使一个很麻烦的问题:在泛型代码内部,没法得到任何有关泛型参数类型的信息 (摘自《Java编程思想第4版》)。ios
为了体验万恶的擦除的“万恶”,咱们与C++作一个比较:程序员
C++模板:编程
1 #include <iostream> 2 using namespace std; 3 template<class T> class Manipulator { 4 T obj; 5 public: 6 Manipulator(T x) { obj = x; } 7 void manipulate() { obj.f(); } 8 }; 9 class HasF { 10 public: 11 void f() { cout << "HasF::f()" << endl; } 12 }; 13 int main() { 14 HasF hf; 15 Manipulator<HasF> manipulator(hf); 16 manipulator.manipulate(); 17 } /* Output: 18 HasF::f() 19 ///:~
在这段代码中,咱们声明了一个模板(即泛型)类Manipulator,这个类接收一个T类型的对象,并在内部调用该对象的f方法,在main咱们向Manipulator传入一个拥有f方法的类HasF,而后代码很正常的经过编译并且顺利运行。数组
C++代码里其实有一个很奇怪的地方,就是在代码第7行,咱们利用传入的T类型对象来调用它的f方法,那么我怎么知道你传入的类型参数T类型是否有方法f呢?可是从整个编译来看,C++中确实实现了,而且保证了整个代码的正确性(能够验证一个没有方法f的类传入,就会报错)。至于怎么作到,咱们稍后会略微说起。安全
OK,咱们将这段代码用Java实现下:app
Java泛型:函数
1 public class HasF { 2 public void f() { System.out.println("HasF.f()"); } 3 } 4 class Manipulator<T> { 5 private T obj; 6 public Manipulator(T x) { obj = x; } 7 // Error: cannot find symbol: method f(): 8 public void manipulate() { obj.f(); } 9 } 10 public class Manipulation { 11 public static void main(String[] args) { 12 HasF hf = new HasF(); 13 Manipulator<HasF> manipulator = 14 new Manipulator<HasF>(hf); 15 manipulator.manipulate(); 16 } 17 } ///:~
你们会发如今C++咱们很方便就能实现的效果,在Java里没法办到,在代码第7行给出了错误提示,就是说在Manipulator内部咱们没法获知类型T是否含有方法f。这是为何呢?就是由于万恶的擦除引发的,在Java代码运行的时候,它会将泛型类的类型信息T擦除掉,就是说运行阶段,泛型类代码内部彻底不知道类型参数的任何信息。如上面代码,运行阶段Manipulator<HasF>类的类型信息会被擦除,只剩下Mainipulator,因此咱们在Manipulator内部并不知道传入的参数类型时HasF的,因此第8行代码obj调用f天然就会报错(就是我哪知道你有没有f方法啊)
综上,咱们能够看出擦除带来的代价:在泛型类或者说泛型方法内部,咱们没法得到任何类型信息,因此泛型不能用于显示的引用运行时类型的操做之中,例如转型、instanceof操做和new表达式。例以下代码:
1 public class Animal<T>{ 2 T a; 3 public Animal(T a){ 4 this.a = a; 5 } 6 // error! 7 public void animalMove(){ 8 a.move(); 9 } 10 // error! 11 public void animalBark(){ 12 a.bark(); 13 } 14 // error! 15 public void animalNew(){ 16 return new T(); 17 } 18 // error! 19 public boolean isDog(){ 20 return T instanceof Dog; 21 } 22 } 23 public class Dog{ 24 public void move(){ 25 System.out.println("dog move"); 26 } 27 public void bark(){ 28 System.out.println("wang!wang!); 29 } 30 } 31 public static void main(String[] args){ 32 Animal<Dog> ad = new Animal<Dog>(); 33 }
咱们声明一个泛化的Animal类,以后声明一个Dog类,Dog类能够移动move(),吠叫bark()。在main中将Dog做为类型参数传递给Animal<Dog>。而在代码的第8行和第11行,咱们尝试调用传入类的函数move()和bark(),发现会有错误;在代码16行,咱们试图返回一个T类型的对象即new一个,也会获得错误;而在代码20行,当咱们试图利用instanceof判断T是否为Dog类型时,一样是错误!
另外,我这里想强调下Java泛型是不支持基本类型的(基本类型可参见【Java心得总结一】Java基本类型和包装类型解析)感谢CCQLegend
因此仍是上面咱们说过的话:在泛型代码内部,没法得到任何有关泛型参数类型的信息 (摘自《Java编程思想第4版》),咱们在编写泛化类的时候,咱们要时刻提醒本身,咱们传入的参数T仅仅是一个Object类型,任何具体类型信息咱们都是未知的。
2、为何Java用擦除
上面咱们简单阐述了Java中泛型的一个擦除问题,也体会到它的万恶,给咱们编程带来的不便。那Java开发者为何要这么干呢?
这是一个历史问题,Java在版本1.0中是不支持泛型的,这就致使了很大一批原有类库是在不支持泛型的Java版本上建立的。而到后来Java逐渐加入了泛型,为了使得原有的非泛化类库可以在泛化的客户端使用,Java开发者使用了擦除进行了折中。
因此Java使用这么具备局限性的泛型实现方法就是从非泛化代码到泛化代码的一个过渡,以及不破坏原有类库的状况下,将泛型融入Java语言。
3、怎么解决擦除带来的烦恼
解决方案1:
不要使用Java语言。这是废话,可是确实,当你使用python和C++等语言,你会发如今这两种语言中使用泛型是一件很是轻松加随意的事情,而在Java中是事情要变得复杂得多。以下示例:
python:
1 class Dog: 2 def speak(self): 3 print "Arf!" 4 def sit(self): 5 print "Sitting" 6 def reproduce(self): 7 pass 8 9 class Robot: 10 def speak(self): 11 print "Click!" 12 def sit(self): 13 print "Clank!" 14 def oilChange(self) : 15 pass 16 17 def perform(anything): 18 anything.speak() 19 anything.sit() 20 21 a = Dog() 22 b = Robot() 23 perform(a) 24 perform(b)
python的泛型使用简直称得上写意,定义两个类:Dog和Robot,而后直接用anything来声明一个perform泛型方法,在这个泛型方法中咱们分别调用了anything的speak()和sit()方法。
C++
1 class Dog { 2 public: 3 void speak() {} 4 void sit() {} 5 void reproduce() {} 6 }; 7 8 class Robot { 9 public: 10 void speak() {} 11 void sit() {} 12 void oilChange() { 13 }; 14 15 template<class T> void perform(T anything) { 16 anything.speak(); 17 anything.sit(); 18 } 19 20 int main() { 21 Dog d; 22 Robot r; 23 perform(d); 24 perform(r); 25 } ///:~
C++中的声明相对来讲条条框框多一点,可是一样可以实现咱们要达到的目的
Java:
1 public interface Performs { 2 void speak(); 3 void sit(); 4 } ///:~ 5 class PerformingDog extends Dog implements Performs { 6 public void speak() { print("Woof!"); } 7 public void sit() { print("Sitting"); } 8 public void reproduce() {} 9 } 10 class Robot implements Performs { 11 public void speak() { print("Click!"); } 12 public void sit() { print("Clank!"); } 13 public void oilChange() {} 14 } 15 class Communicate { 16 public static <T extends Performs> void perform(T performer) { 17 performer.speak(); 18 performer.sit(); 19 } 20 } 21 public class DogsAndRobots { 22 public static void main(String[] args) { 23 PerformingDog d = new PerformingDog(); 24 Robot r = new Robot(); 25 Communicate.perform(d); 26 Communicate.perform(r); 27 } 28 }
Java代码很奇怪的用到了一个接口Perform,而后在代码16行定义泛型方法的时候指明了<T extends Perform>(泛型方法的声明方式请见:【Java心得总结三】Java泛型上——初识泛型),声明泛型的时候咱们不是简单的直接<T>而是肯定了一个边界,至关于告诉编译器:传入的这个类型必定是继承自Perform接口的,那么T就必定有speak()和sit()这两个方法,你就放心的调用吧。
能够看出Java的泛型使用方式很繁琐,程序员须要考虑不少事情,不可以按照正常的思惟方式去处理。由于正常咱们是这么想的:我定义一个接收任何类型的方法,而后在这个方法中调用传入类型的一些方法,而你有没有这个方法,那是编译器要作的事情。
其实在python和C++中也是有这个接口的,只不过它是隐式的,程序员不须要本身去实现,编译器会自动处理这个状况。
解决方案2:
固然啦,不少状况下咱们仍是要使用Java中的泛型的,怎么解决这个头疼的问题呢?显示的传递类型的Class对象:
从上面的分析咱们能够看出Java的泛型类或者泛型方法中,对于传入的类型参数的类型信息是彻底丢失的,是被擦除掉的,咱们在里面连个new都办不到,这时候咱们就能够利用Java的RTTI即运行时类型信息(后续博文)来解决,以下:
1 class Building {} 2 class House extends Building {} 3 public class ClassTypeCapture<T> { 4 Class<T> kind; 5 T t; 6 public ClassTypeCapture(Class<T> kind) { 7 this.kind = kind; 8 } 9 public boolean f(Object arg) { 10 return kind.isInstance(arg); 11 } 12 public void newT(){ 13 t = kind.newInstance(); 14 } 15 public static void main(String[] args) { 16 ClassTypeCapture<Building> ctt1 = 17 new ClassTypeCapture<Building>(Building.class); 18 System.out.println(ctt1.f(new Building())); 19 System.out.println(ctt1.f(new House())); 20 ClassTypeCapture<House> ctt2 = 21 new ClassTypeCapture<House>(House.class); 22 System.out.println(ctt2.f(new Building())); 23 System.out.println(ctt2.f(new House())); 24 } 25 }/* Output: 26 true 27 false 28 true 29 *///:~
在前面的例子中咱们利用instanceof来判断类型失败,由于泛型中类型信息已经被擦除了,代码第10行这里咱们使用动态的isInstance(),而且传入类型标签Class<T>这样的话咱们只要在声明泛型类时,利用构造函数将它的Class类型信息传入到泛化类中,这样就补偿擦除问题
而在代码第13行这里咱们一样可利用工厂对象Class对象来经过newInstance()方法获得一个T类型的实例。(这在C++中彻底能够利用t = new T();实现,可是Java中丢失了类型信息,我没法知道T类型是否拥有无参构造函数)
(上面提到的Class、isInstance(),newInstance()等Java中类型信息的相关后续博文中我本身再总结)
解决方案3:
在解决方案1中咱们提到了,利用边界来解决Java对泛型的类型擦除问题。就是咱们声明一个接口,而后在声明泛化类或者泛化方法的时候,显示的告诉编译器<T extends Interface>其中Interface是咱们任意声明的一个接口,这样在内部咱们就可以知道T拥有哪些方法和T的部分类型信息。
4、通配符之协变、逆变
在使用Java中的容器的时候,咱们常常会遇到相似List<? extends Fruit>这种声明,这里问号?就是通配符。Fruit是一个水果类型基类,它的导出类型有Apple、Orange等等。
协变:
1 class Fruit {} 2 class Apple extends Fruit {} 3 class Jonathan extends Apple {} 4 class Orange extends Fruit {} 5 public class CovariantArrays { 6 public static void main(String[] args) { 7 Fruit[] fruit = new Apple[10]; 8 fruit[0] = new Apple(); // OK 9 fruit[1] = new Jonathan(); // OK 10 // Runtime type is Apple[], not Fruit[] or Orange[]: 11 try { 12 // Compiler allows you to add Fruit: 13 fruit[0] = new Fruit(); // ArrayStoreException 14 } catch(Exception e) { System.out.println(e); } 15 try { 16 // Compiler allows you to add Oranges: 17 fruit[0] = new Orange(); // ArrayStoreException 18 } catch(Exception e) { System.out.println(e); } 19 } 20 } /* Output: 21 java.lang.ArrayStoreException: Fruit 22 java.lang.ArrayStoreException: Orange 23 *///:~
首先咱们观察一下数组当中的协变(协变就是子类型能够被看成基类型使用),Java数组是支持协变的。如上述代码,咱们会发现声明的一个Apple数组用Fruit引用来存储,可是当咱们往里添加元素的时候咱们只能添加Apple对象及其子类型的对象,若是试图添加别的Fruit的子类型如Orange,那么在编译器就会报错,这是很是合理的,一个Apple类型的数组很明显不能放Orange进去;可是在代码13行咱们会发现,若是想要将Fruit基类型的对象放入,编译器是容许的,由于咱们的数组引用是Fruit类型的,可是在运行时编译器会发现实际上Fruit引用处理的是一个Apple数组,这是就会抛出异常。
然而咱们把数组的这个操做翻译到List上去,以下:
1 public class GenericsAndCovariance { 2 public static void main(String[] args) { 3 // Wildcards allow covariance: 4 List<? extends Fruit> flist = new ArrayList<Apple>(); 5 // Compile Error: can’t add any type of object: 6 // flist.add(new Apple()); 7 // flist.add(new Fruit()); 8 // flist.add(new Object()); 9 flist.add(null); // Legal but uninteresting 10 // We know that it returns at least Fruit: 11 Fruit f = flist.get(0); 12 } 13 } ///:~
咱们这里使用了通配符<? extends Fruit>,能够理解为:具备任何从Fruit继承的类型的列表。咱们会发现不只仅是Orange对象不容许放入List,这时候极端的连Apple都不容许咱们放入这个List中。这说明了一个问题List是不能像数组那样拥有协变性。
这里为何会出现这样的状况,经过查看ArrayList的源码咱们会发现:当咱们声明ArrayList<? extends Fruit>中的add()的参数也变成了"? extends Fruit",这时候编译器没法知道你具体要添加的是Fruit的哪一个具体子类型,那么它就会不接受任何类型的Fruit。
可是这里咱们发现咱们可以正常的get()出一个元素的,很好理解,由于咱们声明的类型参数是<? extends Fruit>,编译器确定能够安全的将元素返回,应为我知道放在List中的必定是一个Fruit,那么返回就好。
逆变:
上面咱们发现get方法是能够的,那么当咱们想用set方法或者add方法的时候怎么办?就可使用逆变即超类型通配符。以下:
1 public class SuperTypeWildcards { 2 static void writeTo(List<? super Apple> apples) { 3 apples.add(new Apple()); 4 apples.add(new Jonathan()); 5 // apples.add(new Fruit()); // Error 6 } 7 } ///:~
这里<? super Apple>意即这个List存放的是Apple的某种基类型,那么我将Apple或其子类型放入到这个List中确定是安全的。
总结一下:
<? super T>逆变指明泛型类持有T的基类,则T确定能够放入
<? extends T>指明泛型类持有T的导出类,则返回值必定可做为T的协变类型返回
说了这么多,总结了一堆也发现了Java泛型真的很渣,很差用,对程序员的要求会更高一些,一不当心就会出错。这也就是咱们使用类库中的泛化类时常看到各类各样的警告的缘由了。。。
参考——《Java编程思想第4版》
上面在通配符这里本人理解还不是很透彻,之后我也会根据本身理解修改整理。