导读
泛型是Java最基础的语法之一,不过这种语法依然有值得一说的地方:如果仅停留在泛型的基本使用上,泛型当然很简单;但如果从字节码层次来看泛型,将会发现更多泛型的本质。
本文并不打算介绍泛型的基本用法,这些内容应该属于普通的使用,本文讲解的是两个容易混淆的东西:List类型和List<?>之间的区别和联系。
▊ List和List<?>的相似之处
首先要说的是:如果仅从意义上来看,List和List<?>看上去具有一定的相似之处:List代表集合元素可以是任意类型的列表;List<?>似乎也代表集合元素可以任意类型的列表!
事实上呢?并不是如此!List<?>代表集合元素无法确定的列表。
不过它们有相似的地方,由于List完全没有指定泛型,因此程序可以将泛型为任意类型的List(如List<Integer>、List<String>...等)赋值给List类型的变量;类似的,程序也可将泛型为任意类型的List(如List<Integer>、List<String>...等)赋值给List<?>类型的变量。
例如如下程序:
import java.util.*;public class GenericTest{ public static void main(String[] args) { List<Integer> intList = List.of(2, 3, 10); List<String> strList = List.of("java", "swift", "python"); // 下面两行代码都是正确的 List list1 = intList; List list2 = strList; // 下面两行代码也是正确的 List<?> list3 = intList; List<?> list4 = strList; }}
从上面代码可以看到,List<String>、List<Integer>类型的列表可以直接赋值给List、也可直接赋值给List<?>。
如果仅看上面程序,List和List<?>似乎差别不大?真的是这样吗?
▊ 原始类型擦除了泛型
首先需要说明一点:早期的Java是没有泛型的——Java 5才加入泛型,对于90后的小朋友来说,Java 5应该是一个古老的传说了。
正因为早期Java没有泛型,因此早期Java程序用List等集合类型时只能写成List,无法写成List<Integer>或List<String>!这样就造成了一个现状:虽然后来Java 5增加了泛型,但Java必须保留和早期程序的兼容,因此Java 5+必须兼容早期的写法:List不带泛型。
换句话来说,使用泛型类不带尖括号、具体类型的用法,其实是一种妥协:为了与早期程序的兼容。
也就是说:对于现在写的程序,谁要是使用泛型类时不填写具体类型,都应该打屁股哦。
注意:现在使用泛型类时,都应该为泛型指定具体的类型。
为了保持与早期程序兼容,Java允许在使用泛型类时不传入具体类型的搞法,被称为“原始类型(raw type)”。
原始类型会导致泛型擦除,这是一种非常危险的操作。例如如下程序:
import java.util.*;public class GenericErase{ public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); intList.add(20); intList.add(3); intList.add(5);
// 泛型擦除 List list = intList; // ① // list是List类型,因此可以添加String类型的元素 list.add("疯狂Java"); // ②
}}
上面①号代码使用了原始类型,这样就导致了泛型擦除——擦除了所有的泛型信息,因此程序可以在②号代码处向list集合添加String类型的元素。
那么问题来了,②号代码处是否可以向list集合(其实是List<Integer>集合)添加String类型的元素呢?
如果你不运行这个程序,你能得到正确答案吗?
答案是:完全可以添加进去!——这是因为原始类型导致泛型信息完全被擦除了。
因此你完全可以在②号代码后使用如下代码来遍历该list集合。
// 使用Lambda表达式遍历list集合 list.forEach(System.out::println);
但是,如果你试图使用如下代码来遍历intList集合就会导致错误。
for (Integer i : intList){ System.out.println(i);}
上面代码编译时没有任何问题——道理很简单,因为intList的类型是List<Integer>,因此编译器会认为它的集合元素都是Integer,因此程序在for循环中声明它的集合元素为Integer类型——这合情合理。
但运行该程序就会导致如下运行时错误。
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer
这就是原始类型问题:原始类型导致了泛型擦除,因此编译器不会执行泛型检查,这样就会给程序引入潜在的问题。
幸运的是,Java编译器非常智能,只要你的程序中包含了泛型擦除导致的潜在的错误,编译器就会提示unchecked警告。
那么问题来了,List<?>是否有这个问题呢?
▊ List<?>不能添加元素
很明显,List<?>是很规范的泛型用法,因此它不会导致泛型擦除,因此将List<Integer>、List<String>赋值给List<?>类型的变量完全不会导致上面的错误。
List<?>怎么处理的呢?Java的泛型规定:List<?>不允许添加任何类型的元素!
List<?>相当于上限是Object的通配符,因此List<?>完全相当于List<? extends Object>,这种指定通配符上限的泛型只能取出元素,不能添加元素。
注意:这种指定通配符上限的用法被称为泛型协变,关于泛型协变的深入介绍可参考《疯狂Java讲义》9.3节或参考《Effective Java》。
实际上,Google推荐的Android开发语言:Kotlin在处理泛型协变时更加简单粗暴,它不再搞什么上限、下限,而是直接用in、out来修饰泛型——out代表泛型协变、泛型协变只能出不能进;in代表泛型逆变,泛型逆变只能进不能出。相比之下,Kotlin在处理泛型型变、逆变时具有更好的可读性。
备注:如需了解Kotlin的泛型型变、逆变的内容,可参考《疯狂Kotlin讲义》。
对于如下程序:
import java.util.*;public class GenericWildcard{ public static void main(String[] args) { List<Integer> intList = new ArrayList<>(); intList.add(20); intList.add(3); intList.add(5);
// 泛型通配符,此处的本质就是泛型协变 List<?> list = intList; // ① // list是List类型,因此可以添加String类型的元素 list.add("疯狂Java"); // ②
}}
上面程序中①号代码将List<Integer>类型的变量赋值给List<?>变量,此时的本质就是泛型协变。
由于List<?>代表元素不确定类型的List集合,因此程序无法向 List<?>类型的集合中添加任何元素——因此Java编译器会禁止向list添加任何元素,故程序②号代码报错。
上面程序编译就会报错,这样程序就健壮多了。
▊ List和List<?>的本质是一样的
需要说明的是,泛型类并不存在!
泛型只是一种编译时的检查,因此List和List<?>的本质是一样。
例如如下使用原始类型的程序:
import java.util.*;public class RawTypeTest{ public static void main(String[] args) { List<Integer> inList = new ArrayList<>(); List rList = inList; }}
用javap分析上面程序的字节码,可看到如下输出:
public class RawTypeTest { public RawTypeTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
public static void main(java.lang.String[]); Code: 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: astore_2 10: return}
对于如下使用通配符的程序:
import java.util.*;public class WildcardTest{ public static void main(String[] args){ List<Integer> inList = new ArrayList<>(); List<?> wList = inList;
}}
用javap分析上面程序的字节码,同样可看到如下输出:
public class WildcardTest { public WildcardTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
public static void main(java.lang.String[]); Code: 0: new #2 // class java/util/ArrayList 3: dup 4: invokespecial #3 // Method java/util/ArrayList."<init>":()V 7: astore_1 8: aload_1 9: astore_2 10: return}
从上面字节码可以看到,泛型检查的主要工作是在编译阶段完成,编译之后生成的字节码并没有太大的差别。
—— 图书推荐 ——
《疯狂Java讲义(第5版)》
李刚 编著
本书深入介绍了Java编程的相关方面,《疯狂Java讲义》历时十年沉淀,经过无数Java学习者的反复验证,被包括北京大学在内的大量985、211高校的优秀教师引荐为参考资料、选作教材。
赠送1700分钟课程讲解视频、源代码、电子书、课件、面试题,提供作者亲自在线的微信+QQ答疑群+配套学习交流网站。
获取本书详情
扫码获取Java好课
《跟着李刚老师学Java》
如果喜欢本文
欢迎 在看丨留言丨分享至朋友圈 三连
热文推荐
▼点击阅读原文,了解本书详情~
本文分享自 博文视点Broadview 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!