Java泛型原理及总结

前言

虽然平时除了一些集合类以外很少会用到泛型,但深入了解泛型一直是以前的 Todo list 之一,拖了挺久了,这次就借这篇笔记来一次解决。

这篇笔记参考自 Oracle Java 官方文档中的 Generics 部分与 Baeldung 的 这篇文章,示例代码也来自于Oracle Java 官方文档。

重点:通配符、PECS 原则、类型擦除。

关于泛型

“In a nutshell, generics enable types (classes and interfaces) to be parameters when defining classes, interfaces and methods. Much like the more familiar formal parameters used in method declarations, type parameters provide a way for you to re-use the same code with different inputs. The difference is that the inputs to formal parameters are values, while the inputs to type parameters are types.”

泛型(Generics)可以参数化类型,提供了一种处理不同输入下复用代码的方式。普通参数输入的是值,而泛型参数的输入是类型。简而言之,泛型就是类型参数(Type Parameters)

那么,为何要引入泛型呢,Oracle 是这么解释的:

“Generics were introduced to the Java language to provide tighter type checks at compile time and to support generic programming.”

为了编译期中提供更为严格的类型检查,泛型被引入了 Java 中。简单来说,泛型可以将错误 / 异常提前到编译期,而不是运行期,这样我们就可以在编译期解决问题,而不是等到其在运行期才暴露出来。

泛型中的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类泛型接口泛型方法

泛型的优点

  • Stronger type checks at compile time (编译期中更具健壮性的类型检查)
    Java 编译器会在编译期对泛型进行类型安全检查,编译报错相比运行错误更容易发现。
  • Elimination of casts (消除强转)
  • Enabling programmers to implement generic algorithms (编写更具复用性的程序)
    使用泛型,可以令程序灵活处理不同的参数类型而不用改变代码。

泛型类

泛型类的定义方式:class name<T1, T2, ..., Tn> { /* ... */ }

先定义一个非泛型的 Box 类,你可以自由地往 Box 中传入任何对象。但在程序编译时,编译器无法确定这个 Box 类的类型。设想在这样的一个场景中:某处的代码 set 了一个 Integer 给 object,但在另一处代码用 String 的方式使用 get 了 Box 内的 object,此时就会报一个 ClassCastException。

1
2
3
4
5
6
public class Box {
private Object object;

public void set(Object object) { this.object = object; }
public Object get() { return object; }
}

在上面的情况下,如果要实现一个存放 String 的 Box 就需要重写另一个 Box。但通过引入泛型,就可以避免以上问题。

下面的代码中的泛型的 Box 类可以通过替换 T 实现复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;

public void set(T t) { this.t = t; }
public T get() { return t; }
}

Box<Integer> integerBox = new Box<Integer>();
Box<String> stringBox = new Box<String>();

Java SE 7 以后,只要编译器能从上下文中确定或推断类型参数,就可以用一个空类型集合<>替换构造函数中的类型参数,即类型推断(Type inference)

1
2
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();

类型参数的命名规则

按照惯例,类型参数的命名规则是单个大写字母,例如:

  • E - Element (used extensively by the Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types

注意,泛型的类型参数不可使用八大基本类型。

原始类型(Raw Types)

“A raw type is the name of a generic class or interface without any type arguments.

原始类型就是没有任何类型参数的泛型类或接口,例如:

1
2
3
4
5
6
7
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}

// rawBox就是一个Raw Type
Box rawBox = new Box();

泛型接口

与泛型类相似。

1
2
public interface Box<T> {
}

泛型方法

“Generic methods are methods that introduce their own type parameters. “

泛型方法在调用方法时指明泛型的具体类型。

和泛型类做一下比较,泛型类是在实例化类时指明泛型的具体类型。

“The syntax for a generic method includes a list of type parameters, inside angle brackets, which appears before the method’s return type. For static generic methods, the type parameter section must appear before the method’s return type.

泛型方法在方法返回类型前的尖括号<>内包括了一系列的泛型类型参数,对于静态的泛型方法,泛型类型参数必须出现在方法返回类型的前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Util {
// 比较两个对象是否相等的compare()泛型方法
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}

public class Pair<K, V> {
private K key;
private V value;

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

public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
1
2
3
4
5
6
// 调用方式
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
// 或者通过类型推断,让编译器自动推倒出类型参数
boolean same = Util.compare(p1, p2);

泛型通配符

有的时候,我们希望在泛型中限定能够使用的类型参数,比如类型参数只准传入某种类型的父类或某种类型的子类,这就需要借助通配符的帮助来实现该功能。

Java 泛型中使用 ?来表示通配符。

< ? extends T > 上界通配符

若希望为泛型添加上界(即传入的类型实参必须是指定类型的子类型),需要在传入类型参数的后面加入extends和其上界。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Box<T> {
private T t;
public void set(T t) {this.t = t;}
public T get() {return t;}

// 声明上界为Number
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}

public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}

