夏眠鱼

Oct 27, 2017

Java 泛型-使用限制

为了有效使用泛型,我们必须考虑以下的限制:

  • 不能使用基本类型来初始化泛型类型
  • 不能创建类型参数的实例
  • 不能声明数据类型为类型参数的静态域
  • 不能在参数化类型上使用 Casts 或 instance of
  • 不能创建参数化类型的数组
  • 不能创建、捕获或抛出参数化类型的对象
  • 一个类不能有多个在类型擦除后具有相同签名的重载方法

不能使用基本类型来初始化泛型类型

思考下面的参数化类型:

1
2
3
4
5
6
7
8
9
10
11
12
class Pair<K, V> {

private K key;
private V value;

public Pair(K key, V value) {
this.key = key;
this.value = value;
}

// ...
}

要创建一个 Pair 对象,我们不能使用基本类型来替换类型参数 K 或 V:

1
Pair<int, char> p = new Pair<>(8, 'a'); // 编译错误

我们只能使用非基本类型来替换类型参数 K 或 V:

1
Pair<Integer, Character> p = new Pair<>(8, 'a');

Java 编译器能将 8 和 ‘a’ 自动装箱成 Integer.valueOf(8) 和 Character(‘a’):

1
Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));

不能创建类型参数的实例

思考下面的例子:

1
2
3
4
public static <E> void append(List<E> list) {
E elem = new E(); // 编译错误
list.add(elem);
}

变通一下,我们可以使用反射来创建:

1
2
3
4
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // 编译正确
list.add(elem);
}

最后我们可以调用 apend 方法了:

1
2
List<String> ls = new ArrayList<>();
append(ls, String.class);

不能声明数据类型为类型参数的静态域

类的静态字段是类级别的变量,为类中的所有非静态对象所共享。所以,数据类型为类型参数的静态字段是不允许使用的。思考下面的例子:

1
2
3
4
5
public class MobileDevice<T> {
private static T os;

// ...
}

如果数据类型为类型参数的静态域是允许的,那么下面的代码就会让人很困惑:

1
2
3
MobileDevice<Smartphone> phone = new MobileDevice<>();
MobileDevice<Pager> pager = new MobileDevice<>();
MobileDevice<TabletPC> pc = new MobileDevice<>();

因为静态字段 os 被 phone、pager 和 pc 对象共享,那么 os 的实际类型是什么?不能同时是 SmartPhone、Pager 和 TablePC 吧。所以我们不能创建数据类型为类型参数的静态字段。

不能在参数化类型上使用 Casts 或 instance of

思考下面的例子:

1
2
3
4
5
public static <E> void rtti(List<E> list) {
if (list instanceof ArrayList<Integer>) { // 编译错误
// ...
}
}

因为 Java 编译器会擦除泛型代码中的所有类型参数,所以在运行时我们就不能区分泛型中的参数类型。

传递给rtti方法的参数化类型为:
S = { ArrayList, ArrayList LinkedList, … }

因为运行时不跟踪类型参数,因此无法区分 ArrayList<Integer>ArrayList<String>。我们最多只能使用一个无界的通配符来验证列表是否为 ArrayList:

1
2
3
4
5
public static void rtti(List<?> list) {
if (list instanceof ArrayList<?>) { // 编译正确
// ...
}
}

通常,除非使用无界通配符参数化,否则不能将其转换为参数化类型。例如:

1
2
List<Integer> li = new ArrayList<>();
List<Number> ln = (List<Number>) li; // 编译错误

然而,在某些情况下,编译器知道类型参数总是有效的就允许转换。例如:

1
2
List<String> l1 = ...;
ArrayList<String> l2 = (ArrayList<String>)l1; // 编译正确

不能创建参数化类型的数组

下面的例子不能通过编译:

1
List<Integer>[] arrayOfLists = new List<Integer>[2]; // 编译错误

下面的代码演示了将不同类型插入到数组中的情况:

1
2
3
Object[] strings = new String[2];
strings[0] = "hi"; // 编译正确
strings[1] = 100; // 抛出 ArrayStoreException 异常

如果我们套在泛型上,也会出现同样的问题:

1
2
3
Object[] stringLists = new List<String>[];  // 编译错误,这里我们假设能编译正确
stringLists[0] = new ArrayList<String>(); // 编译正确
stringLists[1] = new ArrayList<Integer>(); // 本来应该会抛出 ArrayStoreException 异常,但是运行时不能发现这个问题

如果允许参数化列表的数组,那么上面的代码将无法抛出 ArrayStoreException。

不能创建、捕获或抛出参数化类型的对象

泛型类不能直接或间接的继承 Throwable 类,思考下面的例子:

1
2
3
class MathException<T> extends Exception { /* ... */ } // 编译错误

class QueueFullException<T> extends Throwable { /* ... */ } // 编译错误

方法不能捕获类型参数的实例,思考下面的例子:

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) { // 编译错误
// ...
}
}

但是我们可以在 throws 子句中使用类型参数:

1
2
3
4
5
class Parser<T extends Exception> {
public void parse(File file) throws T { // 编译正确
// ...
}
}

一个类不能有多个在类型擦除后具有相同签名的重载方法

思考下面的例子:

1
2
3
4
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}

上面的重载方法,当类型擦除后会共享相同的方法签名,导致编译错误。

泛型的学习到这里已经结束了,想必你已经感受到它的强大了吧,但是使用好它并不容易。

OLDER > < NEWER