Java 泛型
| 本文总阅读量次1. 概念
其他参数术语:
参数化的类型(parameterized type):List<String>
实际类型参数(type arguments):String
泛型(generic type):List<E>
形式类型参数(formal parameter types):E
无限制通配符类型(unbounded wildcards):List<?>
原生态类型(raw type):List
有限制类型参数(bounded type parameter):<E extends Number>
递归类型限制(recursive type restriction):<T extends Comparable<T>>
有限制通配符类型(bounded wildcards):List<? extends Number>
泛型方法(generic method):static <E> List<E> asList(E[] a)
类型令牌(type token):String.class
– 摘自《Effective Java》
Java集合有个缺点:集合对元素类型没有任何限制,这样就会引发一些问题,例如:创建一个只保存Dog对象的集合,但是程序也能将Cat对象放进去。由于把对象放进集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此去除集合元素后通常还需要进行强制类型转换。
为了解决上述问题,从JDK1.5之后,Java引入了“参数化类型(parameterized type)”的概念,Java的参数化类型被称为泛型(Generic)。
所谓泛型:就是允许在定义类、接口时指定类型形参(type parameters),这个类型形参将在声明变量、创建对象时确定。泛型的作用就是在编译时保证类型安全。
2. 使用
定义泛型接口、类示例:
1 | //定义接口时指定一个类型形参 |
注意:
- 包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参(type arguments),从而可以动态生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
- 当创建泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如:为
Apple<T>
类定义构造器,其构造器名依然是Apple
,而不是Apple<T>
,但调用构造器时可以使用Apple<T>
,此时T应该为实参类型。
2.1 从泛型类派生子类
当创建子类使用泛型接口或类时,不能再包含类型形参。如下代码时错误的:public class A extends Apple<T>{}
正确方式如下:public class A extends Apple<String>{}
类的静态变量和方法在所有的实例间共享,所以在静态方法、静态初始化或者静态变量的声明和初始化中不允许使用类型形参。原因见4.3 不能声明静态字段的类型为类型参数
由于系统对于泛型类或接口并不会生成真正的泛型类或接口(即逻辑上的子类,并不是生成真正的子类),所以instanceof运算符后不能使用泛型类。(具体原因见4.4 不能使用参数化类型强制类型转换或者instanceof)如下的代码时错误的:
1 | Collection cs = new ArrayList<String>(); |
2.2 类型通配符
类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型。使用通配符比显式声明通配符声明类型形参更加清晰准确,所以在可能的情况下,使用通配符更好。
2.2.1 不受约束的通配符
通配符可用于各种情况:作为参数,字段或局部变量的类型;有时作为返回类型(虽然更好的编程实践更具体)。通配符从不用作泛型方法调用,泛型类实例创建或超类型的类型参数。
如果满足下面的条件任意一个,就可以使用不受约束通配符:
- 如果你正在编写可以使用Object类中提供的方法实现的方法。
- 当代码使用在泛型类中不依赖类型参数方法时。例如:List.size 或者 List.clear。 事实上,Class<?>经常被使用,因为Class
中的大多数方法不依赖T。
使用通配符时,不能将元素放入未知类型的集合中。例如:
1 | List<?> list = new ArrayList<String>(); |
2.2.2 上限通配符
使用? extend type
表示所有type泛型类的子类(包含type本身)。
2.2.3 下限通配符
使用? super type
表示所有type泛型类的父类(包含type本身)。只能用于泛型方法(有待验证)。
2.2.4 通配符捕获和Helper方法
在一些情况下,编译器会推断一个通配符的类型。例如,一个列表可以被定义为List<?>
,当评估一个表达式时,编译器会从代码中推断一个特定类型。此方案称为通配符捕获。
1 | public class WildcardError { |
上面的例子中,i.set
方法编译异常,类型参数List<?>
为不确定类型参数,所以i.get(0)
获取的类型参数不确定,因此i.set
方法不能将未知类型放入i
中。(其中i.set默认是i.set(Integer,Object),因为不确定i.get(0)的类型,所以产生编译问题)解决方案如下:
1 | public class WildcardFixed { |
2.2.5 通配符和子类型
如泛型,继承和子类型中所述,泛型类或接口仅仅因为它们的类型不同而无关。但是,您可以使用通配符在泛型类或接口之间创建关系。下图是Number和Integer之间的继承关系:
2.3 泛型方法
示例:1
2
3
4
5static <T> void fromArrayToCollection(T[] a, Collection<T> c){
for(T o:a){
c.add(o);
}
}
上面示例中,定义了一个泛型方法,该泛型方法中定义了一个T类型形参,这个T类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中的定义的类型形参则可以在整个接口、类中使用。
与类、接口中使用泛型参数不同的是,方法中的泛型参数无需显式传入实际类型参数,根据实参推断类型形参的值。如果编译器不能推断你希望它拥有的类型,可以通过一个显示的类型参数(explicit type parameter)来告诉它要使用哪种类型。
泛型方法的用法格式:1
2
3修饰符 <T,S> 返回值类型 方法名(形参列表){
//方法体
}
提示:
如果某个方法中一个形参(a)的类型或返回值类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符,因为形参(a)、或返回值与该形参(b)的类型,如果形参(b)的类型无法确定,程序无法定义形参(a)的类型。在这种情况下,只能考虑使用在方法签名中声明类型形参。
类型通配符与显式声明类型形参区别:
- 类型通配符即可在方法签名中定义形参的类型,也可以用于定义变量的类型。但泛型方法中类型形参必须在对应方法中显式声明。
- 泛型方法允许类型形参用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的依赖关系,不应该使用泛型方法。
2.4 泛型使用准则
“in”变量:in变量向代码提供数据。想象复制方法有两个参数:
copy(src, dest)
。src
参数提供复制数据,因此时”in”参数。
“out”变量:out变量保存数据以供其他地方使用。在复制的例子中,copy(src, dest)
,dest
参数接受数据,因此时”out”参数。
- 使用
extends
关键字定义带有上限通配符的“in”变量。 - 使用
super
关键字定义带有下限通配符的“out”变量。 - 在可以使用Object类中定义的方法访问“in”变量的情况下,使用无界通配符。
- 在代码需要作为“in”和“out”变量访问的情况下,不要使用通配符。
3. 泛型的擦除与转换
泛型被引入Java语言,以便在编译时提供更严格的类型检查并支持通用编程(向上兼容)。为了实现泛型,Java编译器将类型擦除应用于:
- 使用边界替换所有在泛型中的类型参数或者如果类型参数是无界的则使用
Object
替换。因此生成的字节码只包含通用的类,接口和方法。 - 如果必要,插入类型强制转换来保证类型安全。
- 生成桥接方法以保留扩展泛型类型中的多态性。
对于以上3点,1和3可能在3.1和3.2中会详细说明,但是第二点可能不是那么清楚,如果有必要,类型擦除时,会进行强制类型转换。一般这种情况包括:
- 方法的返回类型是类型参数;
- 在访问数据域时,域的类型是一个类型参数。
例如:
项目中的代码:1
2
3List<String> list1 = new ArrayList<>();
list1.add("Hell");
System.out.println(list1.get(0));
编译后:1
2
3List list1 = new ArrayList();
list1.add("Hell");
System.out.println((String)list1.get(0));
字节码,字节码命令请参阅Chapter 6. The Java Virtual Machine Instruction Set:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Code:
0: new #3 // class java/util/ArrayList
3: dup
4: invokespecial #4 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: ldc #5 // String Hell
11: invokeinterface #6, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
16: pop
17: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_1
21: iconst_0
22: invokeinterface #8, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #9 // class java/lang/String 强制类型转换校验是否为String类型
30: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return
上面的例子说明,编译器在擦除泛型代码时,确实保留了List
详细了解请参阅Insert type casts if necessary to preserve type safety
3.1 泛型方法的擦除
Java编译器也会擦除泛型方法中的类型参数。
例如:
1 |
|
因为T是无限制的,所以Java编译器会使用Object代替它,如下:
1 | public static int count(Object[] anArray, Object elem) { |
3.2 类型擦除的影响和桥方法
在编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个称为桥接方法的合成方法,作为类型擦除过程的一部分。您通常不需要担心桥接方法,但如果出现在堆栈跟踪中,您可能会感到困惑。
生成桥接方法以保留扩展泛型类型中的多态性。
例如:
1 | public class Node<T> { |
考虑如下代码:1
2
3
4MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = mn.data; // Causes a ClassCastException to be thrown.
类型擦除后, 代码变成:1
2
3
4MyNode mn = new MyNode(5);
Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello");
Integer x = (String)mn.data; // Causes a ClassCastException to be thrown.
代码执行逻辑如下:
n.setData("Hello")
使得MyNode类对象中的setData(Object)
被执行。
在setData(Object)
方法体内,对象的数据字段引用被分配为String。
通过mn引用的相同对象数据字段,可以访问、且期望是Interger类型。
尝试分配String到Integer造成ClassCastException。
类型擦除后代码:
1 | public class Node { |
在类型擦除之后,方法签名不匹配。 Node方法变为setData(Object),MyNode方法变为setData(Integer)。因此,MyNode setData方法不会覆盖Node setData方法。 为了解决这个问题并在类型擦除后保留泛型类型的多态性,Java编译器会生成一个桥接方法,以确保子类型按预期工作。对于MyNode类,编译器为setData生成以下桥接方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
桥接方法与类型擦除后的Node类的setData方法具有相同的方法签名,委托给原始的setData方法(桥接方法在字节码中可见,javap -c class
)。
3.3 不可具体化类型
可具体化类型是在运行时类型信息完全可用的一种类型。包括基本类型,非泛型类型,原始类型,无界的通配符调用。唯一可具体化参数化类型是无限制通配符类型,如List<?>
和Map<?,?>
。
不可具体化类型是类型信息在编译时通过类型擦除被删除————调用未定义为无界通配符的泛型类型。不可具体化的类型在运行时不是所有信息都可用。不可具体化类型的示例是List <String>
和List <Number>
; JVM无法在运行时区分这些类型。如4 泛型的限制中所示,在某些情况下,不能使用不可具体化的类型:例如,在instanceof
表达式的实例中,或作为数组中的元素。
3.4 堆污染
堆污染发生在当参数化类型的变量引用不是该参数化类型的对象时。如果程序执行某些操作,在编译时产生未经检查的警告,则会出现这种情况。如果在编译时(在编译时类型检查规则的限制内)或在运行时,一个包含参数化类型操作的正确性不能被验证,则会生成未经检查的警告。例如,在混合原始类型和参数化类型时,或者在执行未经检查的强制转换时,会发生堆污染。
在通常情况下,当所有代码在相同时间被编译,编译器为潜在的堆污染产生一个未经检查警告来引起你的注意。如果你分开编译代码的各个部分,很难检查出堆污染的潜在风险。如果你确保你的代码编译没有警告,则不会有堆污染可以发生。
3.5 使用不可具体化形参的可变参数方法的潜在漏洞
包含可变输入参数泛型方法可以造成堆污染。
考虑如下class:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class ArrayBuilder {
public static <T> void addToList (List<T> listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l; // Valid
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0); // ClassCastException thrown here
}
}
如下例子,HeapPollutionExample
使用ArrayBuilder
类:
1 | public class HeapPollutionExample { |
当编译时,如下ArrayBuilder.addToList
方法的定义将产生warning:warning: [varargs] Possible heap pollution from parameterized vararg type T
当编译器遇到一个可变参数方法,它转换可变形参为数组。然而,Java编程语言不允许参数化类型数组的创建。在ArrrayBuilder.addToList
方法中,编译器转换可变形参T...
要素为T[]
要素。因为类型擦除,编译器转换可变形参为Object[]
要素。所以,有堆污染的可能性。
如下声明分配可变形参给对象数组:1
Object[] objectArray = l;
这种声明可能引起堆污染。可以将与可变形参l
的参数化类型匹配的值分配给变量objectArray,因此可以分配给l
。然而,在此声明中,编译器不能生成一个未经检查警告。编译器早已在转换可变形参List<String>...l
到形参List[] l
时生成警告。这个声明是有效的;l
变量的类型是List[]
,是Object[]
的子类型。
因此,如果将任何类型的List对象分配给objectArray数组的任何数组组件,编译器不会发出警告或错误,如下所示:ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
在运行时,JVM在以下语句中抛出ClassCastException
:1
2// ClassCastException thrown here
String s = l[0].get(0);
存储在变量l
的第一个数组组件中的对象具有List<Integer>
类型,但此语句需要一个List <String>
类型的对象。
3.6 使用不可具体化的形参防止可变参数方法发出警告
如果声明具有参数化类型参数的可变参数方法,并确保方法体不会因可变参数形参处理不当而抛出ClassCastException
或其他类似异常,你可以通过给静态和非构造方法声明添加如下的注解防止编译器给这些可变参数方法生成警告:@SafeVarargs
@SafeVarargs
注解是方法约定的记录部分;这个注释断言该方法的实现不会不正确地处理可变形参。
尽管不太可取,但通过在方法声明中添加以下内容来消除此类警告也是可以的:@SuppressWarnings({"unchecked", "varargs"})
但是,此方法不会消除从方法的调用点生成的警告。如果您不熟悉@SuppressWarnings
语法,请参阅注释。
4 泛型的限制
4.1 不能使用基本类型实例化通用类型
考虑如下代码:1
2
3
4
5
6
7
8
9
10
11
12class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
// ...
}
当创建一个Pair
对象,你不能为类型参数K
或者V
替换成基本类型:Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
你仅可以为类型参数K
或者V
替换非基本类型:Pair<Integer, Character> p = new Pair<>(8, 'a');
Java编译器自动装箱8
为Integer.valueOf(8)
和a
为Character('a')
:Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));
4.2 不能创建类型参数实例
1 | public static <E> void append(List<E> list) { |
有一种解决方案,你可以创建一个类型参数对象通过反射:1
2
3
4public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
你可以调用append
方法如下:1
2List<String> ls = new ArrayList<>();
append(ls, String.class);
4.3 不能声明静态字段的类型为类型参数
类的静态字段是类等级变量,被当前类的所有非静态对象共享。因此,类型参数的静态字段是不允许的。考虑如下类:1
2
3
4
5public class MobileDevice<T> {
private static T os;
// ...
}
如果类型参数的静态字段被允许,如下的代码将会混乱:1
2
3MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();
因为静态字段os
被phone
,pager
和pc
共享,什么是os
的真实类型?在相同的时间它不可能是Smartphone
,Pager
,和TablePc
。因此你不能创建类型参数静态字段。
4.4 不能使用参数化类型强制类型转换或者instanceof
因为Java编译器在泛型代码中擦除所有类型参数,您无法验证在运行时使用泛型类型的参数化类型:1
2
3
4
5public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}
传递到rtti
方法的参数化类型集合是:S = { ArrayList<Integer>, ArrayList<String> LinkedList<Character>, ... }
运行时不保持对类型参数的跟踪,因此它不能告诉ArrayList<Integer>
和ArrayList<String>
之间的不同。你最多是使用无限通配符来验证列表是否为ArrayList。1
2
3
4
5public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // OK; instanceof requires a reifiable type
// ...
}
}
通常,你不能强制转换参数化类型,除非它通过无限制通配符参数化。例如:1
2List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // compile-time error
但是,在一些情况下,编译器知道类型参数总是有效的,允许强制类型转换。例如:1
2List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // OK
4.5 不能创建参数化类型的数组
例:1
List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
如下代码说明在不同类型插入列表是发生了什么:1
2
3Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.
如果你使用泛型列表尝试相同的事情,将会有如下问题:1
2
3
4Object[] stringLists = new List<String>[]; // compiler error, but pretend it's allowed
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>(); // An ArrayStoreException should be thrown,
// but the runtime can't detect it.
如果参数化列表数组是允许的,之前的代码将失败抛出ArrayStoreException
。
4.6 不能创建、捕获或者抛出参数化类型对象
泛型类也不能直接或间接继承自Throwable。原因是因为在编译期和运行时都必须知道异常的确切类型。例如如下类将不编译:1
2
3
4
5// Extends Throwable indirectly
class MathException<T> extends Exception { /* ... */ } // compile-time error
// Extends Throwable directly
class QueueFullException<T> extends Throwable { /* ... */ // compile-time error
一个方法不能捕获一个类型参数的实例:1
2
3
4
5
6
7
8public static <T extends Exception, J> void execute(List<J> jobs) {
try {
for (J job : jobs)
// ...
} catch (T e) { // compile-time error
// ...
}
}
但是,你可以在一个throws
子句中使用类型参数:1
2
3
4
5class Parser<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}
如果不能参数化所抛出的异常,那么由于检查型异常的缘故,将不能编写出上述泛化的代码。
4.7 不能重载形式类型参数擦除后相同原始类型的方法
例:1
2
3
4public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}
重载将共享相同的类文件表示,并将生成编译时错误。
5. 泛型与数组
JDK1.5的泛型有一个很重要的设计原则:如果一段代码在编译时系统没有产生:“[unchecked]未经检查的转换“警告,则程序在运行时不会引发”ClassCastException“异常。
数组是协变的(convariant): 如果Sub为Super的子类型,那么数组类型Sub[]
就是Super[]
的子类型。
数组是具体化的(reified): 数组在运行时才知道并检查他们的元素类型约束。
泛型时不可变的(invariant): 对于任意两个不同的类型Type1和Type2,List<Type1>
既不是List<Type2>
的子类型,也不是List<Type2>
的超类型。
泛型只在编译时强化它们的类型信息,并在运行时丢弃(或者擦除)它们的元素类型信息。擦除就是使泛型可以与没有使用泛型的代码随意进行互用。
从技术角度来说,像E
、List<E>
和List<String>
这样的类型应称作不可具体化(non-reifiable)的类型。不可具体化类型是指其运行时表示法包含的信息比它编译时表示法包含的信息更少的类型。唯一可具体化的(reifiable)参数化类型是无限制通配符类型,如List<?>
和Map<?,?>
。创建无限制通配类型的数组是合法的;不可具体化的类型的数组转换只能在特殊情况下使用。
TIPs
如果以上都看完了,可以访问Questions试试自己是否真的懂了。
引用
- 本文链接: http://blog.programer.group/java/2019-05-06-java-generics/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!