前言
虽然平时除了一些集合类以外很少会用到泛型,但深入了解泛型一直是以前的 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 | public class Box { |
在上面的情况下,如果要实现一个存放 String 的 Box 就需要重写另一个 Box。但通过引入泛型,就可以避免以上问题。
下面的代码中的泛型的 Box 类可以通过替换 T 实现复用。
1 | /** |
Java SE 7 以后,只要编译器能从上下文中确定或推断类型参数,就可以用一个空类型集合<>
替换构造函数中的类型参数,即类型推断(Type inference)。
1 | Box<Integer> integerBox = 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 | public class Box<T> { |
泛型接口
与泛型类相似。1
2public 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 | public class Util { |
1 | // 调用方式 |
泛型通配符
有的时候,我们希望在泛型中限定能够使用的类型参数,比如类型参数只准传入某种类型的父类或某种类型的子类,这就需要借助通配符的帮助来实现该功能。
Java 泛型中使用 ?
来表示通配符。
< ? extends T > 上界通配符
若希望为泛型添加上界(即传入的类型实参必须是指定类型的子类型),需要在传入类型参数的后面加入extends
和其上界。
1 | public class Box<T> { |
多个上界的形式<T extends B1 & B2 & B3>
,但是要注意,如果上界中有其中一个是 Class,需要将 Class 放在第一位。1
2
3
4
5
6
7Class 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 | Box<Number> box = new Box<Number>(); |
但是,下面的代码呢,也是正确的吗?
1 | // 假设Box有以下一个boxTest()方法 |
答案是否定的,为什么呢?原因是 Box<Integer>
与 Box<Double>
不是 Box<Number>
的子类型。
例如 Oject 是所有 class 的父类,但是 Oject 的集合并不是所有集合的父类型。例如 Object
是 String
的父类,但 List<Object>
就不是List<String>
的父类型 。Oracle 官网文档中特别指出这是一个在泛型编程中十分常见的误解。
但下面这样的继承关系是存在的。
因此你可以这样实现:1
2
3
4
5
6
7
8interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
PayloadList<String,String>
PayloadList<String,Integer>
PayloadList<String,Exception>
通配符的使用建议(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 | List<? extends Number> arrayList = new ArrayList<Number>; // Number "extends" Number |
super
下面的代码中,在往 arrayList 中 add() 对象时,我们可以保证插入的对象一定是 Number 或是 Number 的父类 Object在;但从 arrayList 中往外取对象时,我们无法确认取出的对象是 Number 还是 Number 的父类 Object(但至少可以取出 Object)。
1 | List<? super Number> arrayList = new ArrayList<Number>; // Number is a "super" of Number |
JDK 中的例子
java.util.Collections 中的 copy() 方法,使用 PECS 原则来保证参数安全:
1 | public static <T> void copy(List<? super T> dest,List<? extends T> src){ |
其中:
dest
为被复制的原集合,需要从中获取元素,所以使用上界<? extends T>
src
为复制后的目标集合,需要往里插入元素,所以使用下界<? super T>
类型擦除(Type Erasure)
先来看看下面的代码,输出的是 true 还是 false 呢?
1 | List <String> l1 = new ArrayList<String>(); |
你可能会回答 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 | List<Integer> arrayList = new ArrayList<>(); |
通过反射获取泛型的实际类型参数
1 | List<Integer> arrayList = new ArrayList<>(); |