1、首先看一个例子:
1 public class Generic { 2 public static void main(String []args){ 3 4 List arrayList = new ArrayList(); 5 arrayList.add("aaaa"); 6 arrayList.add(100); 7 8 for(int i = 0; i< arrayList.size();i++){ 9 String item = (String)arrayList.get(i); 10 System.out.println("泛型测试"+"item = " + item); 11 } 12 } 13 }
乍一看,好像没错,编译也没有报错,但是运行之后就会报错:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at Generic.main(Generic.java:12)
看到这基本就能想到哪里出错了,虽然一眼看过去我知道哪里出错了,可是你叫我解释,我也解释不清楚。
我们可以尝试改代码:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Generic { 5 public static void main(String []args){ 6 7 List<String> arrayList = new ArrayList<String>(); 8 arrayList.add("aaaa"); 9 arrayList.add(100); 10 11 for(int i = 0; i< arrayList.size();i++){ 12 String item = (String)arrayList.get(i); 13 System.out.println("泛型测试"+"item = " + item); 14 } 15 } 16 }
然后在编译期间报错:
Error:(9, 18) java: 对于add(int), 找不到合适的方法
方法 java.util.Collection.add(java.lang.String)不适用
(参数不匹配; int无法转换为java.lang.String)
方法 java.util.List.add(java.lang.String)不适用
(参数不匹配; int无法转换为java.lang.String)
问题在编译期间就被发现了,来一起了解下泛型。看看这玩意儿到底怎么回事,哎,不知道是学校没认真上还是我没认真听课。
2、泛型(依据网上资料整理)
Java的泛型是伪泛型,当代码编译成字节码时,代码中所有与泛型相关的信息会被擦除(我把其理解为C语言中的define的作用),正确理解泛型概念的首要前提是理解类型擦除。类型擦除比较难理解,我们从头开始讲。
泛型主要使用有三种:
1、泛型类
2、泛型方法
3、泛型接口
泛型类:
class GenericCla<T>{ T value; }
尖括号 <>
中的 T 被称作是类型参数,用于指代任何类型。T 只是一种习惯性写法,你也可以这样写:
class GenericCla<A>{ A value; }
但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的有:
1、T 代表一般的任何类。
2、E 代表 Element 的意思,或者 Exception 异常的意思。
3、K 代表 Key 的意思。
4、V 代表 Value 的意思,通常与 K 一起配合使用。
5、S 代表 Subtype 的意思,文章后面部分会讲解示意。
如果一个类被 <T>
的形式定义,那么它就被称为是泛型类。
1 GenericCla<String> gen1 = new GenericCla<>(); 2 GenericCla<Integer> gen2 = new GenericCla<>();
只要在对泛型类创建实例的时候,在尖括号中赋值相应的类型便是。T 就会被替换成对应的类型,如 String 或者是 Integer。你可以想象一下,当一个泛型类被创建时,内部自动扩展成下面的代码。
1 class GenericCla<String>{ 2 String value; 3 } 4 class GenericCla<Integer>{ 5 Integer value; 6 }
同时泛型不只可以接受一个类型参数,还可以接受多个类型参数:
1 class GenericC<K,V>{ 2 K key; 3 V value; 4 public K getKey(){ 5 return key; 6 } 7 public V getValue(){ 8 return value; 9 } 10 }
泛型方法:
1 class GenericClass{ 2 public<T> T getValue(){ 3 return null; 4 } 5 }
泛型方法与泛型类不同的地方是类型参数是写在返回值前面的。<T>
中的 T 被称为类型参数,而方法中的 T 被称为参数化类型,它不是运行时真正的参数。
当然泛型方法和泛型类是可以共存的:
1 class GenericCla2<T>{ 2 3 public void method(T t){ 4 System.out.println(); 5 } 6 public <T> T method1(T t){ 7 return t; 8 } 9 }
GenericCla2<T>
是泛型类,method是泛型类中的普通方法,而 method1是一个泛型方法。而泛型类中的类型参数与泛型方法中的类型参数是没有相应的联系的,泛型方法始终以自己定义的类型参数为准。
也就是说以上代码中红色和T和绿色T没有关系。
1 GenericCla2<String> t = new GenericCla2<String>(); 2 t.method("generic"); 3 Integer i = t.method1(new Integer(1));
使用如上代码通过编译可证明他们之间没有关系。同时我们可以改写为另外一种比较明了的写法:
1 class GenericCla2<T>{ 2 3 public void method(T t){ 4 System.out.println(); 5 } 6 public <E> E method1(E t){ 7 return t; 8 } 9 }
这样写就非常明显的可以区别两者之间的关系了。
泛型接口
泛型接口和泛型类区别不大,直接跳过
1 public interface in<T> { 2 }
通配符 ?
除了用 <T>表示泛型外,还有 <?>这种形式。? 被称为通配符。
可能有同学会想,已经有了 <T>的形式了,为什么还要引进 <?>这样的概念呢?看下面的例子:
1 class Base{} 2 class Sub extends Base{} 3 Sub sub = new Sub(); 4 Base base = sub;
Base 是 Sub 的父类,所以 Sub 的实例可以给一个 Base 引用赋值,那么
1 List<Sub> lsub = new ArrayList<>(); 2 List<Base> lbase = lsub;
最后一行代码成立吗?编译会通过吗?答案在下方,想清楚点开:
编译直接报错。
编译器不会让它通过的。Sub 是 Base 的子类,不代表 List<Sub>和 List<Base>有继承关系。
那么我们应当如何实现以上这种需求呢?通配符?的出现就是为了解决上述问题的:
通配符有 3 种形式。
1、<?>
被称作无限定的通配符。
2、<? extends T>
被称作有上限的通配符。
3、<? super T>
被称作有下限的通配符。
无限定通配符 <?>
无限定通配符经常与容器类配合使用,它其中的 ? 其实代表的是未知类型,所以涉及到 ? 时的操作,一定与具体类型无关。
1 public void testWildCards(Collection<?> collection){ 2 }
上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它表达了一种限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。如下:
1 class Test{ 2 public void testWildCards(Collection<?> collection){ 3 collection.add(1); 4 collection.add("1"); 5 collection.add(new Object()); 6 } 7 }
编译直接报错:
Error:(119, 25) java: 不兼容的类型: int无法转换为capture#1, 共 ?
Error:(120, 25) java: 不兼容的类型: java.lang.String无法转换为capture#2, 共 ?
Error:(121, 25) java: 不兼容的类型: java.lang.Object无法转换为capture#3, 共 ?
以上得,当 <?>
存在时,Collection 对象丧失了 add() 方法的功能,编译器无法通过。
我们再看代码。
1 ArrayList<?> arr2; 2 arr2 = new ArrayList<String>(); 3 ArrayList<Integer> list=new ArrayList<Integer>(); 4 list.add(1); 5 list.add(2); 6 arr2=list; 7 arr2.get(0);
以上代码可以正常编译运行。但是:
1 ArrayList<String> list1=list;
无法通过编译。
1 ArrayList<?> arr2; 2 arr2 = new ArrayList<String>(); 3 ArrayList<Integer> list=new ArrayList<Integer>(); 4 list.add(1); 5 arr2=list; 6 ArrayList lis1t=list; 7 lis1t.add(1);
8 arr2.add(1);
9 arr2.get(0);
以上代码红色部分无法编译通过,但是蓝色部分可以通过编译,也就是说加了通配符<?>的指针无法使用add功能,但是没加通配符的指针可以使用add功能。
个人理解通配符<?>
的作用 提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能(Add涉及到具体类型)。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空?那通配符<?>
既然作用这么渺小,那么为什么还要引用它呢? 网上博主认为,提高了代码的可读性,程序员看到这段代码时,就能够迅速对此建立极简洁的印象,能够快速推断源码作者的意图。
<? extends T>
<?>代表着类型未知,但是我们的确需要对于类型的描述再精确一点,我们希望在一个范围内确定类别,比如类型 A 及 类型 A 的子类都可以。<? extends T> 代表类型 T 及 T 的子类。
public void testSub(Collection<? extends Base> para){ }
上面代码中,para 这个 Collection 接受 Base 及 Base 的子类的类型。 但是,它仍然丧失了写操作的能力。也就是说
1 para.add(new Sub()); 2 para.add(new Base());
上面这两行代码仍然编译不通过。 没有关系,我们不知道具体类型,但是我们至少清楚了类型的范围。
<? super T> 与<? extends T>相对应,代表 T 及 T 的超类,也就是
<? super T> = {X|X=T或者T的超类}
1 public void testSuper(Collection<? super Sub> para){ }
<? super T>神奇的地方在于,它拥有一定程度的写操作的能力。
1 public void testSuper(Collection<? super Sub> para){ 2 para.add(new Sub());//编译通过 3 para.add(new Base());//编译不通过 4 }
通配符与类型参数的区别:一般而言,通配符能干的事情都可以用类型参数替换。 比如
public void testWildCards(Collection<?> collection){}
可以被
1 public <T> void test(Collection<T> collection){}
替代,值得注意的是,如果用泛型方法来取代通配符,那么上面代码中 collection 是能够进行写操作的。只不过要进行强制转换。
1 public <T> void test(Collection<T> collection){ 2 collection.add((T)new Integer(12)); 3 collection.add((T)"123"); 4 }
需要特别注意的是,类型参数适用于参数之间的类别依赖关系,举例说明。
1 public class Test2 <T,E extends T>{ 2 T value1; 3 E value2; 4 } 5 public <D,S extends D> void test(D d,S s){ 6 7 }
E 类型是 T 类型的子类,显然这种情况类型参数更适合。
有一种情况是,通配符和类型参数一起使用。
1 public <T> void test(T t,Collection<? extends T> collection){ 2 3 }
如果一个方法的返回类型依赖于参数的类型,那么通配符也无能为力。
1 public T test1(T t){ 2 return value1; 3 }
类型擦除
如果没有泛型,我们之前是怎么实现类似泛型这种功能的?
1 import java.lang.reflect.Field; 2 import java.util.ArrayList; 3 import java.util.List; 4 5 public class Generic { 6 public static void main(String []args){ 7 8 MyClass myClass=new MyClass(); 9 myClass.setValue(1); 10 int value=(int)myClass.getValue(); 11 myClass.setValue("666"); 12 String str= (String) myClass.getValue(); 13 System.out.println(value+str); 14 } 15 } 16 class MyClass{ 17 Object value; 18 public void setValue(Object object){ 19 this.value=object; 20 } 21 public Object getValue(){ 22 return this.value; 23 } 24 }
以上,通过强制类型转换实现了类似泛型的功能。但是发现了问题,首先强制转换比较麻烦,使用不方便,代码也很不好看。
那么泛型究竟内部是怎么处理的呢?下面给出一个泛型的例子:
1 import java.util.ArrayList; 2 import java.util.List; 3 4 public class Generic { 5 public static void main(String []args){ 6 7 List<String> l1 = new ArrayList<String>(); 8 List<Integer> l2 = new ArrayList<Integer>(); 9 10 System.out.println(l1.getClass() == l2.getClass()); 11 } 12 }
上面这段代码你猜结果是什么?(答案在下方,想清楚点击鼠标打开)
1 true 2 结果表明List<String>和 List<Integer>在 jvm 中的 Class 都是 List.class。
也就是说Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程称为类型擦除。
也就是说在运行期间和泛型有关的信息都被擦除了。那么String和Integer岂不是没有存在感?
1 import java.lang.reflect.Field; 2 import java.util.ArrayList; 3 import java.util.List; 4 5 public class Generic { 6 public static void main(String []args){ 7 8 List<String> list = new ArrayList<String>(); 9 list.add("hello"); 10 Class cls= list.getClass(); 11 System.out.println("List class is:"+ cls.getName()); 12 Field[] fs = cls.getDeclaredFields(); 13 for ( Field f:fs) { 14 System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); 15 } 16 } 17 }
上面使用Java的反射机制获得了List<String>的类名和详细信息,输出如下:
List class is:java.util.ArrayList
Field name serialVersionUID type:long
Field name DEFAULT_CAPACITY type:int
Field name EMPTY_ELEMENTDATA type:[Ljava.lang.Object;
Field name DEFAULTCAPACITY_EMPTY_ELEMENTDATA type:[Ljava.lang.Object;
Field name elementData type:[Ljava.lang.Object;
Field name size type:int
Field name MAX_ARRAY_SIZE type:int
注意红色部分,也就是说list中的成员都是Object类型的,那么是不是就是说泛型被类型擦除后就直接被替换成了Obejct实现呢?这种说法好像正确,但是不能说完全正确。
下面看这段代码,注意红色部分:
1 import java.lang.reflect.Field; 2 import java.util.ArrayList; 3 import java.util.List; 4 5 public class Generic { 6 public static void main(String []args){ 7 8 GenericClass_ genericClass_=new GenericClass_(); 9 Class myclass = genericClass_.getClass(); 10 System.out.println("List class is:"+ myclass.getName()); 11 Field[] fs = myclass.getDeclaredFields(); 12 for ( Field f:fs) { 13 System.out.println("Field name "+f.getName()+" type:"+f.getType().getName()); 14 } 15 } 16 } 17 class GenericClass_<T extends String>{ 18 T value; 19 public void setValue(T object){ 20 this.value=object; 21 } 22 public T getValue(){ 23 return this.value; 24 } 25 }
输出结果如下:
List class is:GenericClass_
Field name value type:java.lang.String
我个人理解是直接写<T>相当于写了<T extends Object>类似Java的类继承,当指明上界时类型参数就被替换成指定上限类型参数。
所以,在反射中:
1 public class Generic<T>{ 2 T object; 3 4 public Generic(T object) { 5 this.object = object; 6 } 7 8 public void add(T object){ 9 10 } 11 }
add() 这个方法对应的 Method 的签名应该是 Object.class。
1 Generic<String> generic= new Generic<String>("hello"); 2 Class eclz = generic.getClass(); 3 System.out.println("erasure class is:"+eclz.getName()); 4 5 Method[] methods = eclz.getDeclaredMethods(); 6 for ( Method m:methods ){ 7 System.out.println(" method:"+m.toString()); 8 }
打印结果是:
method:public void Generic.add(java.lang.Object)
也就是说,如果你要在反射中找到 add 对应的 Method,你应该调用 getDeclaredMethod("add",Object.class)
否则程序会报错,提示没有这么一个方法,原因就是类型擦除的时候,T 被替换成 Object 类型了。
类型擦除带来的局限性
类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。但也因为类型擦除,它会抹掉很多继承相关的特性,这是它带来的局限性。
理解类型擦除有利于我们绕过开发当中可能遇到的雷区,同样理解类型擦除也能让我们绕过泛型本身的一些限制。比如
1 public class Generic { 2 public static void main(String[] args) { 3 4 List<Integer> arrayList = new ArrayList<Integer>(); 5 arrayList.add("aaaa"); 6 arrayList.add(100); 7 } 8 }
正常情况下,因为泛型的限制,编译器不让最后一行代码编译通过,因为类似不匹配,但是,基于对类型擦除的了解,利用反射,我们可以绕过这个限制。
1 public interface List<E> extends Collection<E>{ 2 3 boolean add(E e); 4 }
上面是 List 和其中的 add() 方法的源码定义。
因为 E 代表任意的类型,所以类型擦除时,add 方法其实等同于:
1 boolean add(Object obj);
那么,利用反射,我们绕过编译器去调用 add 方法。
1 public class ToolTest { 2 3 4 public static void main(String[] args) { 5 List<Integer> ls = new ArrayList<>(); 6 ls.add(23); 7 // ls.add("text"); 8 try { 9 Method method = ls.getClass().getDeclaredMethod("add",Object.class); 10 11 12 method.invoke(ls,"test"); 13 method.invoke(ls,42.9f); 14 } catch (NoSuchMethodException e) { 15 // TODO Auto-generated catch block 16 e.printStackTrace(); 17 } catch (SecurityException e) { 18 // TODO Auto-generated catch block 19 e.printStackTrace(); 20 } catch (IllegalAccessException e) { 21 // TODO Auto-generated catch block 22 e.printStackTrace(); 23 } catch (IllegalArgumentException e) { 24 // TODO Auto-generated catch block 25 e.printStackTrace(); 26 } catch (InvocationTargetException e) { 27 // TODO Auto-generated catch block 28 e.printStackTrace(); 29 } 30 31 for ( Object o: ls){ 32 System.out.println(o); 33 } 34 35 } 36 37 }
输出结果是:
23
test
42.9
可以看到,利用类型擦除的原理,用反射的手段就绕过了正常开发中编译器不允许的操作限制。
泛型中值得注意的地方
泛型类或者泛型方法中,不接受 8 种基本数据类型。
所以,你没有办法进行这样的编码。
1 List<int> li = new ArrayList<>(); 2 List<boolean> li = new ArrayList<>();
需要使用它们对应的包装类:
1 List<Integer> li = new ArrayList<>(); 2 List<Boolean> li1 = new ArrayList<>();
对泛型方法的困惑
1 public <T> T test(T t){ 2 return null; 3 }
有的同学可能对于连续的两个 T 感到困惑,其实 <T>
是为了说明类型参数,是声明,而后面的不带尖括号的 T 是方法的返回值类型。
你可以相像一下,如果 test() 这样被调用
1 test("123");
那么实际上相当于
1 public String test(String t);
Java 不能创建具体类型的泛型数组
1 List<Integer>[] li2 = new ArrayList<Integer>[]; 2 List<Boolean> li3 = new ArrayList<Boolean>[];
这两行代码是无法在编译器中编译通过的。原因还是类型擦除带来的影响。
List<Integer>和 List<Boolean>在 jvm 中等同于List<Object>,所有的类型信息都被擦除,程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean>类型。
但是,
1 List<?>[] li3 = new ArrayList<?>[10]; 2 li3[1] = new ArrayList<String>(); 3 List<?> v = li3[1];
借助于无限定通配符却可以,前面讲过 ?代表未知类型,所以它涉及的操作都基本上与类型无关,因此 jvm 不需要针对它对类型作判断,因此它能编译通过,但是,只提供了数组中的元素因为通配符原因,它只能读,不能写。比如,上面的 v 这个局部变量,它只能进行 get() 操作,不能进行 add() 操作,这个在前面通配符的内容小节中已经讲过。
泛型,并不神奇
我们可以看到,泛型其实并没有什么神奇的地方,泛型代码能做的非泛型代码也能做。
而类型擦除,是泛型能够与之前的 java 版本代码兼容共存的原因。
可量也正因为类型擦除导致了一些隐患与局限。