多个上界的形式<T extends B1 & B2 & B3>,但是要注意,如果上界中有其中一个是 Class,需要将 Class 放在第一位。

1
2
3
4
5
6
7
Class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }

class D <T extends A & B & C> { /* ... */ }
// A 不在第一位,会报编译错误
class D <T extends B & A & C> { /* ... */ } // compile-time error

< ? super T > 下界通配符

类似地,为泛型添加下界(即传入的类型实参必须是指定类型的父类型),需要在传入类型参数的后面加入super和其下界。

< ? > 无界通配符

接受任何类型,没什么好说的。

泛型通配符中的继承关系与子类型问题

下面的代码是正确的:

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
// 假设Box有以下一个boxTest()方法
public void boxTest(Box<Number> n) { /* ... */ }

box.boxTest(Box<Integer>) // OK???
box.boxTest(Box<Double>) // OK???

答案是否定的,为什么呢?原因是 Box<Integer>Box<Double> 不是 Box<Number> 的子类型。

generics-subtypeRelationship

例如 Oject 是所有 class 的父类,但是 Oject 的集合并不是所有集合的父类型。例如 ObjectString 的父类,但 List<Object> 就不是List<String> 的父类型 。Oracle 官网文档中特别指出这是一个在泛型编程中十分常见的误解。

但下面这样的继承关系是存在的。

generics-sampleHierarchy

因此你可以这样实现:

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

PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>

generics-payloadListHierarchy

通配符的使用建议(PECS原则)

PECS 原则来自《Effective Java》 中的第28条,即“Producer Extends,Consumer Super”

  • 从集合中获取 T,集合为生产者,使用上界 <? extends T>

  • 往集合中插入 T,集合为消费者,使用下界 <? super T>

  • 如果想同时读写,不要使用通配符

extends

来看看下面的代码,从 arrayList 中往外取对象时,我们可以保证取出来的对象是 Number 或是 Number 的子类;但往 arrayList 中 add() 对象时,我们无法确认存放的对象类型是 Number、Integer 还是 Double。

1
2
3
List<? extends Number> arrayList = new ArrayList<Number>;  // Number "extends" Number
List<? extends Number> arrayList = new ArrayList<Integer>; // Integer extends Number
List<? extends Number> arrayList = new ArrayList<Double>; // Double extends Number

super

下面的代码中,在往 arrayList 中 add() 对象时,我们可以保证插入的对象一定是 Number 或是 Number 的父类 Object在;但从 arrayList 中往外取对象时,我们无法确认取出的对象是 Number 还是 Number 的父类 Object(但至少可以取出 Object)。

1
2
List<? super Number> arrayList = new ArrayList<Number>; // Number is a "super" of Number
List<? super Number> arrayList = new ArrayList<Object>; // Object is a "super" of Number

JDK 中的例子

java.util.Collections 中的 copy() 方法,使用 PECS 原则来保证参数安全:

1
2
3
public static <T> void copy(List<? super T> dest,List<? extends T> src){
// ...
}

其中:

  • dest 为被复制的原集合,需要从中获取元素,所以使用上界 <? extends T>
  • src 为复制后的目标集合,需要往里插入元素,所以使用下界 <? super T>

类型擦除(Type Erasure)

先来看看下面的代码,输出的是 true 还是 false 呢?

1
2
3
List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());

你可能会回答 false,但是答案其实是 true。原因就是泛型中的类型擦除(because all instances of a generic class have the same run-time class, regardless of their actual type parameters)。所有的泛型实例在 JVM 中都是相同的,不管它们的原始参数类型是什么。

Java 编译器会在编译期将擦除所有的类型参数,并将其替换为其边界,如果没有绑定,则替换为 Object,最终编译后的字节码中只包含普通的类、接口与方法,

Oracle 的 Java 文档中指出,引入类型擦除是为了和 JAVA SE 5 之前的代码兼容。

类型擦除的补偿

由于类型擦除的存在,所以在使用泛型时无法使用new T(),也不能使用instanceof,,因为这两个方法都要知道确切的类型。

但通过设计模式中的模板模式与工厂模式可以实现类型擦除的补偿。

泛型与反射

通过反射跳过编译器的类型安全检查

使用反射可绕过编译器,往某个泛型集合中加入其它类型数据。

1
2
3
4
5
6
7
8
9
List<Integer> arrayList = new ArrayList<>();
arrayList.add(23); // OK
arrayList.add("String"); // Error
arrayList.add(9.9f); // Error

// 利用反射调用add()插入其他类型数据
Method addMethod = arrayList.getClass().getDeclaredMethod("add",Object.class);
addMethod.invoke(arrayList,"String"); // OK
addMethod.invoke(arrayList,9.9f); // OK

通过反射获取泛型的实际类型参数

1
2
3
4
List<Integer> arrayList = new ArrayList<>();
Method getMethod = arrayList.getClass().getDeclaredMethod("get",Object.class);
// 反射获得泛型类型参数
Type [] types = getMethod.getGenericParameterTypes();
  • 本文作者: Marticles
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!