泛型是JDK1.5引入的新特性,简而言之就是将原来的具体类型进行参数化,也就是将所操作的数据类型指定为一个参数,在真正使用时传入具体的类型。这种参数可以在类、接口或方法的创建中使用,分别称之为泛型类、泛型接口和泛型方法。例如:

  1. // 泛型接口
  2. public interface List<E> extends Collection<E> {
  3. }
  4. // 泛型类
  5. public class ArrayList<E> extends AbstractList<E>
  6. implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
  7. }
  8. // 泛型方法
  9. public static <T> Stream<T> stream(T[] array) {
  10. return stream(array, 0, array.length);
  11. }

为什么使用泛型

1. 编译期间强类型检查

Java编译器会对代码进行类型检查,如果代码违反类型安全就会报错。编译期错误比运行期错误更容易修复。
注意:泛型只在编译期间有效,在运行期间会进行类型擦除,所以运行期间是没有泛型的。

2. 消除类型转换

在JDK1.5之前,Collection集合的参数类型为Object,这样就会导致获取数据是需要类型转换。而类型转换很容易触发ClassCastException异常。但是使用了泛型之后,就不存在类型转换问题了:

  1. // 需要类型转换
  2. List list = new ArrayList();
  3. list.add("hello");
  4. String s = (String) list.get(0);
  5. // 不需要类型转换
  6. List<String> list = new ArrayList<>();
  7. list.add("hello");
  8. String s = list.get(0);

3. 实现通用的算法,减少代码量和异常率

通过使用泛型,开发者可以实现适用于不同类型集合的泛型算法,这些算法可以自定义,类型安全且易于阅读。

泛型类(Generic Types)

泛型类型指的是一个参数化类型的类或接口。

对于类型参数的命令,通常使用一个单大写字母来进行表示:

E - Element K - Key N - Number T - Type V -Value

定义一个泛型类:

  1. public class Box<T> {
  2. // T stands for "Type"
  3. private T t;
  4. public void set(T t) { this.t = t; }
  5. public T get() { return t; }
  6. }

实例化该泛型:

  1. Box<Integer> integerBox = new Box<Integer>();

在JDK1.7之后添加了泛型的类型推导,JDK1.7之后就写成了一下的形式:

  1. Box<Integer> integerBox = new Box<>();

原生类(RawTypes)

原生类型指的是泛型类去掉泛型参数部分的名称。例如:

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

为了创建一个参数化类型Box,需要为泛型参数T提供一个真实类型:

  1. Box<Integer> intBox = new Box<>();

但是如果真实的参数类型被省略了,创建的就是原生类型:

  1. Box rawBox = new Box();

因此,Box是泛型类Box的原生类型。
注意:非泛型类是不存在原生类型的。

原生类型出现在遗留代码中,因为在JDK1.5之前,许多API类(如Collections类)不是泛型的。当使用原生类型时,得到了是一个预泛型行为—Box类得到Object对象。
为了向后兼容,允许将参数化类型赋值给其原生类型**:

  1. Box<String> stringBox = new Box<>();
  2. Box rawBox = stringBox; // OK

但是如果将一个原生类型赋值给参数化类型的话,会产生一个警告:

  1. Box rawBox = new Box(); // rawBox is a raw type of Box<T>
  2. Box<Integer> intBox = rawBox; // warning: unchecked conversion

如果使用一个原生类型去触发一个定义在其泛型类中的泛型方法,也会出现一个警告:

  1. Box<String> stringBox = new Box<>();
  2. Box rawBox = stringBox;
  3. rawBox.set(8); // warning: unchecked invocation to set(T)

未检查错误信息(Unchecked Error Message)
**
如前所述,在将遗留代码与泛型代码混合时,可能会遇到类似于以下内容的警告消息:

  1. Note: Example.java uses unchecked or unsafe operations.
  2. Note: Recompile with -Xlint:unchecked for details.

unchecked表示编译器没有足够的类型信息去保证类型安全。

泛型方法(Generic Methods)

泛型方法是引入自己类型参数的方法。类型参数的作用域仅限于声明它的方法。静态和非静态方法都可以声明为泛型方法,泛型类构造函数也可以。

泛型方法的语法**包括尖括号内的类型参数列表,它出现在方法的返回类型之前。对于静态泛型方法,类型参数部分必须出现在方法的返回类型之前。**例如:

  1. // 参数列表必须出现在返回值之前
  2. public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
  3. return p1.getKey().equals(p2.getKey()) && p1.getValue().equals(p2.getValue());
  4. }

有界类型参数(Bounded Type Parameters)

