Java 泛型

Java 泛型本质是参数化类型 Parametersized Type 的应用,也就是说操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别被称为泛型类、泛型接口和泛型方法。
我们使用尖括号 <>来表示泛型。Java 7 及以后版本,运行构造器后不需要带完整的泛型信息,只要给出一对尖括号 <> 即可,Java 可以推断出尖括号里应该是什么泛型信息。比如:List<String> list = new ArrayList<>(); 。两个尖括号看起来很想菱形,也称为这种用法为菱形语法

基本概念

类型变量和参数化类型

它们是组成泛型的两个最基本的概念,如泛型 List<T>

  • 类型变量 type variables
    T,即为类型变量,它只能在类,构造方法,普通方法中定义它。
  • 参数化类型 parameterized type
    List<T> 这个整体表示参数化类型,可以用来定义变量,如 List<String> lists。在类型擦除后,并不会保留泛型信息,所以参数化类型实际上是类类型(包含接口)。

相关概念从反射角度也有解释:Type 类型

类型形参和实参

泛型允许在定义类、接口、方法时使用类型参数(type parameter),这个类型参数在声明变量、创建对象、调用方法时动态的指定。

  • 类型形参
    泛型在定义时的参数,如 <K, V>
  • 类型实参
    泛型在使用时指定的参数,即具体参数,如 <K, V 在使用时实际指定的 <Integer, String>。类型实参只能是类类型(包含接口),不能是基本数据类型。
1
2
3
4
5
6
7
// 1. 定义时,为类型形参 E 
public class ArrayList<E> extends AbstractList<E> implements List<E>...{
...
}

// 2. 使用时,需要指定类型实参 String
List<String> list = new ArrayList<String>();

泛型接口

1
2
3
public interface GenericInterface<K, V>{
V put(K key, V value);
}

定义接口时使用泛型声明,指定两个类型形参 K, V,在接口中形参名 K, V 可以作为类型来使用。

泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GenericClass<T> {
private T t;

public GenericClass(T t) {
this.t = t;
}

public void setT(T t){
this.t = t;
}

public T getT(){
return t;
}
}

定义类时使用了泛型声明,指定类型形参为 T,类中使用形参 T 定义了变量 t,并使用形参 T 定义了构造方法。
在定义构造方法时不需要使用类型形参,只有在实例化话时才需要。

泛型方法

泛型方法的类型参数不需要在定义类或者接口时声明,只需要在方法返回值前面声明就可以了。

1
2
3
修饰符 <类型形参列表> 返回值 方法名(方法形参列表){
...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 泛型方法定义
public class GenericClass{
// 构造方法
public <TT> GenericClass(TT tt, String s){...}

// 普通方法
public <M, N> void genericMethod(M key, N value, Map<M, N> map){
map.put(key, value);
}
}
// 2. 泛型方法的 2 种调用方式
GenericClass genericClass = new GenericClass();
HashMap<Integer, String> hashMap1 = new HashMap<>();
HashMap<String, String> hashMap2 = new HashMap<>();
// 调用方式一:显示指定传入的类型
genericClass.<Integer, String>genericMethod(5, "5", hashMap1);
// 调用方式二:Java 7 及之后,编译器会自行推断类型
genericClass.genericMethod("6", "6", hashMap2);

定义方法时使用泛型声明,指定类型形参为 M, N,方法的形参列表方法体中都可以将 M, N 作为类型使用。

值得注意的是,构造方法也可以使用泛型。

泛型继承和子类型

重要概念

在面向对象中有个非常重要的使用方式:向上转型,也就是父类变量引用子类对象。比如:

1
2
3
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK

在方法的形参中,也可以使用向上转型。

1
2
3
4
public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK

在泛型中我们也可以使用向上转型,这也是集合中非常常用的一种方式。

1
2
3
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK

但是请注意:方法的形参列表在使用泛型时,传入的参数必须完全匹配。

1
2
3
4
5
6
7
8
Box<Number> box = new Box<Number>();
Box<Integer> boxI = new Box<Integer>();
Box<Double> boxD = new Box<Double>();
public void boxTest(Box<Number> n) { /* ... */ }

boxTest(box); // OK
boxTest(boxI); // compile-time error
boxTest(boxD); // compile-time error

形参并不能传入 Box<Integer>, Box<Double>。也就是说 Box<Integer>, Box<Double> 并不是 Box<Number> 的子类型

泛型使用的重要概念:给定的两个类型 AB,不管 A, B 是否存在继承关系,MyGenericClass<A>MyGenericClass<B> 都没有任何直接关系,他们的共同父类是 Object

0075-generics-subtypeRelationship.png

泛型类型的继承和接口的实现

泛型接口的实现类,泛型类的子类,在定义泛型时需要注意:

  • 指定具体类型实参
  • 不指定类型参数
  • 指定相同的类型形参
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 指定具体类型实参
public class MyGenericInterface implements
GenericClass.GenericInterface<Integer, String>{

@Override
public String put(Integer key, String value) {
return null;
}
}

// 不指定类型参数
public class SubGenericClass extends GenericClass {
public SubGenericClass(Object o) {
super(o);
}
}

// 指定相同的类型形参
public class SecondSubGenericClass<T> extends GenericClass<T> {

public SecondSubGenericClass(T t) {
super(t);
}
}

示例中泛型接口的实现类指定具体类型实参 Integer, String,继承泛型类时不指定类型参数,默认将类型实参设置为 Object,或者指定相同的类型参数。在实际使用中推荐指定相同的类型参数

泛型类的类型实参如果不同,则不存在继承关系,即使类型实参之间有继承关系。也就是说泛型中只有类型参数相同时,泛型类才存在继承关系

1
2
3
4
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}

