一、Java 泛型历史
泛型
移植的过程,一开始并非朝着类型擦除的方向前进,事实 Pizza 中泛型更加类似于 C# 中的泛型。但是由于Java 需要做到严格的向后兼容性,所以不得不采用泛型擦除
,只需要改动Javac
编译器,不需要改动字节码,不需要改动虚拟机,也保证了之前历史没有泛型代码还可以在新的 JDK 中运行。- 采用
泛型擦除
也舍去了部分高端特性,比如在运行期间无法获取泛型实际类型。
虽然 Java
泛型相对于 C++
、 C#
等语言来说就是一语法糖,但是由于Java严格的向后兼容,导致现在高不成低不就,只是简单骗过编译器而已。希望在后续JDK版本为我们带来”船”新体验。
二、泛型定义
泛型(Generics)
,又叫显式参数多态。代码用显式地以类型作为参数 代入
。泛型在 c++
里又叫 模板
。
2.1 为什么要使用泛型
- 编译时的错误可以在早期检测到,程序员发现并及时处理。
泛型
使更多的错误在编译时可检测,从而增加了代码的稳定性。 - 减少强制转换代码 ```java // 不使用泛型 List list = new ArrayList(); list.add(“hello”); String s = (String) list.get(0);
// 使用泛型
List
- 能够实现 `泛型` 算法: 实现适用于不同类型集合的泛型算法,可以进行定制,并且类型安全且易读。
- 虽然接口可以突破继承体系限制(单继承),但是一旦指定了接口,就要求代码必须使用特定的接口。但是通过 `泛型` 可以编写更通用的代码,能够适用**非特定的类型,而不是一个具体的接口或类。**
- `泛型` 是 ` Java 5 ` 的重大变化之一,它实现了 `参数化类型` ,这样你编写的组件(通常是 `集合` )可以适用于多种类型,促成泛型出现的最主要的动机之一是为了创建集合类。
<a name="b6Lru"></a>
# 三、泛型类型
<a name="I2la7"></a>
## 3.1 简单泛型
<a name="HCyIu"></a>
## 3.2 泛型接口
<a name="N7jBJ"></a>
## 3.3 泛型方法
- 泛型方法独立于类而改变方法。作为准则,请尽可能使用 `泛型方法` 。
- 如果方法是 `static` 的,则无法访问该 `类` 的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
- 一个非泛型类可以存在 `泛型方法` 。
<a name="W7m6B"></a>
### 3.3.1 非静态泛型方法
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
```java
public class GenericMethods {
public <T> void f(T x) {
System.out.println(x.getClass().getName());
}
}
对于 泛型类
,必须在 实例化
该类时指定类型参数。使用 泛型方法
时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为类型参数推断
。
如果使用基本类型调用 f()
, 自动装箱
就开始起作用,自动将基本类型 包装
在它们对应的 包装类型
中。
3.3.2 静态泛型方法
public class Util {
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());
}
}
// 静态泛型方法调用
boolean same = Util.<Integer, String>compare(p1, p2);
boolean same = Util.compare(p1, p2); // 这样也可以编译通过
3.3.3 变长参数和泛型方法
public class GenericVarargs {
@SafeVarargs
public static <T> List<T> makeList(T... args) {
List<T> result = new ArrayList<>();
for (T item : args)
result.add(item);
return result;
}
...
}
@SafeVarargs
注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。
3.4 泛型示例
3.4.1 泛型的实现 Supplier的类
public class BasicSupplier <T> implements Supplier<T> {
private Class<T> type;
public BasicSupplier(Class<T> type) {
this.type = type;
}
@Override
public T get() {
Throwable throwable;
try {
return type.getDeclaredConstructor().newInstance();
} catch (InstantiationException e) {
throwable = e;
} catch (IllegalAccessException e) {
throwable = e;
} catch (InvocationTargetException e) {
throwable = e;
} catch (NoSuchMethodException e) {
throwable = e;
}
throw new RuntimeException(throwable);
}
// Produce a default Supplier from a type token:
public static <T> Supplier<T> create(Class<T> type) {
return new BasicSupplier<>(type);
}
}
class Tea {
private static long counter = 0;
private final long id = counter++;
public long id() {
return id;
}
@Override
public String toString() {
return "CountedObject " + id;
}
}
@Test
public void testGetStreamApi() {
Stream.generate(BasicSupplier.create(Tea.class))
.limit(10)
.forEach(System.out::println);
}
四、泛型擦除
Java
语言引入了泛型,以在编译时提供更严格的类型检查并支持泛型编程。为了实现泛型,Java编译器将 泛型擦除
应用于:
- 如果类型参数不受限制,则将通用类型中的所有类型参数替换为其边界或对象。因此,产生的字节码仅包含普通的
类
,接口
和方法
。 - 必要时插入
类型转换
,以保持类型安全。 - 生成
桥接
方法以在扩展的泛型类型中保留多态
。
类型擦除可确保不会为参数化类型创建新的类;因此,泛型不会产生运行时开销。
Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。
4.1 擦除泛型类型
4.1.1 无边界擦除
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
由于泛型参数 T
是无边界的,所以Java编译器将会使用 Object
替换它:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
4.1.2 有边界擦除
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
Java编译器将会使用第一个绑定参数替换:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
4.2 擦除泛型方法
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
// 无边界将会使用Object替换
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
// 存在边界,则将边界替换泛型T
public static <T extends Shape> void draw(T shape) { /* ... */ }
public static void draw(Shape shape) { /* ... */ }
4.3 桥接方法
当编译一个继承参数化的类或实现参数化的接口时,编译器可能需要创建一个 桥接方法
。通常不需要过于担心,可能在堆栈跟踪时出现桥接方法,这会让你十分困惑。
存在如下类的继承关系:
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); }
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
}
泛型擦除后
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
// 无边界的泛型T被Object替换
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);
}
}
当类型被擦除后,方法签名不匹配。为了解决这个问题且在泛型擦除后保持泛型类型的 多态性
,Java 编译器生成一个 桥接方法
确保子类按预期工作。生成 桥接
代码如下:
class MyNode extends Node {
// Bridge method generated by the compiler
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
System.out.println("MyNode.setData");
super.setData(data);
}
// ...
}
4.4 擦除所带来的问题
擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如 转型
、 instanceof
操作和 new
表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。
当你希望将类型参数不仅仅当作 Object 处理时,就需要付出额外努力来管理 边界
。
4.5 边界处的动作
4.5.1 无泛型类
public class SimpleHolder {
private Object obj;
public void set(Object obj) {
this.obj = obj;
}
public Object get() {
return obj;
}
public static void main(String[] args) {
SimpleHolder holder = new SimpleHolder();
holder.set("Item");
String s = (String) holder.get();
}
}
4.5.2 有泛型类
public class GenericHolder2<T> {
private T obj;
public void set(T obj) {
this.obj = obj;
}
public T get() {
return obj;
}
public static void main(String[] args) {
GenericHolder2<String> holder = new GenericHolder2<>();
holder.set("Item");
String s = holder.get();
}
}
字节码如下
所产生的字节码是相同的。对进入 set()
的类型进行检查是不需要的,因为这将由编译器执行。而对 get()
返回的值进行转型仍然是需要的,只不过不需要你来操作,它由编译器自动插入,这样你就不用编写(阅读)杂乱的代码。 get()
和 set()
产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住: 边界就是动作发生的地方
。
六、补偿擦除
6.1 创建类型实例
6.1.1 方式一: 使用显式工厂泛型
因为擦除,我们将失去执行泛型代码中某些操作的能力。即 无法在运行时知道确切类型
:
// 编译失败: 因为泛型擦除,我们无法直接通过泛型创建数组。但在C++中当前操作非常自然且安全
public class Erased<T> {
private final int SIZE = 100;
public void f(Object arg) {
// error: illegal generic type for instanceof
if (arg instanceof T) {
}
// error: unexpected type
T var = new T();
// error: generic array creation
T[] array = new T[SIZE];
// warning: [unchecked] unchecked cast
T[] array = (T[]) new Object[SIZE];
}
}
解决方案
- 通过实现
Supplier
接口表示这是一个工厂类,提供get()
方法创建实例对象。 - 内部使用
Class
保持泛型的实际类型。 ```java import java.util.function.Supplier;
class ClassAsFactory
ClassAsFactory(Class<T> kind) {
this.kind = kind;
}
@Override
public T get() {
try {
return kind.newInstance();
} catch (InstantiationException |
IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
这样可以编译,但对于 ` ClassAsFactory<Integer>` 会失败,这是因为 `Integer` 没有无参构造函数。由于错误不是在编译时捕获的,因此语言创建者不赞成这种方法。他们建议使用 `显式工厂(Supplier)` 并约束类型,以便只有实现该工厂的类可以这样创建对象。
```java
public class Suppliers {
// Create a collection and fill it:
public static <T, C extends Collection<T>> C
create(Supplier<C> factory, Supplier<T> gen, int n) {
return Stream.generate(gen)
.limit(n)
.collect(factory, C::add, C::addAll);
}
// Fill an existing collection:
public static <T, C extends Collection<T>>
C fill(C coll, Supplier<T> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(coll::add);
return coll;
}
// Use an unbound method reference to
// produce a more general method:
public static <H, A> H fill(H holder,
BiConsumer<H, A> adder, Supplier<A> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(a -> adder.accept(holder, a));
return holder;
}
}
class IntegerFactory implements Supplier<Integer> {
private int i = 0;
@Override
public Integer get() {
return ++i;
}
}
class Widget {
private int id;
Widget(int n) {
id = n;
}
@Override
public String toString() {
return "Widget " + id;
}
public static
class Factory implements Supplier<Widget> {
private int i = 0;
@Override
public Widget get() {
return new Widget(++i);
}
}
}
class Fudge {
private static int count = 1;
private int n = count++;
@Override
public String toString() {
return "Fudge " + n;
}
}
class Foo2<T> {
private List<T> x = new ArrayList<>();
Foo2(Supplier<T> factory) {
Suppliers.fill(x, factory, 5);
}
@Override
public String toString() {
return x.toString();
}
}
public class FactoryConstraint {
public static void main(String[] args) {
// System.out.println(new Foo2<>(new IntegerFactory()));
// System.out.println(new Foo2<>(new Widget.Factory()));
Foo2<Integer> integerFoo2 = new Foo2<>(new IntegerFactory());
Foo2<Widget> widgetFoo2 = new Foo2<>(new Widget.Factory());
System.out.println(new Foo2<>(Fudge::new));
}
}
/* Output:
[1, 2, 3, 4, 5]
[Widget 1, Widget 2, Widget 3, Widget 4, Widget 5]
[Fudge 1, Fudge 2, Fudge 3, Fudge 4, Fudge 5]
*/
IntegerFactory
本身就是通过实现Supplier<Integer>
的工厂。Widget
包含一个内部类,它是一个工厂。Fudge
并没有做任何类似于工厂的操作,并且传递Fudge::new
仍然会产生工厂行为,因为编译器将对函数方法::new
的调用转换为对get()
的调用。System.out.println(new Foo2<>(Fudge::new));
语句对应字节码
如下:
6.1.2 使用模板模式
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() {
element = create();
}
abstract T create();
}
class X {
}
class XCreator extends GenericWithCreate<X> {
@Override
X create() {
return new X();
}
void f() {
System.out.println(
element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
XCreator xc = new XCreator();
xc.f();
}
}
/* Output:
X
*/
6.2 泛型数组
我们无法使用如下语句创建 泛型数组
:
T[] t = T[123];
通用解决方案是在试图创建泛型数组的时候使用 ArrayList
:
public class ListOfGenerics<T> {
private List<T> array = new ArrayList<>();
public void add(T item) {
array.add(item);
}
public T get(int index) {
return array.get(index);
}
}
这样做可以获得数组的行为,并且还具有泛型提供的编译时类型安全性。
有时候,仍然会创建泛型数组。若采用以下方式则不行:
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {
// 我们永远无法创建具有该确切类型(包括类型参数)的数组,
// 所有数组,无论它们持有什么类型,都具有相同的结构(每个数组插槽的大小和数组布局),
// 因此似乎可以创建一个 Object 数组并将其转换为所需的数组类型。
// 实际上,这确实可以编译,但是会产生 ClassCastException :
// 问题在于数组会跟踪其实际类型,而该类型是在创建数组时建立的。
// 强转信息 (Generic<Integer>[]) 只会在编译时存在,在运行时它仍然是一个Object数组,这会引起问题。
// 成功创建泛型类型的数组的唯一方法是创建一个已擦除类型的新数组,并将其强制转换。
gia = (Generic<Integer>[]) new Object[SIZE];
} catch (ClassCastException e) {
System.out.println(e.getMessage());
}
// 正常
gia = (Generic<Integer>[]) new Generic[SIZE];
gia[0] = new Generic<>();
}
}
即使使用 Object[]
保存数据,但是强制转换还是会出问题
public class GenericArray2<T> {
// 最好在集合中使用Object[],并在使用数组元素时向T添加强制类型转换
// 在内部将数组视为 Object[] 而不是 T[] 的优点是,
// 我们不太可能会忘记数组的运行时类型并意外地引入了bug,
// 尽管大多数(也许是全部)此类错误会在运行时被迅速检测到。
private Object[] array;
public GenericArray2(int sz) {
array = new Object[sz];
}
public void put(int index, T item) {
array[index] = item;
}
@SuppressWarnings("unchecked")
public T get(int index) {
return (T) array[index];
}
// 这里强制转换仍然会报错,并在编译时生成警告。
// 因此,无法破坏基础数组的类型,该基础数组只能是Object[]
@SuppressWarnings("unchecked")
public T[] rep() {
return (T[]) array; // Unchecked cast
}
}
6.2.1 使用类型标记实现泛型数组
public class GenericArrayWithTypeToken<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithTypeToken(Class<T> type, int sz) {
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
// Expose the underlying representation:
public T[] rep() {
return array;
}
public static void main(String[] args) {
GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
// This now works:
Integer[] ia = gai.rep();
}
}
类型标记 Class<T>
被传递到构造函数中,弥补泛型擦除后缺少的类型。
七、边界
边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中 调用方法
。
interface HasColor {
java.awt.Color getColor();
}
class WithColor<T extends HasColor> {
T item;
WithColor(T item) {
this.item = item;
}
T getItem() {
return item;
}
// The bound allows you to call a method:
java.awt.Color color() {
return item.getColor();
}
}
当我们使用类似 T
这类无边界泛型参数时,Java 编译器会使用 Object
替换。因此,我们不能获取到真实类型的方法。但是可以通过边界可以让我们绑定 方法
。
八、通配符
8.1 概念
可变性
是以一各类型安全的方式将一个对象当做另一个对象来使用。如果不能将一个类型替换为另一个类型,那么这个类型就称之为不变量
。 协变
和逆变
是两个相互独立的概念。
- 如果某个返回的类型可以由其
派生类型(子类)
替换,那么这个类型就是支持协变
。 如果某个参数类型可以由其
基类(父类)
替换,那么这个类型就是支持逆变
。8.2 extend
Apple
的List
不是Fruit
的List
。Apple
的List
将持有Apple
和Apple
的子类型,Fruit
的List
将持有任何类型的Fruit
,但是它不是一个Apple
的List
,它仍然是Fruit
的List
。Apple
的List
在类型上不等价
于Fruit
的List
,即使Apple
是一种Fruit
类型。- 与
数组
不同,泛型没有内建的协变类型
。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及采用什么规则。
但是如果你想建立某种向上转型关系, 通配符
可以产生这种关系:
但使用 add
方法添加对象,即便这个对象继承或为 Fruit
,编译器也会报错。因为 ArrayList
泛型方法 add(T)
中的 T
变为? extends Fruit
,意味着它可以是任何事件,编译器无法验证 任何事件
的类型安全性。
8.2 逆变
使用超类型通配符
。表示由特定类的任何基类
来界定的。
使用 super
关键字,编译器就会知道向其中添加 Apple
或其子类是安全的。
8.3 无通配符
List
实际上表示持有任何 Object 类型的原生 List
。List<?>
表示具有某种特定类型的非原生 List
。
九、关于泛型问题
- 任何基本类型都不能作为类型参数。比如
ArrayList<int>
是错误的。解决方案是基于基本类型的包装类及自动装箱机制。但这样性能会受影响。 - 一个类不能实现同一个泛型接口的两种变体。因为泛擦除原因,相当于同时实现两个相同的接口。
- 使用带泛型类型的转型或
instanceof
不会有任何效果。
class Manipulator<T> {
private T obj;
Manipulator(T x) {
obj = x;
}
// Error: cannot find symbol: method f():
public void manipulate() {
obj.f();
}
}
public class Manipulation {
public static void main(String[] args) {
HasF hf = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hf);
manipulator.manipulate();
}
}
用 C++
编写这种代码很简单,因为当模版被实例化时,模版代码就知道模版参数的类型。因为擦除, Java
编译器无法将一个泛型方法中的类映射到事实方法上。为了调用 f()
,我们必须协助泛型类,给定泛型类一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends
关键字。
public class Manipulator2<T extends HasF> {
private T obj;
Manipulator2(T x) {
obj = x;
}
public void manipulate() {
obj.f();
}
}
泛型类型参数会擦除到它的第一个边界,可能有多个边界。
泛型参数擦除,编译器实际上会把类型参数替换为它的擦除。
泛型只有在类型参数比某个具体类型(以及其子类)更加 泛化
—— 代码能跨多个类工作时才有用。
为了减少潜在的关于擦除的困惑,你必须清楚地认识到这不是一个语言特性。它是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的,所以就有了这种妥协。
基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List
擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为 迁移兼容性
。
因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。在确定了这个目标后,Java 设计者们和从事此问题相关工作的各个团队决策认为擦除是唯一可行的解决方案。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。
get()
和 set()
产生了相同的字节码,这就告诉我们泛型的所有动作都发生在边界处——对入参的编译器检查和对返回值的转型。这有助于澄清对擦除的困惑,记住: 边界就是动作发生的地方
。
原始类型
原始类型
是没有任何类型参数的泛型 类
或 接口
的名称。
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
// #1 创建带泛型参数
Box<Integer> intBox = new Box<>();
// #2 省略泛型参数
Box rawBox = new Box();
原始类型出现在遗留代码中,因为许多 API 类(例如 Collections
类)在 JDK 5.0
之前不是通用类。当使用原始类型时,您实际上得到了 预泛型行为(pre-generics)
,即 泛型使用Object
代替。
若使用原始类型绕过安全检查,将不安全代码的捕获延迟到 进行时
。因此,应该避免使用原始类型。
将历史遗留的代码与泛型代码混合使用时,可能会遇到如 警告
信息:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
unchecked
: 编译器没有足够的类型信息执行所有必要的类型检查。默认情况下禁用。若要查看所有unchecked
警告,使用-Xlint: unchecked
重新编译。
类型参数命名约定
E
- Element(通常用于集合框架)K
- Key(键)N
- Number(数值)T
- Type (类型)V
- Value (值)S,U,V
etc. - 2nd, 3rd, 4th types(第二,第三,第四类)