有时候需要限制可在参数化类型中用作类型参数的类型。例如,对数字进行操作的方法可能只希望接受数字或其子类的实例。这就是有界类型参数的用途。

若要声明有界类型参数,请列出类型参数的名称,后跟extends关键字,再后跟其上限(在本例中为Number)。

注意,这里的extends在一般意义上是指Java类中的extendsimplements
**

  1. public <U extends Number> void inspect(U u){
  2. System.out.println("T: " + t.getClass().getName());
  3. System.out.println("U: " + u.getClass().getName());
  4. }

多边界(Multiple Bounds)
泛型参数也可以存在多个边界:

  1. Class A { /* ... */ }
  2. interface B { /* ... */ }
  3. interface C { /* ... */ }
  4. class D <T extends A & B & C> { /* ... */ }

注意:如果其中一个边界是类,那么就需要排在泛型参数列表的第一位,否则将导致异常。

  1. class D <T extends B & A & C> { /* ... */ } // compile-time error

A是一个class,必须排在第一个位置。

泛型、继承与子类型(Generic、Inheritance、andSubtypes)

将一种类型的对象赋值给另一种类型的对象是可以的,前提是两个对象之间是类型兼容的。例如,可以将一个Integer类型的对象赋值给Object类型的对象,因为Integer是Object的子类。

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

在面向对象的术语中,称之为” is a” 的关系。因为Integer是一种Object,所以是可以被赋值的。但是Integer同样也是Number类型,故而下面的代码也是合法的:

  1. public void someMethod(Number n) { /* ... */ }
  2. someMethod(new Integer(10)); // OK
  3. someMethod(new Double(10.1)); // OK

这种关系应用到泛型中也是可行的:

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

但是,对于下面这个方法来说,我们可以传递什么样的参数呢?

  1. public void boxTest(Box<Number> n) { /* ... */ }

该方法接收一个类型为Box的类型作为参数,但是,如果我们将Box作为参数传递进去就会报错了,因为Box不是Box的子类

使用泛型时,这个知识点是非常迷惑人的,但也确实是很重要的。
image.pngimage.pngimage.png捕获.PNG
捕获.PNG
注意:假设存在两个类型A和B,尽管A/B之间可能存在关联,但是Class和Class是不存在任何关系的。

泛型类的子类型(Generic Classes and Subtyping)

开发者可以通过继承或实现去子类化一个泛型类。两者之间的类型参数关系是通过extendsimplements来决定的。例如:ArrayList实现了List,List继承了Collection,所以 ArrayList是List的一个子类, List是Collection的子类。只要不改变类型参数,类型之间的子类型关系就会保持不变。
捕获.PNG
注意:父类子类的关系都是通过extends或implements关键字来实现的,Box和Box两者是同一个类,类型擦除之后,都是Box类型。
**

通配符(Wildcards)

在泛型中使用的?称之为通配符,表示一个未知的类型。通配符可以在多种情况下使用:作为参数、字段或局部变量的类型;有时也可作为返回类型。但是,通配符从不用作泛型方法调用、泛型类实例创建或超类型的类型参数。

上界通配符

格式如下:

  1. extends Any

真实参数只能是Any或其子类。其元素可以直接当做Any类型类访问。

  1. public static void process(List<? extends Any> list) {
  2. for (Any elem : list) {
  3. // ...
  4. }
  5. }

无界通配符

无界通配符类型直接使用了通配符符号。例如List<?>.这调用了一个位置类型的集合。无界通配符使用的场景如下:

  1. 正在编码的method可以使用Object类中提供的方法
  2. 当代码在泛型类中使用不依赖于类型参数的方法时。例如,List.size或者List.clear. 事实上,Class<?>之所以经常使用,是因为Class中的大多数方法都不依赖于T。

思考下面的printList方法:

  1. public static void printList(List<Object> list) {
  2. for (Object elem : list)
  3. System.out.println(elem + " ");
  4. System.out.println();
  5. }

方法的目标是打印一个任意类型的集合,但是现在的实现却打印的是Object实例。这个方法不能打印List,List等,因为他们都不是List的子类,为了得到一个通用的printList方法,可以使用List<?>:

  1. public static void printList(List<?> list) {
  2. for (Object elem: list)
  3. System.out.print(elem + " ");
  4. System.out.println();
  5. }

注意:对于任意具体类型A来说,List都是List<?>的子类。

下界通配符(Lower Bounded Wildcards)

格式如下:

  1. <? super A>