PayloadList<String,String>, PayloadList<String,Integer>, PayloadList<String,Exception> 三者的关系如下:

0075-generics-payloadListHierarchy.png

这部分参考教程:Generics, Inheritance, and Subtypes

泛型中类型变量边界限定

语法格式

类型变量 T 可以表示任何类型,通过关键字 extends 可以限制类型变量的边界,语法格式:

1
2
3
4
// 1. 单个限定
<T extends BoundingType>
// 2. 多个限定
<T extends BoundingType1 & BoundingType2 & BoundingType3>

表示 T 是“边界类型”的子类型,“边界类型”可以是类也可以是接口,如果不做边界限定,默认边界为 Object。但是在多限定中,只能有一个类,而且必须放在限定表的第一位;接口的数量和位置没有限制。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class GenericTypeParaBounder<T extends Number>{
T t;

// 1. 单限定
public <T1 extends Number> void testGenericBounder(T1 t1){
//...
}

// 2. 多限定,只能有一个类而且必须放在第一位
public <T2 extends A & B & C> void testGenericMultipleBounder(T2 t2){
//...
}

private class A{}
private interface B{}
private interface C{}

// 3. 多限定,接口数量和位置没有限制
public <T3 extends Comparable & Serializable> void
testGenericMultipleBounder2(T3 t3){
//...
}
}

泛型通配符

通配符

为了解决类型不能动态根据实例来确定的缺点,引入“通配符泛型 ?”。但通配符并不是具体的类型,所以代码中不能使用 ? 作为一种类型。

  • 不限定通配符
    使用 ? 代替类型实参,即它可以是任何类型,如 Class <?>
  • 限定性通配符上界 extends
    <? extends Father> 表示上界,即参数化类型的可能是 Father 或是 Father 的子类。也可以看出不限定通配符实际上是 <? extends Object>
  • 限定性通配符下界 super
    <? super Son> 表示下界,表示参数化类型是 Son 的超类型(父类型),直至 Object

通配符泛型继承

使用通配符可以解决泛型类向上转型时的问题

  • 无限定通配符
    List<Number>, List<Integer> 都是 List<?> 的子类。

0075-generics-listParent.gif

  • 限定通配符
    List<Number>, List<Integer>, List<Long>, List<Float> 都是 List<? extends Number> 的子类。
    List<Integer>, List<Number>, List<Object> 都是 List<? super Integer 的子类。

0075-generics-wildcardSubtyping.gif

  • 示例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 泛型中使用向上转型时,Integer 继承 Number
    // 但是 List<Number> 和 List<Integer> 并没有继承关系
    List<Integer> li = new ArrayList<>();
    List<Number> ln = li; // compile-time error

    // 1. 无限定通配符
    List<?> l = li; // OK

    // 2. 上界
    List<? extends Integer> eli = new ArrayList<>();
    List<? extends Number> eln = eli; // OK

    // 3. 下界
    List<? super Number> sln = new ArrayList<>();
    List<? super Integer> sli = sln; // OK

通配符集合操作注意事项

在集合中,对于边界,上界不能 add,下界不能 get

首先我们需要明白集合中限定性通配符的意义:

1
2
3
4
5
6
7
List<? extends Number> ln = new ArrayList<>();
// 根据限定性通配符上界的继承关系
// ln 表示这个集合可以是 List<Number>,List<Integer>, List<Long>,List<Float> 等等

List<? super Integer> li = new ArrayList<>();
// 根据限定性通配符下界的继承关系
// li 表示这个集合可以是 List<Integer>, List<Number>, List<Object>
  • 上界不能 add 边界
