第 8 章 泛型
java 集合的原生类型(即 Set、List、Map 等)有一个缺点——把对象放入集合以后,集合就会忘记对象的数据类型,当再次取出对象时,该对象的编译时类型变成了 Object (其运行时类型没变)。使用对象的方法前,需要强制转换类型。而泛型集合,可以记住集合中元素的类型(通过编译器在编译时,自动插入类型转换实现),并可以在编译时检查集合中元素的类型,如果试图向集合中添加不满足类型要求的对象,编译器会报错。
Java 泛型的设计原则是:如果程序在编译时没有发出警告,运行时就不会产生 ClassCastExecution 异常。
8.1 泛型的菱形语法
在 Java7 以前,创建泛型类对象时,类名后面必须显式给出类型实参。从 Java7 开始,java 允许在类名后面省略类型实参,只要给出一对尖括号(<>)即可,Java 自行推断出类型实参。这种语法称为"菱形"语法。例如:
//Java7以前
List<String> strList = new ArrayList<String>();
//Java7之后
List<String> srList = new ArrayList<>();
Map<String,List<String>> school = new HashMap<>();
8.2 深入泛型
泛型就是在定义类、接口、方法时使用类型形参,在声明变量、创建对象、调用方法时传入类型实参。泛型的实质:类型形参在整个类、接口和方法体内当成普通类型使用。从而定义的类、接口和方法可作为多种类型实参的模板使用。
8.3 泛型类的继承规则
8.3.1 类 A<B>
不是类 A<B的父类>
的子类
在数组中,Integer[] 可以赋给 Number[] 数组。如果把一个 Double 对象保存到该 Number[] 数组时,编译可以通过,但在运行时,抛出 ArrayStoreException异常,例如:
public class ArrayErr{
public static void main(String[] args){
Integer[] ia = new Integer[5];
Number[] na = ia;
na[0] = 0.5;
}
}
针对数组变量赋值的缺陷,Java 在设计泛型时进行了改进,当泛型类的两个类型实参之间存在父子关系时,对应的泛型类之间没有父子关系。例如:List<Integer>
不是 List<Number>
的子类,从而下面代码将会导致编译错误:
List<Integer> iList = new ArrayList<>();
List<Number> nList = iList;
8.3.1 定义泛型类或泛型接口类的实现类
当定义了泛型类、接口之后,可以为该类、接口定义实现类。在定义实现类时,必须给出泛型类、接口的类型实参。例如。
//下面语句是错误的
public class A extends Apple<T>{ }
//下面语句是正确的
public class A extends Apple<String>{ }
定义泛型类、接口的实现类时,也可以不指定类型实参。Java编译器可能发出警告:使用了未经检查或不安全的操作-----这就是泛型检查的警告。此时类型实参默认为 Object。目的是与遗留代码兼容:
public class A extends Apple{ }
当定义了泛型类、接口的实现类后,子类从父类或接口中继承的方法为:将父类或接口中所有方法的的类型变量替换为类型实参,因此,当需要在子类中覆盖父类或接口中的方法时,形参列表中的类型必须为类型实参。返回值类型也需注意,否则只是重载父类或接口中的方法。例如:
class Apple<T>{
private T info;
T getInfo(){
return info;
}
}
public class A2 extends Apple<String>{
//覆盖父类的方法
public String getInfo(){
...
}
}
子类 A2 从 Apple<String>
继承了两个方法:String getInfo() 和 void setInfo(String)。子类 A2 重写了 Apple 的 getInfo(),所以子类的 getInfo() 的返回值类型必须为 String。
从而在定义泛型接口或类的实现类时,不管是否指定类型实参,子类都不再是泛型类,而是普通类。
8.3.2 定义泛型接口或类的泛型子类
泛型类可以扩展或实现其他的泛型类或泛型接口。用法与普通类没有区别。例如:ArrayList<T>
类实现了List<T>
的接口。这意味着,ArrayList<Manager>
是 List<Manager>
的子类。易知 ArrayList<Manager>
不是 ArrayList<Employee>
的子类。定义泛型子类的格式如下:
class child<T> extends Parent<T>{...}
8.3.3 泛型类的非静态内部类
非静态内部类中可以直接使用外部泛型类的类型形参。也可以重新定义为泛型类。示例如下:
//内部类是泛型
public class LinkedStack<T>{
private class Node<U>{
U item;
Node<U> next;
Node(){
item = null;
next = null;
}
Node(U item, Node<U> next){
this.item = item;
this.next = next;
}
}
Node<T> top = new Node<>();
public void push(T item){
top = new Node<>(item, top);
}
public T pop(){
T temp;
if(top.next == null){
temp = null;
}
else{
temp = top.item;
top = top.next;
}
return temp;
}
}
//内部类不是泛型
public class Main{
public static void main(String[] args){
LinkedStack<Integer> stack = new LinkedStack<>();
stack.push(1);stack.push(2);
System.out.println(stack.pop());
}
}
class LinkedStack<T>{
class Node{
T item;
Node next;
Node(){
item = null;
next = null;
}
Node(T item, Node next){
this.item = item;
this.next = next;
}
}
Node top = new Node();
public void push(T item){
top = new Node(item, top);
}
public T pop(){
T temp;
if(top.next == null){
temp = null;
}
else{
temp = top.item;top = top.next;
}
return temp;
}
}
8.4 类型通配符
通配符类型可以很好的解决类 A<B>
不是类 A<B的父类>
的子类的问题,同时不会引入数组中子类数组赋给父类数组的问题。
8.4.1 使用类型通配符
为了表示泛型类的父类,可以使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给 List 集合,写作:List<?>
(意思是元素类型未知的 List 或元素类型为任意类型的 List )。List<?>
表示任何 List 的父类。可以定义如下方法:
//表示test方法对元素类型为任意类型的List都能处理
public void test(List<?> c){
for(int i = 0; i < csize(); i++){
System.out.println(c.get(i));
}
}
那么,给 test 方法传任意类型的 List,程序都可以访问集合 c 中的元素,不管 List 的类型实参是什么,在程序中,元素的类型都当作 Object。
但这种带通配符的 List 仅表示它是各种泛型的 List 的父类,并不能把元素加入其中,例如:
List<?> c = new ArrayList<String>();
c.add(new Object());
因为程序无法确定集合 c 中元素的类型,当调用 c.add() 添加元素时,由于不知道 c 中元素的具体类型,当添加的元素是 c 中元素的子类型或和 c 中元素类型无关的其他类型时,编译器会报错。
另一方面,程序可以调用 get() 返回 List<?>
集合中的元素,其返回值类型未知,但它肯定是一个 Object,因此,可以把 get() 返回值赋给一个 Object 类型的变量。
8.4.2 设定通配符上限
当直接使用 List<?>
时,表明这个 List 集合是任何 List 的父类。此时,List<?>
类型的变量可以引用元素类型为任意类型的 List,程序希望对通配符加以限定,让它只是部分 List 的父类。例如,希望它表示 List<Number>
、List<Integer>
、List<Double>
的父类,则 List<?>
可以具体化为 List<? extends Number>
,此时 List<? extends Number>
类型的变量只能引用元素类型为 Number 类型或其子类的 List,因此把 Number 称为这个通配符的上限。
8.4.3 设定通配符下限
此外,java 也允许设定通配符下限,将通配符可取的类型范围限定为 Type 或其父类,语法格式为:<? super Type>
,例如:
public static <T> T copy(Collection<? super T>dst, Collection<T>src){
T last = null;
for(T ele : src){
last = ele;
dst.add(ele);
}
return last;
}
代码意思为:形参 src 集合里的元素类型为 T,而 dst 集合里面的元素可取类型必须为 T 或 T 子类型。Collection<? super T>
是元素类型为 T 或 T 祖先类型的所有 Collection 集合的父类。
一般情况下,如果一个结构的元素类型为 ? extends E,我们只能从结构中取出元素,不能向结构中添加元素;如果结构的元素类型为 ? super E 时,我们只能向结构中添加 E 及 E 的子类型元素,不能从结构中读取元素。
8.4.4 类型形参的上限
java 泛型不仅可以给通配符设置限定,也可以给类型形参添加限定,表示类型实参只能取满足限定的类型。格式如下,注意,在类型变量限定中,不管限定类型是接口还是类,都用关键字 extends。
<T extends Comparable<T> > T min(T[] a)....
若类型实参不满足条件,编译器会报错:
public class Apple<T extends Number>{
T col;
public static void main(){
Apple<Integer> ai = new Apple<>();
Apple<Double> ai = new Apple<>();
//下面代码将引发编译异常,因为String不是Number的子类型,不能作为T的实际类型参数。
Apple<String> ai = new Apple<>();
}
}
程序可以为类型变量设定多个上限(至多有一个父类上限,可以有多个接口上限),表明该类型形参必须是父类或父类的子类,并且实现了多个接口。当为类型变量添加多个限定时,所有的接口限定必须位于类限定之后,限定类型用“&”分隔。例如:
class Apple<T extends Number & Comparable>{...}
8.4.5 通配符的一种用法
private static <T> void swapHelper(Pair<T> p){
T k = p.getKey();
p.setKey(p.getValue());
p.setValue(k);
}
public static void swap(Pair<?> p){
swapHelper(p);
}
///等价于,因为泛型方法无须显式给出类型实参
public static <T> void swap(Pair<T> p){
T k = p.getKey();
p.setKey(p.getValue());
p.setValue(k);
}
8.4.6 原始集合、泛型集合和通配符集合的比较
原始集合可以存放任意类型的元素,同时可以从中读取元素,使用读取元素时,需要进行类型转换。作用类似于类型实参为 Object 的泛型类。此外,可以创建原始集合数组。
泛型集合中只能存放特定类型(类型实参)的元素。当放入非类型实参子类型的元素时,编译器会报错。泛型集合不能创建数组。
通配符集合可以引用任意类型实参的泛型集合。在将泛型集合赋给通配符集合时,会丢失类型实参信息。通配符集合中存放的仍然是同种类型的元素。不能向通配符集合中添加元素,从通配符集合中读取的元素只能保存在 Object 对象中。
8.5 泛型方法
泛型方法就是在声明方法时定义一个或多个类型形参。泛型方法的用法格式如下:
修饰符 <T,S> 返回值类型 方法名(形参列表){ //方法体 ...}
//调用格式
实例对象名/类名.<类型实参>方法名(实参)
泛型方法比普通方法的返回值类型前面多了类型形参声明,类型变量声明以尖括号括起来,多个类型变量之间以逗号隔开,且类型形参声明放在方法修饰符和方法返回值类型之间。与泛型类、接口中不同的是,泛型方法调用时,无需显式传入类型实参,编译器根据方法实参和返回值类型,推断出类型实参,它经常推断出最直接的类型实参(方法实参的类型)。
泛型方法使得该方法能够独立于类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,应该尽量使用泛型方法,即如果使用泛型方法可以取代整个类泛型化,就应该只使用泛型方法,因为它可以使事情更清楚明白。
显式的类型指定
在泛型方法中,可以显式地指定类型,不过这种语法很少使用。要显式地指明类型:
- 必须在点操作符与方法名直接插入尖括号,然后把类型置于尖括号内。
- 如果是在定义该方法的类的内部,必须在点操作符之前使用 this 关键字。
- 如果泛型方法是静态方法,必须在点操作符之前加上类名。
使用显式的类型说明,可以解决类型推断不成功的问题。
8.5.1 泛型方法和通配符的区别
通常情况下,可以使用泛型方法来代替类型通配符,例如,对于 Java 的 Collection接口中两个方法,有如下方式:
public interface Collection<E>{
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c){
for(E obj : c){
...
}
}
}
上面集合中两个方法都采用了类型通配符的形式,也可以采用泛型方法的形式:
public interface Collection<E>{
<T> boolean containsAll(Collection<T> c);
<T extends E>boolean addAll(Collection<T> c){
for(T obj : c){
...
}
}
}
上面两个方法中类型形参 T 只使用了一次,类型形参 T 产生的唯一效果是可以在不同的调用点,传入不同的实际类型。对于这种情况,应该使用通配符:通配符就是用来支持灵活的子类化的。
泛型方法允许用类型形参表示方法的一个或多个类型形参之间的类型依赖关系,或者方法返回值的类型与类型形参之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。
如果某个方法中一个形参 a 的类型或返回值的类型依赖于另一个形参 b 的类型,则形参 b 的类型声明不应该使用通配符----因为形参a或返回值的类型依赖于 b 的类型,如果 b 的类型无法确定,程序就无法定义形参a或返回值的类型,在这种情况下,只能考虑使用泛型方法。
如果有需要,也可以同时使用类型形参和通配符,
public class Collections{
public static <T> void copy(List<T> dest, List<? extends T> src){
...
}
}
上述代码,也可以转换为完全使用泛型方法:
public class Collections{
public static <T, S extends T> void copy(List<T>dest, List<s>src){
...
}
}
但是由于形参 S 仅使用了一次,其他类型形参以及返回值类型都不依赖它,所以类型形参 S 没有存在的必要,即可以用通配符来代替 S。使用通配符比使用类型形参更加清晰和准确。因此,java 设计该方法时使用了通配符,而不是类型形参。
类型通配符与类型形参还有一个显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明。?
8.5.2 Java 7 的“菱形语法”与泛型构造器
正如泛型方法允许在方法签名中声明类型形参一样,java 也允许在构造器签名中声明类型形参,即泛型构造器。
调用泛型构造器时,既可以显式指定类型实参,也可以让 java 自行推断类型实参。
“菱形”语法允许调用构造器时在构造器后,即类名后使用一对尖括号来代表泛型类的类型实参信息。如果代码显式给出了泛型构造器的类型实参,则本泛型类不可以使用 "菱形" 语法。
class MyClass<E>{
public <T> MyClass(T t){
System.out.println("t 参数的值为:" + t);
}
}
public class GenericDiamondTest{
public static void main(String[] args){
MyClass<String> mc1 = new MyClass<>(5);
//如果显式指定泛型构造器中声明的T形参是Integer类型
//此时就不能使用菱形语法,必须显式地在构造器中指出泛型类的类型实参。
MyClass<String> mc2 = new <Integer>MyClass<String>(5);
//Error
MyClass<String> mc3 = new <Integer>MyClass<>(5);
}
}
8.5.3 泛型方法与方法重载
public class MyUtils{
public static <T> void copy(Collection<T> dest, Collection<? extends T> src){...}
public static <T> T copy(Collection<? super T> dest, Collection<T> src){...}
}
上面的两个 copy 方法的两个参数都是 Collection 对象,且前一个 Collection 对象中的元素都是后一个 Collection 对象中元素的父类。在该类中定义这两个方法不会有问题,但是只要调用这个方法就会引起编译错误,例如:
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
copy(ln, li);
调用的 copy 方法既可以匹配 MyUtils 类中的第一个方法,此时 T 类型参数的类型是 Number;也可以匹配第一个方法,此时 T 参数的类型是 Integer。编译器无法确定哪个 copy 方法被调用。所以,编译出错。
泛型子类如何重载和覆盖泛型父类的方法。示例如下:
class Par<T>{
void show(T str){
System.out.println("parent" + str);
}
}
class Child<T> extends Par<T>{
void show(T str){
System.out.println("child" + str);
}
}
8.5.4 Java 8 改进的类型推断
Java 8 改进了泛型方法的类型推断能力,类型推断主要有如下两个方面:
- 可通过调用方法的上下文来推断类型参数的目标类型。
- 可在方法调用链中,将推断得到的类型参数传递到最后一个方法。
示例如下:
class MyUtil<E>{
public static <z> nil(){
return null;
}
public static <z> MyUtil<z> cons(z head, MyUtil<z> tail){
return null;
}
E head(){
return null;
}
}
public class InferenceTest{
public static void main(String[] args){
//可以通过方法赋值的目标参数来推断类型参数为String
MyUtil<String> ls = MyUtil.nil();
//无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil<String> mu = MyUtil.<String>nil();
//可调用cons()方法所需的参数类型来推断类型参数为Integer
MyUtil.cons(42,MyUtil.nil());
//无需使用下面语句在调用nil()方法时指定类型参数的类型
MyUtil.cons(42,MyUtil.<Integer>nil());
}
}
虽然Java 8 增强了泛型推断能力,但能力终究有限,例如下面代码就是错误的:
//希望系统能推断出调用nil()方法时类型参数为String类型
//但实际上java8推断不出来,所以代码报错
String s = MyUtil.nil().head();
因此,上面代码必须显式指定类型参数,即将代码改为如下形式:
String s = MyUtil.<String>nil().head();
8.6 泛型代码与虚拟机
java 泛型只能用于在编译期间的静态类型检查。然后编译器在生成字节码时会擦除相应的类型。虚拟机没有泛型类型对象---所有对象都属于普通类。
8.6.1 类型擦除
无论何时定义泛型类,泛型类都自动提供了一个原始类。原始类的名字就是删去类型形参后的泛型类名。同时会将泛型类中所有类型形参擦除,并替换为限定类型(无限定的类型变量替换为Object)。例如:
class Pair<T>{
private T field;
Pair(T arg){field = arg};
T getField(){return field};
void setField(T arg){field = arg};
}
\\擦除类型后为,:
class Pair{
private Object field;
Pair(Object arg){field = arg};
Object getField(){return field};
void setField(Object arg){field = arg};
}
类型擦除后,Pair变为了一个普通类。在程序中可以有不同类型实参的Pair。例如,Pair<String>
或 Pair<LocalDate>
,而擦除后就变成了原始的Pair类型。
类型形参替换为第一个限定类型,如果没有限定就替换为 Object。例如,T 没有限定,所以替换为 Object。
8.6.2 翻译泛型表达式
当程序调用泛型方法时,如果擦除了返回类型,编译器插入强制类型转换。例如:
Pair<Employee> bud = ...;
Employee buf = bud.getField();
擦除 getField 返回值类型后,将返回 Object 类型。编译器自动插入 Employee 强制类型转换。即编译器把这个方法调用翻译为两条虚拟机指令:
调用原始方法 Pair.getField
将返回的 Object 类型强制转换为 Employee 类型。
当存取一个泛型域时也要插入强制类型转换。假设 Pair 类的 field 为 public,对于表达式:Employee buf = bud.field;,虚拟机也将加入强制类型转换,变为:
Employee buf = (Employee)bud.field;
8.6.3 翻译泛型方法
覆盖变重载的修复
类型擦除也会出现在泛型方法中。通常认为下述的泛型方法是一个完整的方法族
public static <T extends Comparable> T min(T[] a)
而擦除类型后,只剩下一个方法:
public static Comparable min(Comparable[] a)
方法的类型擦除会造成与多态性冲突:
例如当有一个类 DateInterval 继承了 Pair<LocalDate>
类,并覆盖 Pair<LocalDate>
的 setField 方法
class DateInterval extends Pair<LocalDate>{
public void setField(LocalDate arg){
......
}
}
但由于类型擦除, DateInterval 类从 Pair<LocalDate>
继承的 setField 方法为:
void setField(Comparable arg)
//而不是
void setField(LocalDate arg)
此时 DateInterval 定义的 setField() 只是重载了父类的方法,而不是覆盖。此时,类型擦除与多态发生了冲突。
为了解决这个问题,编译器在 DateInterval 类中生成了一个桥方法(bridge method):
public void setField(Comparable arg){setField((LocalDate)arg);}
返回值类型变小时的覆盖处理
当子类方法覆盖父类方法,且子类方法的返回值类型是父类返回值类型的子类型时,编译器也会添加桥方法为,示例如下:
class Par{
Par get(){
return new Par();
}
}
class Child extends Par{
Child get(){
return new Child();
}
}
此时,编译器在编译时,会在 Child 类中添加一个桥方法:
Par get(){
return (Child)this.get();
}
此时,子类中有两个仅返回值类型不同的方法。明显违反java 语法。但是在虚拟机中,用参数类型和返回值类型确定一个方法。因此,编译器可以产生两个仅返回类型不同的方法字节码,虚拟机能正确地处理这种情况。
所以当 DateInterval 覆盖了父类的 getField()时 :
LocalDate getField(){...}
由于泛型擦除,父类的 getField() 的返回值类型为 Object,于是子类覆盖了父类的 getField(),且子类该方法的返回值类型是父类该方法返回值类型的子类型。于是,编译器会添加一个桥方法。
综上,需要记住有关Java泛型转换的事实:
- 虚拟机中没有泛型,只有普通的类和方法
- 所有的类型形参都替换为他们的限定类型
- 编译器合成桥方法来保持多态。
- 为保持类型安全性,会在表达式中插入强制类型转换。
- 在类型擦除后,泛型类引用的类型均为 Pair。
- 编译器在泛型类内部的动作有:编译器将所有类型参数都替换为原始类型,并添加相应的桥方法。在泛型类的内部,编译器不会添加强制类型转换。编译器仅仅在对外的方法上(如返回值为类型参数的方法,以及访问泛型类成员的表达式中)添加强制类型转换。
类型擦除例题
public class Main {
public static void main(String[] args) throws Exception{
Obj[] arr = new Obj[2];
arr[0] = new Obj(3);
arr[1] = new Obj(1);
Obj elem = new Obj(2);
System.out.println(Test.counter(arr, elem));
}
}
class Test{
static public <T extends Comparable<T>>int counter(T[] arr, T elem){
int count = 0;
for(T t : arr){
if(t.compareTo(elem) < 0){
count++;
}
}
return count;
}
}
class Obj implements Comparable<Obj>{
int val;
public Obj(int val){
this.val = val;
}
public int compareTo(Obj oth){
return val - oth.val;
}
}
上述代码类型擦除后如下:
public class Main {
public static void main(String[] args) throws Exception{
Obj[] arr = new Obj[2];
arr[0] = new Obj(3);
arr[1] = new Obj(1);
Obj elem = new Obj(2);
System.out.println(Test.counter(arr, elem));
}
}
class Test{
static public int counter(Comparable[] arr, Comparable elem){
int count = 0;
for(Comparable t : arr){
if(t.compareTo(elem) < 0){
count++;
}
}
return count;
}
}
class Obj implements Comparable<Obj>{
int val;
public Obj(int val){
this.val = val;
}
public int compareTo(Obj oth){
return val - oth.val;
}
}
- 此时,在 main 函数中调用 counter() 方法时,传入了两个参数 Obj[] 和 Obj 对象。
- 因此,在 counter() 方法体内,t 和 elem 的实际类型是 Obj。
- 当调用 t 的 compareTo() 方法时,编译器是通过参数的编译时类型确定方法调用的,所以会将该方法调用解析为 Comparable 接口中定义的 int compareTo(Comparable obj)。
- 但是由于桥方法的存在,编译器会在 Obj 类中添加一个桥方法 int compareTo(Comparable obj){return compareTo((Obj)obj);},
- 从而,t.compareTo() 具有多态性, t.compareTo() 实际调用的是桥方法。,而桥方法内部调用 int compareTo(Obj oth)。
- 所以,t.compareTo() 调用的是 int compareTo(Obj oth)。
8.6 4 泛型约束与局限性
8.6.4.1 不能用基本类型作为类型实参
主要原因是类型擦除后,类型变量替换为 Object,而 Object 不能存储基本类型的值。
8.6.4.2 运行时泛型类变量的类型查询只适用于原始类型
虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。例如:
if(a instanceof Pair<String>)
实际上仅仅测试 a 是否为任意类型的一个 Pair。
为提醒这一风险,试图查询一个对象是否属于某个泛型类型时,倘若使用 instanceof,会得到一个编译错。如果使用强制类型转换,会得到一个警告。
此外,getClass 总返回原始类型的类信息,例如:
Pair<String> str = ...;
Pair<Integer> itg = ...;
//下面判断为true
if(itg.getClass() == str.getClass())
8.6.4.3 不能创建泛型类型的数组
例如:
Pair<String>[] table = new Pair<String>[10];//error
因为类型擦除后,table 的类型为 Pair[] ,可以把它转换为
Object[]:Object[] obj = table;
数组会记住它的元素类型,如果试图存储其他类型的元素,会抛出异常。但是对于泛型类型,擦除会使机制无效。以下赋值:
boj[0] = new Pair<Employee>();
会通过数组的类型检查,因为类型擦除后,新建对象的类型就是 Pair。可以通过编译,但是运行会出错。这违背泛型的只要编译不报错,运行不会出错的原则。所以,不允许创建泛型类型的数组。但是可以声明泛型类型的数组变量。
通过泛型数组包装器,如 ArrayList 类,维护一个 Object数组,然后通过限定进出口方法 set、get 的类型和强制转换数组类型,从而间接实现泛型数组。例如:ArrayList:ArrayList<Pair<T>>
、ArrayList<T>
。
8.6.4.4 不能创建类型形参的对象
不能使用像new T(),new T[...]或T.class这样的表达式。例如:
public Pair(){first = new T();second = new T()}。
类型擦除后,T 替换为 Object。而本意并不是创建 Object 实例。在 java8 之后,最好的解决方法是让调用者提供一个构造器Lambda表达式。例如:
Pair<String> p = Pair.makePair(String::new);
不能调用T.class.newInstance(),还是因为 T 变为了 Object。
8.6.4.5 不能构造类型形参的数组
因为类型擦除后,new T[...] 变为 new Object[...],最好的方法是利用反射,或者通过参数传入构造器。
8.6.6.6 泛型类的静态上下文类中型变量无效
不能声明泛型类型的静态变量,在静态初始化块中,也不能使用泛型变量。
因为类型擦除的存在,不管为类型变量传入哪一种类型实参,它们都是同一个类,只占用一块内存空间,共享静态变量。因此在静态方法、静态初始化块或静态变量的声明和初始化中不允许使用类型参数。否则,会违背程序本意。
此外,由于系统中并不会真正生成泛型类,所以 instanceof 运算符右操作数不能为泛型类。否则,会违背程序本意。
8.6.6.8 注意擦除后的冲突
???不理解