导读
泛型是Java最基础的语法之一,不过这种语法依然有值得一说的地方:如果仅停留在泛型的基本使用上,泛型当然很简单;但如果从字节码层次来看泛型,将会发现更多泛型的本质。
泛型是Java最基础的语法之一,不过这种语法依然有值得一说的地方:如果仅停留在泛型的基本使用上,泛型当然很简单;但如果从字节码层次来看泛型,将会发现更多泛型的本质。
本文并不打算介绍泛型的基本用法,这些内容应该属于普通的使用,如果连简单的在集合类中使用泛型都不熟悉,或泛型类、泛型方法这些基础内容不熟,那么能力不足就要多读书,比如再翻翻手上的《疯狂Java讲义》。
本文讲解的是两个容易混淆的东西:List类型和List<?>之间的区别和联系。
List和List<?>的相似之处
首先要说的是:如果仅从意义上来看,List和List<?>看上去具有一定的相似之处:List代表集合元素可以是任意类型的列表;List<?>似乎也代表集合元素可以任意类型的列表!
事实上呢?并不是如此! List<?>代表集合元素无法确定的列表。
不过它们有相似的地方,由于List完全没有指定泛型,因此程序可以将泛型为任意类型的List(如List<Integer>、List<String>...等)赋值给List类型的变量;类似的,程序也可将泛型为任意类型的List(如List<Integer>、List<String>...等)赋值给List<?>类型的变量。
例如如下程序:
代码语言:javascript复制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)“。
原始类型会导致泛型擦除,这是一种非常危险的操作。例如如下程序:
代码语言:javascript复制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集合。
代码语言:javascript复制 // 使用Lambda表达式遍历list集合
list.forEach(System.out::println);
但是,如果你试图使用如下代码来遍历intList集合就会导致错误。
代码语言:javascript复制 for (Integer i : intList)
{
System.out.println(i);
}
上面代码编译时没有任何问题——道理很简单,因为intList的类型是List<Integer>,因此编译器会认为它的集合元素都是Integer,因此程序在for循环中声明它的集合元素为Integer类型——这合情合理。
但运行该程序就会导致如下运行时错误。
代码语言:javascript复制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讲义》。
对于如下程序:
代码语言:javascript复制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<?>的本质是一样。
例如如下使用原始类型的程序:
代码语言:javascript复制import java.util.*;
public class RawTypeTest
{
public static void main(String[] args)
{
List<Integer> inList = new ArrayList<>();
List rList = inList;
}
}
用javap分析上面程序的字节码,可看到如下输出:
代码语言:javascript复制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
}
对于如下使用通配符的程序:
代码语言:javascript复制import java.util.*;
public class WildcardTest
{
public static void main(String[] args)
{
List<Integer> inList = new ArrayList<>();
List<?> wList = inList;
}
}
用javap分析上面程序的字节码,同样可看到如下输出:
代码语言:javascript复制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
}
从上面字节码可以看到,泛型检查的主要工作是在编译阶段完成,编译之后生成的字节码并没有太大的差别。
本文结束