1
2
3
List<? extends Number> ln = new ArrayList<>();
Number n = ln.get(0); // OK
//ln.add(n); // compile-time error.

get/add 的类型展开:

1
2
? extends Number get(int i);
void add(? extends Number);

ln = {List<Number>, List<Integer>, List<Long>, List<Float>...}ln 是其中某一类型,边界是 Number。在 get 时不管拿到的是哪种类型 Number, Integer, Long, Float,都可以向上转型到边界 Number 。而如果 ln 当前是 List<Float>,在 add 时添加边界 Number 会存在不安全的向下转型,导致编译报错。

  • 下界不能 get 边界
1
2
3
List<? super Integer> li = new ArrayList<>();
// Integer i = li.get(0); // compile-time error.
li.add(1); // OK.

get/add 的类型展开:

1
2
? super Integer get(int i);
void add(? super Integer);

li = {List<Integer>, List<Number>, List<Object>...}li 是其中某一类型,边界是 Integer。在 add 边界 Integer 时,不管 li 是哪种类型,Integer 都能正确的向上转型到 Integer, Number, Object。而如果 li 当前是 List<Object>,在 get 时转换为边界 Integer 是不安全的向下转型,导致编译错误。

类型擦除

Java 的泛型只在源码中存在,在编译后的字节码文件中都会被替换为原始类型 Raw Type,并且在相应的地方插入了强制转型代码。 Java 语言泛型的实现方法称为类型擦除Type Erasure),这种实现也被称为伪泛型。
类型擦除后的原始类型为限定类型(如果是不限定则为 Object)。

类型擦除示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 1. 源码
Map<String, String> map = new HashMap<String, String>();
map.put("hello", "world");
System.out.println(map.get("hello"));
// 限定符
List<? extends Number> list = new ArrayList<>();
Number n = list.get(0);

// 2. 反编译结果:类型擦除后在使用时做强行转换
Map map = new HashMap();
map.put("hello", "world");
System.out.println((String) map.get("hello"));
// 限定符使用边界
ArrayList localArrayList = new ArrayList();
Number localNumber = (Number)localArrayList.get(0);

JVM 中关于方法的几个基本概念

Class 文件格式中方法表,包含的几个重要字段:名称、描述符、属性组等。

  • 特征签名
    方法特征签名:仅仅包括方法名称、参数类型以及参数顺序(不包含返回值)。
  • 描述符
    描述符的作用用来描述字段的数据类型、方法的参数列表(包含数量、类型和顺序)和返回值,只有描述符不一致的两个方法才能再同一个 Class 文件中共存。
  • Sigature 属性
    Java 语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量或者参数化类型,则 Signature 属性会记录泛型签名信息。所以类型擦除仅仅是对方法的字节码进行了擦除,实际上 Signature 属性保留了泛型信息。

泛型方法不能重载

如下示例中方法 mytest 重载,但是参数类型都是泛型 List<E>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.List;
import java.util.ArrayList;

public class TestGeneric{
public static int mytest(List<Integer> s) {
return 0;
}

public static String mytest(List<String> s) {
return "";
}

public static void main(String[]agrs){
mytest(new ArrayList<String>());
mytest(new ArrayList<Integer>());
}
}

关于泛型方法的重载,Java 6 和以后的版本表现完全不一样:

  • Java 6 能通过编译并正常运行
    Java 6 只需要 class 文件中方法描述符(返回值不同)不一致就可以共存,并且 Singture 属性保留了泛型信息,所以能正常编译和运行。
  • Java 7 及以后版本不能正常编译
    Java 7 开始,编译开始就检查泛型的特征签名(不包含返回值)。根据特征签名的定义,以及泛型的类型擦除特性,mytest 特征签名是一样的,所以在编译过程中报错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Java 7 及以上不能编译通过