当你需要一个方法,方法参数为Integer或其父类的时候,就可以使用下界通配符了:

  1. public static void addNumbers(List<? super Integer> list) {
  2. for (int i = 1; i <= 10; i++) {
  3. list.add(i);
  4. }
  5. }

类型擦除

泛型被引入到Java语言中,以在编译时提供更严格的类型检查,并支持泛型编程。为了实现泛型,Java编译器将类型擦除应用于:

  • 用它们的边界或Object(如果泛型是无界的)来替换所有类型参数。因此,生成的字节码只包含普通类、接口和方法。
  • 必要时插入类型强制转换以保持类型安全。
  • 生成桥接方法以在扩展泛型类型中保留多态性。

类型擦除确保没有为参数化类型创建新类;因此,泛型不会产生运行时开销。
**

类型擦除和桥接方法的影响

有时,类型擦除会造成没有预料到的情况。下面的示例显示了如何发生这种情况。这个例子(在Bridge Methods中描述)展示了编译器有时如何创建一个合成方法,称为Bridge方法,作为类型擦除过程的一部分。

  1. public class Node<T> {
  2. public T data;
  3. public Node(T data) { this.data = data; }
  4. public void setData(T data) {
  5. System.out.println("Node.setData");
  6. this.data = data;
  7. }
  8. }
  9. public class MyNode extends Node<Integer> {
  10. public MyNode(Integer data) { super(data); }
  11. public void setData(Integer data) {
  12. System.out.println("MyNode.setData");
  13. super.setData(data);
  14. }
  15. }

思考一下以下代码:

  1. MyNode mn = new MyNode(5);
  2. Node n = mn; // A raw type - compiler throws an unchecked warning
  3. n.setData("Hello");
  4. Integer x = mn.data; // Causes a ClassCastException to be thrown.

在类型擦除之后,这部分代码变为:

  1. MyNode mn = new MyNode(5);
  2. Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warning
  3. n.setData("Hello");
  4. Integer x = (String)mn.data; // Causes a ClassCastException to be thrown

这段代码将会按照以下步骤执行:

  1. n.setData("Hello");会导致MyNode的setData(Object)方法被执行;
  2. setData(Object)方法体中,引用n对应的对象的数据域被赋值为String类型;
  3. 相同对象(mn)的数据域同样能够被访问到且期望类型为Integer(因为mn是一个MyNode对象,MyNode是一个Node
  4. 尝试着将String类型赋值给Integer类型,触发ClassCastException异常

    桥接方法

    当编译一个继承了参数化类或实现了参数化接口的类或接口时,编译器可能需要创建一个称为桥方法的合成方法,作为类型擦除过程的一部分。通常开发者不需要关心桥接方法的,但是这样一个方法出现在堆栈跟踪中,开发者可能会感到困惑。

在类型擦除之后,Node和MyNode将变成:

  1. public class Node {
  2. public Object data;
  3. public Node(Object data) { this.data = data; }
  4. public void setData(Object data) {
  5. System.out.println("Node.setData");
  6. this.data = data;
  7. }
  8. }
  9. public class MyNode extends Node {
  10. public MyNode(Integer data) { super(data); }
  11. public void setData(Integer data) {
  12. System.out.println("MyNode.setData");
  13. super.setData(data);
  14. }
  15. }

在类型擦除之后,方法签名不再匹配。Node方法变成setData(Object data),而MyNode方法变成setData(Integer data),因此,MyNode 的setData方法不是重写的Node setData方法。

为了解决这个问题并在类型删除后保留泛型类型的多态性,Java编译器生成一个桥方法来确保子类型按预期工作。对于MyNode类,编译器为setData生成以下桥方法:

  1. class MyNode extends Node {
  2. // Bridge method generated by the compiler
  3. //
  4. public void setData(Object data) {
  5. setData((Integer) data);
  6. }
  7. public void setData(Integer data) {
  8. System.out.println("MyNode.setData");
  9. super.setData(data);
  10. }
  11. // ...
  12. }

MyNode生成一个桥接方法setData(Object data) ,该方法签名与Node setData()一样,在类型擦除之后,代理原来的setData()方法。

不可恢复类型(Non-Reifiable Types)

可恢复类型是指其类型信息在运行时完全可用的类型。这包括基本类型、非泛型类型、原生类型和无界通配符的调用。
不可恢复类型是指在编译时通过类型擦除删除信息的类型—对未定义为无界通配符的泛型类型的调用。不可恢复类型在运行时没有其所有信息可用。不可修改类型的例子有List和List;JVM在运行时无法区分这些类型。如泛型限制中所示,在某些情况下,不可修改类型不能使用:例如,在instanceof表达式中,或者作为数组中的元素。