泛型
泛型可以作为 Java 集合的补充,因为 JDK 1.5 增加泛型支持在很大程度上都是为了让集合能记住其元素的数据类型。在没有泛型之前,一旦把一个对象“丢进” Java 集合中,集合就会忘记对象的类型,把所有的对象当成 Object 类型处理。当程序从集合中取出对象后,就需要进行强制类型转换,这种强制类型转换不仅使代码臃肿,而且容易引起 ClassCastException 异常。
增加了泛型支持后的集合,完全可以记住集合中元素的类型,并可以在编译时检查集合中元素的类型,如果视图向集合中添加不满足类型要求的对象,编译器就会提示错误。增加泛型后的集合,可以让代码更加简洁,程序更加健壮。
Java 泛型设计原则:Java 泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生 ClassCastException 异常。
(泛型的学习大都是通过问题进行抛出,再解决问题)
泛型入门
Java 集合有一个缺点:当把一个对象“丢尽”集合里后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了 Object 类型,其运行时类型没变。
Java 集合之所以被设计成这样,是因为集合的设计者不知道使用者会用集合来保存什么类型的对象,为了具有很好的通用性,所以把集合设计成能保存任何类型的对象。但这样做会带来如下两个问题:
- 集合对元素类型没有限制,这样很可能会引发一些问题。如,一个只能保存 A 对象的集合,但程序也可以轻易地将 C 对象“丢”进去,可能引发异常
- 由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是 Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制转换及增加了编程的复杂度,也可能引发ClassCastException 异常
使用泛型
从 Java 5 以后,Java 引入了“参数化类型(parameterized type)”的概念,允许程序在创建集合时指定集合元素的类型,如,List<Integer>
。这表明该 List 只能保存字符串类型的对象,可以称 List 是一个带类型参数的泛型接口。Java的参数化类型被称为泛型(Generic)。
泛型的支持让代码更加健壮,程序再也不能“不小心”把其他对象“丢进”集合中;而且程序更加简洁,集合会自动记住所有集合元素的数据类型,从而无需对集合元素进行强制类型转换(就是解决了上面的两个问题)。
Java 7 泛型的“菱形”语法
从 Java 7 开始,Java 允许在构造器后不需要带完整的泛型信息,只需给出一对尖括号 <>
即可,Java 可以自行推断尖括号里应该是什么泛型信息。
泛型语法对原有的泛型并没有什么改变,只是更好地简化了泛型编程。
深入泛型
所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也称类型实参)。
Java 5 改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建集合对象时传入类型实参。
泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可以当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。
如,使用 List<E>
类型时,如果为 E 形参传入 String 类型实参,在逻辑上则产生了一个新的类型:List<String>
类型,可以把 List<String>
类型想象成 E 被全部替换成 String 的特殊 List 子接口。通过这种方式,虽然程序只定义了一个 List<E>
接口,但实际使用时可以产生无数多个 List 接口,只要为 E 传入不同的类型实参,系统就会多出一个新的 List 子接口。必须指出:List<String>
绝不会产生另一个类(如 ListString),系统没有进行源码复制,在二进制代码中没有,磁盘中没有,内存中也没有。
包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参,从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上并不存在。
除此之外,并不是只有集合类才可以使用泛型声明(虽然集合类是泛型的重要使用场所),可以为任何类、接口增加泛型声明。
从“泛型类”派生子类
当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类。需要注意的是:当使用这些接口、父类时不能再包含类型形参。
// 定义 Apple 类时使用泛型声明
class Apple<T> {
...
}
// 从“泛型类”派生子类的错误写法
class A extends Apple<T> { ... }
// 使用 Apple 类时,为 T 形参传入 String 类型(就是让形参有一个确定的类型)
/*
* 如果从 Apple<String> 派生子类,则在 Apple 类中所有使用 T 类型形参的地方都将被替换成 String 类型
*/
class A extends Apple<String> { ... }
// 或不为 T 传入实际的类型参数
/*
* 使用 Apple 类时没有传入实际的类型参数,Java 编译器可能发出警告:
* * 使用了未经检查或不安全的操作(这就是泛型检查的警告)
* * 此时,系统会把 Apple<T> 类里的 T 当成 Object 类型处理
*/
class A extends Apple { ... }
方法中的形参代表变量、常量、表达式等数据,这里直接称为数据形参(或形参)。定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似,定义类、接口、方法时可以声明类型形参,使用类、接口、方法时应该为类型形参传入实际的类型。
有一个不同:调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为类型形参传入实际的类型参数(假装不存在)。
并不存在泛型类
在 深入泛型
中讲到,逻辑上可以把 ArrayList<String>
看成 ArrayList 的子类,事实上,ArrayList<String>
类也确实像一种特殊的 ArrayList 类:该 ArrayList<String>
对象只能添加 String 对象作为集合元素。但实际上,系统并没有为 ArrayList<String>
生成新的 class 文件,而且也不会把 ArrayList<String>
当成新类来处理,不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class):
List<String> list1 = new ArrayList<>();
List<Integer> list2 = new ArrayList<>();
System.out.println(list1.getClass() == list2.getClass()); // true
不管泛型的类型形参传入哪一种类型实参,对于 Java 来说,它们依然被当成同一个类处理,在内存中也只占用同一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化块中不允许使用类型形参。
由于系统中并不会真正生成泛型类,所以 instanceof 运算符后不能使用泛型类(Demo01.java)。
类型通配符
在使用泛型类时,都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。假设现在需要定义一个方法,该方法里有一个集合形参,集合形参的元素类型是不确定的,那该怎么定义呢?
考虑如下示例:
public void test(List c) {
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
在上述代码中使用 List 接口时没有传入实际类型参数,这将引起警告。为此,考虑为 List 接口传入实际的类型参数,因为集合里的元素类型是不确定的,所以给形参 T 传入 Object 类型:
public void test(List<Object> c) {
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
调用该方法:
List<String> strList = new ArrayList<>();
test(strList);
在编译上述代码时,就会发生编译错误,这表明了 List<String>
对象不能被当成 List<Object>
对象使用,也就是说,List<String>
类并不是 List<Object>
类的子类。
注意:如果 A 是 B 的一个子类型(子类或者子接口),而 G 是具有泛型声明的类或者接口,G<A>
并不是 G<B>
的子类型!
与数组进行对比
在数组中,程序可以直接把一个 Integer[] 数组赋给一个 Number[] 变量。如果把一个 Double 对象保存到该 Number[] 数组中,编译可以通过,但在运行时抛出 ArrayStoryException 异常(这就是一种潜在的风险):
public static void main(String[] args) {
Integer[] ints = new Integer[5];
Number[] numbers = ints;
numbers[0] = 1;
// 引发 ArrayStoreException 异常
numbers[2] = 0.5;
}
在早期的 Java 设计中,允许 Integer[] 数组赋值给 Number[] 数组变量存在缺陷,因此在设计泛型时进行了改进,它不再允许把 List<Integer>
对象赋值给 List<Number>
对象变量:
List<Integer> integerList = new ArrayList<>();
List<Number> numberList = integerList; /* 编译错误 */
数组和泛型的不同:假设 A 是 B 的子类型,那么 A[] 依然是 B[] 的子类型;但 G<A>
并不是 G<B>
的子类型。
Java 泛型的设计原则是,只要代码在编译时没有出现警告,就不会遇到 ClassCastException 异常。
使用类型通配符
为了表示各种泛型 List 的父类,可以使用类型通配符(?)。将一个问号作为类型实参传给 List 集合,写作:List<?>
(意思是与元素类型未知的 List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。从而解决了上面的问题:
public void test(List<?> c) {
for (int i = 0; i < c.size(); i++) {
System.out.println(c.get(i));
}
}
这时,正常调用再没有什么问题了。
【注意】类型通配符不可以在定义类、接口中使用。如,class A<?> { ... }
会导致编译错误:Unexpected wildcard(意想不到的通配符)。
【埋了新坑又添一坑】但这种带通配符的 List 仅表示它是各种泛型 List 的父类,并不能把元素加入到其中:
List<?> list = new ArrayList<>();
list.add("a"); /* 编译错误 */
因为程序无法确定 list 集合中元素的类型,所以不能向其中添加对象(但 null 例外,它是所有引用类型的实例)。
设定类型通配符的上限
当直接使用 List<?>
这种形式时,即表名这个 List 集合可以是任何泛型 List 的父类。但还有一种特殊的情形,程序不希望这个 List<?>
是任何泛型 List 的父类,只希望它代表某一类泛型 List 的父类。
以一个绘图类举例:
public abstract class Shape {
public abstract void draw(Canvas c);
}
class Circle extends Shape {
public void draw(Canvas c) { System.out.println("把圆画在画布" + c + "上"); }
}
class Rectangle extends Shape {
public void draw(Canvas c) { System.out.println("把矩形在画布" + c + "上"); }
}
上述代码定义了三个形状类,一个 Shape 抽象父类,两个子类 Circle 和 Rectangle。该画布类 Canvas 可以画数量不等的形状(就是 Shape 子类的对象),那应该如何定义这个 Canvas 类呢?
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<Shape> shapes) {
for(Shape s : shapes) {
s.draw(this);
}
}
}
如果让 List<Shape>
作为形参类型,但 List<Shape>
并不是 List<Circle>
的父类,因此在使用 drawAll() 方法时将引起编译错误。
为了表示 List<Circle>
的父类,可以考虑使用类型通配符 List<?>
:
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<?> shapes) {
for(Object obj : shapes) {
Shape s = (Shape)obj;
s.draw(this);
}
}
}
在使用了通配符后,drawAll() 可以接收 List<Circle>、List<Rectangle>
对象作为参数,问题是也可以接收任意的对象作为参数,除此之外,上述代码的实现显得极为臃肿和烦琐:使用了强制类型转换。
实际上需要一种泛型表示方法,它可以表示所有 Shape 泛型 List 的父类。为了满足这种需求,Java 泛型提供了被限制的泛型通配符:
public class Canvas {
// 同时在画布上绘制多个形状
public void drawAll(List<? extends Shape> shapes) {
for(Shape s : shapes) {
s.draw(this);
}
}
}
即 List<? extends Shape>
可以表示 List<Circle>、List<Rectangle>
的父类(只要 List 尖括号里的类型是 Shape 的子类型(也可以是本身)即可),因此把 Shape 称为这个通配符的上限(upper bound)。
【依旧是上个没填的坑】类似地,由于程序无法确定这个受限制的通配符的具体类型,所以不能把 Shape 对象或其子类的对象加入这个泛型集合中:
public void addRectangle(List<? extends Shape> shape) {
shape.add(0, new Rectangle());
/**
* "? extends Shape"代表 Shape 未知的子类,程序无法确定这个类型是什么,
* 所以无法将任何对象添加到这种集合中。
*/
}
设定类型形参的上限
Java 泛型不仅允许在使用通配符形参时设定上限,而且可以在定义类型形参时设定上限,用于表示传给该类型形参的实际类型要么是该上限类型,要么是该上限类型的子类。
public class Demo04<T extends Number> {
T msg;
public static void main(String[] args) {
Demo04<Integer> aInteger = new Demo04<>();
Demo04<Double> aDouble = new Demo04<>();
// 编译错误,因为试图把 String 传给 T,而 String 并不是 Number 的子类
// Demo04<String> aString = new Demo04<>();
}
}
在一种更极端的情况下,程序要为类型形参设定多个上限(至多一个父类上限,可以有多个接口上限)表名该类型形参必须是其父类的子类(是父类本身也行),并且实现多个上限接口。
与类同时继承父类、实现接口类似的是,为类型形参指定多个上限时,所有的接口上限都必须位于类上限之后。也就是说,如果需要为类型形参指定类上限,类上限必须位于第一位(而且只能有一个类上限):
public class A<T extends Number & Serializable> { ... }
泛型方法
在定义类、接口时可以使用类型形参,在该类的方法定义和成员变量定义、接口的方法定义中,这些类型形参可被当成普通类型来用。在另外一些情况下,定义类、接口时没有使用类型形参,但定义方法时想自己定义类型形参,这也是可以的,Java 5 还提供了对泛型方法的支持。
定义泛型方法
【引出:对于类型通配符中“不能把元素加入”的填坑】对于需要实现一个方法:该方法负责将一个 Object 数组的所有元素添加到一个 Collection 集合中:
static void fromArrayToCollection(Object[] a, Collection<Object> c) {
for (Object o : a) {
c.add(o);
}
}
对于 c 形参,它的数据类型是 Collection<Object>
。因为 Collection<String>
不是 Collection<Object>
的子类型,他只能将 Object[] 数组的元素赋值到元素为 Object(Object 的子类不行,所以这个方法的功能非常有限)的集合中。改进,使用泛型通配符行不行呢:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // 有编译异常,因为 Java 不允许把对象放进一个未知类型的集合中(这也就是前面埋下的坑!!)
}
}
为了解决这个问题,可以使用 Java 5 提供的泛型方法(Generic Method)。所谓泛型方法,就是在声明方法时定义一个或多个类型形参:
修饰符 <T, S> 返回值类型 方法名(形参列表) {
...
}
和普通方法格式对比发现,泛型方法的方法签名比普通方法的方法签名多个类型形参声明,所以的类型形参声明放在方法修饰符和方法返回值类型之间。
采用支持泛型的方法,改写上述代码为:
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // 完美解决了上述的问题
}
}
方法声明中定义的形参与接口、类声明中定义的类型形参不同的是:(作用域不同、使用方式不同)
- 方法声明中定义的形参只能是在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用
- 方法中的泛型参数无需显式传入实际类型参数,如
GerericMethodTest.java
。因为编译器根据实参可推断类型形参的值(它通常推断出最直接的类型参数)
==注意:==为了让编译器能准确地推断出泛型方法中类型形参的类型,不要制造迷惑!一旦系统迷惑了,就是程序员错了!如,ErrorTest.java
。
泛型方法和类型通配符的区别
多数时候都可以使用泛型方法来代替类型通配符。
举例说明泛型通配符和泛型方法
/* 泛型通配符的形式 */
public interface Collection<E> {
boolean contaonsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
...
}
/* 泛型方法的形式 */
public interface Collection<E> {
<T> boolean contaonsAll(Collection<T> c);
<T extends E> boolean addAll(Collection<T> c);
//<T> boolean addAll(Collection<T extends E> c); /* 没有 T extends E 这样的语法!! */
...
}
对于 <T extends E>
的泛型形式,这是定义类型形参时设定上限(其中 E 是 Collection 接口里定义的类型形参,在接口里 E 可以当成普通类型使用)。
什么时候使用泛型通配符,什么时候使用泛型方法?
对于上面的情况,类型形参 T 只使用了一次,类型形参 T 产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符,通配符就是被设计用来支持灵活的子类化的。
泛型方法允许类型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系(如果没有这种类型依赖关系,就使用通配符)。如果某个方法中一个形参(a)的类型或返回值依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符,因为形参(a)或返回值的类型依赖于该形参(b)的类型,如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型(简单说,就是上面填坑的那种情况)
如果有需要,也可以同时使用泛型通配符和泛型方法
如:ErrorTest02
中
static <T> void test(Collection<? extends T> from, Collection<T> to) {
for (T ele : from) {
to.add(ele);
}
}
from 和 to 存在着明显的依赖关系,从 from 中复制出来的元素,必须可以“丢进” to 中,所以 from 集合元素的类型只能是 to 集合元素的类型的子类型或者它本身。但定义 from 使用的是类型通配符,而不是泛型方法。这是因为:该方法无需向 from 集合中添加元素,也无需修改 from 集合里的元素,所以可以使用类型通配符,无需使用泛型方法。
当然,也可以把上面的方法签名还未使用泛型方法,不使用类型通配符:
static <T, S extends T> void test(Collection<S> from, Collection<T> to) { ... }
这个方法签名与上面的方法签名作用上没有不同。但注意,类型形参 S,仅使用了一次,其他参数的类型、方法返回值的类型都不依赖它,那类型形参 S 就没有存在的必要,即可通过通配符来代替 S。
类型通配符 vs 泛型方法:
-
泛型方法可以解决类型依赖关系
-
泛型方法可以在静态方法中使用,通配符不可以
-
使用通配符比使用泛型方法更加清晰和准确
-
类型通配符既可以在方法签名中定义形参的类型,也可以用于定义变量的类型;但泛型方法中的类型形参必须在对应方法中显式声明
Java 7 的“菱形”语法与泛型构造器
正如泛型方法允许在方法签名中声明类型形参一样,Java 也允许在构造器签名中声明类型形参,这样,就产生了所谓的泛型构造器。
一旦定义了泛型构造器,接下来在调用构造器时,就不仅可以让 Java 根据传入参数的数据类型来“推断”类型形参的类型,而且程序员也可以显式地为构造器中的类型形参指定实际的类型。详见:GenericConstructor.java
。
**注意:**如果程序显式指定了泛型构造器中声明的类型形参的实际类型,则不可以使用“菱形”语法。
设定通配符下限
【问题抛出】将 src 集合里的元素复制到 dest 集合里的功能,并且返回最后一个被复制的元素。
【解决】1. 因为 dest 集合可以保存 src 集合里的所有元素,所以 dest 集合元素的类型应该是 src 集合元素类型的父类;2. 为了表示两个参数之间的关系,考虑使用通配符、泛型方法来实现。
public static <T> T copy(Collection<T> dest, Collection<? extends T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
表面上看起来解决了这个问题,实际上有一个问题:当遍历 src 集合的元素时,src 元素的类型是不确定的(只可以肯定它是 T 的子类),程序只能用 T 来笼统地表示各种 src 集合的元素类型。如:
List<Number> numList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
Integer last = copy(numList, intlist); /* 编译错误 */
【抛出新问题】对于 numList 的类型是 Collection<Number>
,与 copy() 方法签名的形参类型(Collection<T>
)进行比较,得到 T 的实际类型是 Number,而不是 Integer,所以 copy() 的返回值也是 Number。也就是说,程序再复制集合元素的过程中丢失了 src 集合元素的类型。
【解决】对于 copy() 方法,可以这样理解两个集合参数之间的依赖关系:不管 src 集合元素的类型是什么,只要 dest 集合元素的类型与前者相同或是前者的父类即可。为了表达这种约束关系,Java 允许设定通配符的下限:<? super Type>
,这个通配符表示它必须是 Type 本身或者是 Type 的父类。具体如下:
public class MyUtils {
public static <T> T copy(Collection<? super T> dest, Collection<T> src) {
T last = null;
for (T ele : src) {
last = ele;
dest.add(ele);
}
return last;
}
public static void main(String[] args) {
List<Number> numList = new ArrayList<>();
List<Integer> intList = new ArrayList<>();
Integer last = copy(numList, intlist);
}
}
这样程序就可以通过 intlist 的类型是 Collection<Integer>
来得到 T 的类型是 Integer,从而方法的返回值也是 Integer 了。
实际上,Java 集合框架中的 TreeSet<E>
、TreeMap<E>
等也用到了类似的构造器。
泛型方法与方法重载
因为泛型即允许设定通配符的上限,也允许设定通配符的下限,从而允许在一个类里包含如下两个方法的定义:
public class A {
public static <T> void copy(Collection<T> dest, Collection<? extends T> src) { ... }
public static <T> T copy(Collection<T> dest, Collection<? extends T> src) { ... }
}
??? 与书中冲突,暂时忽略这一块
Java 8 改进的类型推断
Java 8 改进了泛型方法的类型推断能力,主要有两个方面:
- 可通过调用方法的上下文来推断类型参数的目标类型(与之前类似)
- 可在方法调用链中,将推断得到的类型参数传递到最后一个方法
虽然 Java 8 增强了泛型推断的能力,但泛型推断不是万能的。
如,InferenceTest.java
。
擦除和转换
在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的 Java 代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型参数(类型实参)。如果没有为这个泛型指定实际的类型参数,则该类型参数被称作 raw type(原始类型),默认是声明该类型参数时指定的第一个上限类型。
【擦除】当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。如,一个 List<String>
类型被转换为 List,则该 List 对集合元素的类型检查变成了类型参数的上限(即 Object)。
【转换】从逻辑上看,List<String>
是 List 的子类,如果直接把一个 List 对象赋给一个 List<String>
对象应该会引起编译错误,但实际上不会。对于泛型而言,可以直接把一个 List 对象赋给一个 List<String>
对象,编译器仅仅提示“[unchecked] 未经检查的转换”。操作不当容易抛出 ClassCastException 异常。
泛型与数组
Java 泛型有一个很重要的设计原则:如果一段代码在编译时没有提出“[unchecked] 未经检查的转换”警告,则程序在运行时不会引发 ClassCastException 异常。正是基于这个原因,所以数组元素的类型不能包含类型变量或类型形参,除非是无上限的类型通配符(?)。但可以声明元素类型包含类型变量或类型形参的数组。也就是说,只能声明 List<String>[]
形式的数组,但不能创建 ArrayList<String>[10]
这样的数组对象(即,java 不支持创建泛型数组)。
假设 Java 支持创建 ArrayList<String>[10]
这样的数组对象:
(编译看“左边”,运行看“右边”)
List<String>[] lsa = new List<String>[10]; /* 编译错误 */
// 将 lsa 向上转型为 Object[] 类型的变量
Object[] oa = (Object[]) lsa;
List<Integer> list = new ArrayList<>();
list.add(4);
// 将 List<Integer> 作为 oa 的元素
oa[1] = list;
// 引发 ClassCastException 异常
String s = lsa[1].get(0); /* java.lang.Integer cannot be cast to java.lang.String */
在上面的代码中,如果第一行创建 List<String>[10]
是合法的,经过中间系列的程序运行,势必在最后引发运行时异常,这就违背了 Java 泛型设计原则。
但如果将程序改为如下形式:
List<String>[] lsa = new List[10]; /* 编译没错误,发出 Unchecked 警告 */
// 将 lsa 向上转型为 Object[] 类型的变量
Object[] oa = (Object[]) lsa;
List<Integer> list = new ArrayList<>();
list.add(4);
// 将 List<Integer> 作为 oa 的元素
oa[1] = list;
// 引发 ClassCastException 异常
String s = lsa[1].get(0); /* java.lang.Integer cannot be cast to java.lang.String */
声明 List<String>[]
形式的数组是泛型允许的,但同时也发出了“[unchecked] 未经检查的转换”的警告,即编译器并不保证这段代码是类型安全的。在代码最后同样会抛出异常,但因为编译器已经提出了警告,所以并没有违背 Java 泛型设计的原则,所以可以声明 List<String>[]
形式的数组。
Java 允许创建无上限的通配符泛型数组,如,new ArrayList<?>[]
,因此也可以将上述代码改为:
List<?>[] lsa = new List<?>[10];
// 将 lsa 向上转型为 Object[] 类型的变量
Object[] oa = (Object[]) lsa;
List<Integer> list = new ArrayList<>();
list.add(4);
// 将 List<Integer> 作为 oa 的元素
oa[1] = list;
// 在这种情况下,不得不进行强制类型转换
String s = (String) lsa[1].get(0); /* 引发 ClassCastException 异常: java.lang.Integer cannot be cast to java.lang.String */
上面的代码不会有任何编译错误,也不会发出警告,但还是会抛出异常。因为程序需要将 lsa 的第二个数组元素的第一个集合元素强制类型转换为 String 类型,所以应该通过 instanceof 运算符来保证它的数据类型:
List<?>[] lsa = new List<?>[10];
// 将 lsa 向上转型为 Object[] 类型的变量
Object[] oa = (Object[]) lsa;
List<Integer> list = new ArrayList<>();
list.add(4);
// 将 List<Integer> 作为 oa 的元素
oa[1] = list;
Object target = lsa[1].get(0);
// 代码安全
if (target instanceof String) {
String s = (String) lsa[1].get(0);
}
与此类似,创建元素类型是类型变量的数组对象也将导致编译错误:
<T> T[] makeArray(Collection<T> cool) {
return new T[cool.size()]; /* 导致编译错误 */
}
这是因为类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器在粗体字代码处报错。