xmt@server005:~/test/java$ java -version
java version "1.7.0_91"
OpenJDK Runtime Environment (IcedTea 2.6.3) (7u91-2.6.3-0ubuntu0.12.04.1)
OpenJDK 64-Bit Server VM (build 24.91-b01, mixed mode)
xmt@server005:~/test/java$ javac TestGeneric.java
TestGeneric.java:9: error: name clash: mytest(List<String>) and
mytest(List<Integer>) have the same erasure
public static String mytest(List<String> s) {
^
1 error

// Java 6 可以编译通过并正常运行
xmt@server005:~/test/java$ java -version
java version "1.6.0_31"
Java(TM) SE Runtime Environment (build 1.6.0_31-b04)
Java HotSpot(TM) 64-Bit Server VM (build 20.6-b01, mixed mode)
xmt@server005:~/test/java$ javac TestGeneric.java
xmt@server005:~/test/java$ java TestGeneric

详细参考:JVM:深入理解Java虚拟机–读书笔记:泛型

泛型方法重写

泛型方法支持重写,支持的原因是 Java 编译器会自动为重写方法添加一个适配的方法,这个方法称为桥方法

  • 源码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    public class Node<T> {
    public T data;

    public Node(T data) {
    this.data = data;
    }

    public void setData(T data) {
    System.out.println("Node.setData");
    this.data = data;
    }


    public class MyNode extends Node<Integer> {
    public MyNode(Integer data) { super(data); }

    @Override
    public void setData(Integer data) {
    System.out.println("MyNode.setData");
    super.setData(data);
    }
    }

    public void testGenericOverride(){
    MyNode mn = new MyNode(5);
    // A raw type - compiler throws an unchecked warning
    Node n = mn;
    n.setData("Hello");
    // Causes a ClassCastException to be thrown.
    Integer x = mn.data;
    }
    }
  • 类型擦除后

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    public class Node {
    public Object data;

    public Node(Object data) { this.data = data; }

    public void setData(Object data) {
    System.out.println("Node.setData");
    this.data = data;
    }

    public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }

    public void setData(Integer data) {
    System.out.println("MyNode.setData");
    super.setData(data);
    }
    }

    public void testGenericOverride(){
    MyNode mn = new MyNode(5);
    //A raw type-compiler throws an unchecked warning
    Node n = (MyNode)mn;
    n.setData("Hello");
    //Causes a ClassCastException to be thrown.
    Integer x = (String)mn.data;
    }
    }

类型擦除后,可以看出 setData 的参数类型并不一样,也就说 setData 在重载。那 Java 中是如何实现重写的呢?

  • 桥方法
    反编译 MyNode 的字节码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    xmt@server005:~/$ javap -v Node\$MyNode.class
    ...

    public void setData(java.lang.Integer);
    flags: ACC_PUBLIC
    Code:
    stack=2, locals=2, args_size=2
    0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
    3: ldc #4 // String MyNode.setData
    5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    8: aload_0
    9: aload_1
    10: invokespecial #6 // Method Node.setData:(Ljava/lang/Object;)V,调用桥方法
    13: return
    LineNumberTable:
    line 19: 0
    line 20: 8
    line 21: 13

    // Java 编译器自动添加的 桥方法
    public void setData(java.lang.Object);
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
    stack=2, locals=2, args_size=2
    0: aload_0
    1: aload_1
    2: checkcast #7 // class java/lang/Integer
    5: invokevirtual #8 // Method setData:(Ljava/lang/Integer;)V
    8: return
    LineNumberTable:
    line 14: 0
    }
    ...

从反编译的字节码中可以看出,多了一个 setData(Object),这个就是编译器自动添加的桥方法,从而实现了重写。

泛型数组

不能直接创建泛型数组

1
2
3
4
5
6
7
8
9
10
11
// 1. Normal Array.
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.

// 2. Cannot create generic array directly.
Object[] stringLists = null;
//stringLists = new List<String>[2]; // compiler error
stringLists[0] = new ArrayList<String>(); // OK
// An ArrayStoreException should be thrown, but the runtime can't detect it.
stringLists[1] = new ArrayList<Integer>();

数组在使用中,数组元素的类型必须一致,否则在运行时会报错 An ArrayStoreException is thrown,比如字符串数组 strings[1] = 100; 赋值一个整数导致运行时报错。同样,如果创建了泛型数组,虽然类型不一致,但是类型擦除后,无法确认具体的类型,运行时并不能检测到错误,所以泛型中禁止直接创建泛型数组

通配符创建泛型数组

1
2
3
4
5
6
7
8
9
10
11
12
13
// 不安全的向下转型 List<String> 是 List<?> 的子类
// List<String>[] lsa = new List<?>[10]; // compiler error,

// OK, array of unbounded wildcard type.
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(3);
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = (String) lsa[1].get(0);

The Java™ Tutorials: Generics ,官网教程给出使用通配符来创建泛型数组。这个示例在运行时,最后一句会抛出类型转换错误 Integer 不能直接转换为 String

反射创建泛型数组

1
2
3
4
5
6
7
8
9
List<String>[] lsa = (List<String>[])Array.newInstance(ArrayList.class, 4);
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(3);
// Correct.
oa[1] = li;
// Run time error, but cast is explicit.
String s = lsa[1].get(0);

使用反射创建数组,并强制转换为泛型数组,此处会给出 Uncheck 警告,没有做类型检查。

显性转换

不管是通配符还是反射创建的泛型数组,在都需要做一次显示的类型转换。通配符是在使用时做转换,反射是在创建时转换。

注意事项 Restrictions on Generics

Java 在泛型使用中有如下约束规则和限制,大都是类型擦除引起,参考Restrictions on Generics

不能使用基本数据类型定义类型参数

泛型类型形参只能是类类型,不能是基本数据类型。

1
2
Pair<int, char> p = new Pair<>(8, 'a');             // compile-time error
Pair<Integer, Character> p = new Pair<>(8, 'a'); // OK!

不能使用类型参数创建实例

不能使用类型参数来创建实例,但是可以通过反射调用 Class.newInstance 方法来构造泛型对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. Error
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}
// 类型擦除后为 Object,显然我们并不是希望实例化 Object

// 2. Right
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
// 通过 cls.newInstance 来构造泛型对象,Class<T> 在实例化的时候,T 要替换成具体类
// 此处不能使用 T.class.newInstance,还是因为泛型擦除会变成 Object

泛型类的静态上下文

即在类上定义的泛型,不能在当前类的静态属性,静态方法,静态代码块,静态类中出现。因为 static 属于类的,只加载一次,在类型擦除后,无法转换为每个具体的类型实参。

1
2
3
4
5
6
7
8
9
10
public class MobileDevice<T> {
private static T os;

// ...
}

// os 将出现混淆?具体是哪种实际类型呢?
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

根据上面示例,os 将出现混淆,无法确定具体的实际类型。所以泛型类中不允许出现在静态上下中。其他错误示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenericRestrictions<Z> {
private static Z z; // compile-time error

public static Z get(){ // compile-time error
return z;
}

static class ZZ{
Z t; // compile-time error
}

static {
Z z; // compile-time error
}

但是我们可以脱离静态上下文(也就是不使用类上定义的泛型),重新指定泛型,这样就可以出现在泛型方法和静态类中了:

1
2
3
4
5
6
7
8
9
10
11
12
// 静态泛型方法
public static <TT> TT getTT(){...}
// 静态类
public static class StaticInnerClassGeneric<S>{
S s;
void set(S s){
this.s = s;
}
S get(){
return s;
}
}

泛型中类型转换 castinstanceof

  • cast
    因为泛型中只有类型参数是同一类型时,才可能存在继承关系;类型参数不同,泛型类没有任何关系。
1
2
3
4
5
6
7
// 参数类型存在继承关系,泛型却没有任何关系
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // compile-time error

// 参数类型必须一致
List<String> l1 = new ArrayList<>();
ArrayList<String> l2 = (ArrayList<String>)l1; // OK
  • instanceof
    类型擦除后,并不知道运行时参数类型具体是什么?
1
2
3
4
5
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // compile-time error
// ...
}
}

方法 rtti 在运行时,传入的参数类型可能是这些:S={ArrayList<Integer>, ArrayList<String>, LinkedList<Character>...},而根据泛型重要概念了解到只有参数类型完全一致才存在继承关系,所以 instanceof 中泛型并不允许直接使用类型实参

1
2
3
4
5
6
public static void rtti(List<?> list) {
// instanceof requires a reifiable type
if (list instanceof ArrayList<?>) { // OK;
// ...
}
}

即:instanceof 中只能使用通配符的泛型

不能创建泛型数组

正常的数组定义和使用中,如果类型不匹配会抛出异常:

1
2
3
4
5
6
7
8
9
10
Object[] strings = new String[2];
strings[0] = "hi"; // OK
strings[1] = 100; // An ArrayStoreException is thrown.

// 泛型
Object[] stringLists = new List<String>[]; // compiler error
stringLists[0] = new ArrayList<String>(); // OK
stringLists[1] = new ArrayList<Integer>();
// An ArrayStoreException should be thrown,
// but the runtime can't detect it.

但是在泛型中,类型擦除后,并不能检查出 ArrayList<Integer>() 的错误,所以泛型禁止创建数组。

但是可以使用泛型数组?????

泛型异常

  • 泛型类不能直接或间接继承 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
  • 不能 catch 泛型实例

    1
    2
    3
    4
    5
    6
    7
    8
    public 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
5
class Parser<T extends Exception> {
public void parse(File file) throws T { // OK
// ...
}
}

泛型比较

1
2
3
4
5
public final class Algorithm {
public static <T> T max(T x, T y) {
return x > y ? x : y; // compile-time error
}
}

泛型不支持直接使用关系运算符 >< 等。通常限定类型变量边界为 Comparable 并通过 compareTo 来比较。

参考文档

0%