Java泛型知识点总结

从 Java 程序设计语言 1.0 版本发布以来,变化最大的部分就是泛型,致使Java SE 5.0 中增加泛型机制的主要原因是为了满足 1999 年制定的最早的 Java 规范需求之一 (JSR 14)。使用泛型机制编写程序代码要比那些杂乱地使用 Object 变量,然后再进行强制类型转换的代码具有更好的安全性和可读性。[1]

为什么要使用泛型

泛型程序设计(Generic programming)意味着编写的代码可以被很多不同类型的对象所重用,例如 ArrayList 类 可以存放 String 类型对象,也可以存放 Integer 类型对象

类型参数的好处

在 Java 增加泛型类之前,泛型程序设计是用继承实现的,ArrayList 类只维护一个 Object 引用的数组。

public class ArrayList { // before generic classes
    private Object[] elementData;
    ...
    public Object get(int i) {}
    public void add(Object 0) {}
}

这种方法存在两个问题:

  • 当获取一个值是必须进行强制类型转换:String filename = (String) files.get(0);
  • 没有错误检查,可以向数组列表中添加任意类型的对象:files.add(new File("..."));对于这个调用,编译和运行都不会报错,然而在其他地方,如果将 get 的结果强制类型转换为 String 类型,就会产生一个错误。

泛型提供了更好的解决方案:类型参数(type parameters): ArrayList files = new ArrayList();

Java SE7 之后,构造函数中可以省略泛型类型: ArrayList files = new ArrayList<>();

编译器可以很好地使用类型参数, 当调用 get 时,不用进行强制类型转换,编译器知道返回类型为 String,而且编译器知道有类型为 String 的 add 方法,会检查插入参数的类型是否一致,这些使程序具有更好的可读性和安全性。

泛型类

一个泛型类(generic class)就是具有一个或者多个类型变量的类,参考 corejava 的示例代码,我们只关注泛型,而不会为数据存储的细节烦恼。

public class Pair {
    private T first; // use the type variable
    private T second;

    public Pair() {
        first = null;
        second = null;
    }

    public Pair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public T getSecond() {
        return second;
    }


    public void setFirst(T first) {
        this.first = first;
    }

    public void setSecond(T second) {
        this.second = second;
    }
}

类定义中的类型变量(type parameters,示例代码中为T)制定方法的返回类型以及域和局部变量的类型,泛型类可以看成普通类的工厂。

// 自定义泛型类的简单使用
public class PairTest1 {
    public static void main(String[] args) {
        String[]  words = {"Mary", "had", "little", "lamb"};
        Pair mm = ArrayAlg.minmax(words);
        System.out.println("min = " + mm.getFirst()); // min = Mary
        System.out.println("max = " + mm.getSecond()); // max = lamb
    }
}

class ArrayAlg {
    public static Pair minmax(String[] a) {
        if (a == null || a.length == 0)  return null;

        String min = a[0];
        String max = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) min = a[i];
            if (min.compareTo(a[i]) < 0) max = a[i];
        }
        return new Pair<>(min, max);
    }
}

泛型方法

泛型方法可以定义在普通类中,也可以 定义在泛型类中,其中类型变量放在修饰符后面,返回类型前面。

class ArrayAlg {
    public static  T getMiddle(T... a) {
        return a[a.length / 2];
    }
}
String middle = ArrayAlg.getMiddle(words);
String middle = ArrayAlg.getMiddle(words); // 编译器可以 自动判断出T的类型。

类型变量的限定

有时需要对类或方法对泛型参数进行限定,此时可以通过使用通配符来解决。

public class PairTest2 {
    public static void main(String[] args) {
        LocalDate[] birthdays = {
                LocalDate.of(1906, 12, 9),
                LocalDate.of(1985, 3, 5),
                LocalDate.of(1406, 2, 4),
                LocalDate.of(1996, 6, 7),
        };

        Pair mm = ArrayAlg.minmax(birthdays);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

class ArrayAlg {
    public static  Pair minmax(T[] a) {
        if (a == null || a.length == 0)  return null;

        T min = a[0];
        T max = a[0];
        for (int i = 0; i < a.length; i++) {
            if (min.compareTo(a[i]) > 0) min = a[i]; // min = 1406-02-04
            if (min.compareTo(a[i]) < 0) max = a[i]; // max = 1996-06-07
        }
        return new Pair<>(min, max);
    }
}

表示 T 应该是绑定类型的子类型, T 和绑定类型可以是类,也可以是接口,选择关键字 extends 的原因是更接近子类的概念。一个变量或通配符可以有多个限定,如 T, U extends Comparable. & Serializable在 Java 继承中,可以选择多个接口超类型,但限定中至多有一个类,如果用一个类作为限定,它必须是限定列表 中的第一个。

泛型代码和虚拟机

虚拟机没有泛型类型对象——所有对象都属于普通类。

类型擦除

无论何时定义一个泛型类型,都自动提供了一个相应的原始类型(raw type),原始类型的名字就说是删去类型参数后的反响类型名。擦除(erased)类型变量,并替换为限定类型(无限定的变量用Object),如之前在继承与多态中用的示例代码,在擦除后的原始类型如下:

public class Pair {
    private Object first;
    private Object second;

    public Pair2(Object first, Object second) {
        this.first = first;
        this.second = second;
    }

    public Object getFirst() {
        return first;
    }

    public Object getSecond() {
        return second;
    }

    public void setFirst(Object first) {
        this.first = first;
    }

    public void setSecond(Object second) {
        this.second = second;
    }
}

因为 T 是一个无限定变量,所以直接用 Object 替换。在程序中可以包含不同类型的 Pair,如 PairPair, 而擦除类型之后就变成原始的 Pair 类型。

泛型擦除的规则为:原始类型用第一个限定的类型变量来替换,如果没有给定限定就用 Object 来替换。我们看下面的例子。

public class Interval implements Serializable {
    private T lower;
    private T upper;

    public Interval(T lower, T upper) {
        if (lower.compareTo(upper) > 0) {
            this.lower = lower;
            this.upper = upper;
        } else {
            this.upper = lower;
            this.lower = upper;
        }
    }
}

在泛型擦除之后,原始类型如下:

public class Interval implements Serializable {
    private Comparable lower;
    private Comparable upper;

    public Interval(Comparable lower, Comparable upper) {
        if (lower.compareTo(upper) > 0) {
            this.lower = lower;
            this.upper = upper;
        } else {
            this.upper = lower;
            this.lower = upper;
        }
    }
}

如果写出 class Interval,那么原始类型会用 Serializable 来代替 T,而编译器在必要时要向 Comparable 插入强制类型转换。所以为了提高效率,应该将标签(tagging)接口(即没有方向的接口)放在边界列表的末尾。

翻译泛型表达式

当程序调用泛型方法时,如果擦除返回类型,编译器会插入强制类型转换,例如我们的Pair

Pair buddies = ...
Employee buddy = buddies.getFirst();

调用 getFirst 方法时编译器把这个方法调用翻译为两条虚拟机指令:

  • 对原始方法 Pair.getFirst 的调用
  • 将返回的 Object 类型强制转换为 Employee 类型

同样的情况也会出现在存取一个泛型域时,如

Employee buddy = buddies.first;

翻译泛型方法

类型擦除也会出现在泛型方法中,我们看以下泛型方法的定义:

public static  T min(T[] a)

泛型擦除后:

public static Comparable min(Comparable[] a)

我们可以看到参数 T 已经被擦除了,只留下了限定类型 Comparable,方法的擦除会带来两个巨大的问题。这里使用 oracle tutorial 中的代码来讲解[2]

// before erasure
public class Node {
    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 {
    public MyNode(Integer data) {
        super(data);
    }

    public void setData(Integer data) {
        System.out.println("MyNode.setData");
        super.setData(data);
    }
}
// after erasure
public class Node {

    public Object data;

    public Node(Object data) { this.data = data; }

    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);
    }
}

MyNode 是 Node 的子类,并且它复写了父类的 setData 方法。当我们尝试调用以下代码。

// after erasure
MyNode mn = new MyNode(1);
Node n  = mn;  
n.setData("Hello");
Integer x = mn.data; 
// before erasure
MyNode mn = new MyNode(1);
Node n  = (MyNode) mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Integer x = (String) mn.data; 

看到上述代码时,第一反应是为什么n.setData("Hello");不会报错,按照多态的原理,此时会调用MyNode.setData(Integer data) 方法,放入一个 String 为什么编译器会没报错?

先引用官方的解释

After type erasure, the method signatures do not match. The Node method becomes setData(Object) and the MyNode method becomes setData(Integer). Therefore, the MyNode setData method does not override the Node setData method.

因为存在泛型擦除,在擦除后 Node 中的方法变为 Node.setData(Object data) , 很明显 Object 与 Integer 不是同一个类型,所以 MyNode 中同时存在从 Node 中继承过来的 Node.setData(Object data)方法,n.setData("Hello"); 实际上调用的 Node.setData(Object data), 所以在编译时期能检查通过,这行代码运行时发生如下操作:

  • 调用 MyNode 类中的 setData(Object) (这个方法会被编译器自动改写为桥方法)方法(因为 MyNode 从 Node 中继承了 setData(Object)方法)
  • n 引用的对象中的 data 域 被赋值为 "Hello"
  • mn 引用与 n 同一个对象,但是这里的 data 域被期望为 Integer 类型,因为 mn 是 MyNode extends Node 的对象,此时因为桥方法中的强制类型转换而抛出ClassCastException,程序结束。

桥方法(Birdge Method)

根据多态的设计初衷,n.setData("Hello"); 应该调用 MyNode.setData(Integer data),但是最终它调用的却是 Node.setData(Object data),我们可以看出类型擦除(type erasure) 与多态(polymorphism) 之间存在冲突,为了保证多态的可用性,Java 编译器会自动生成桥方法来解决这个问题, 如 MyNode 将会变成如下代码。

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);
    }

    // ...
}

正是因为有桥方法的存在,才能保证多态功能的正常使用。

桥方法还可以用在其他地方,比如一个方法覆盖另一方法是可以指定一个 更严格的返回类型。

public class Employee implements Cloneable{
    public Employee clone() throws CloneNotSupportedException {
        Employee clone = (Employee) super.clone();
        return clone;
    }
}

这里其实 Employee 有两个 clone 方法

Employee clone();
Object clone();

此时也需要编译器合成桥方法,合成的桥方法调用了新定义的方法。

总结

  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都用他们的限定类型替换
  • 桥方法被用来合成保持多态
  • 为保持类型安全性,必要时插入强制类型转换。

约束与局限性

使用泛型时也有一些约束与局限性,大部分的约束都是由类型擦除引起的。

  • 不能用基本类型实例化类型参数:如不能 Pair,只能 Pair原因很简单,当类型擦除之后,只剩下 Object 类型的域, 而 Object 不能存储 double 的值,这样做与 Java 语言中基本类型的独立状态相一致。

  • 运行时类型查询只适用于原始类型:所有的类型查询只产生原始类型,如

    if (a instanceof Pair) // Error
    if (a instanceof Pair) // Error
    
    Pair stringPair = new Pair<>();
    Pair employeePair = new Pair<>();
    stringPair.getClass() == employeePair.getClass // true getClass方法总返回原始类型
    
  • 不能创建参数化类型的数组:Node[] node = new Node[10]; // Error,因为类型擦除后 node 的类型变成 Node[],可以把它转化为 Object[] objArray = node;,数组会记住它的元素类型,如果试图存储其他的类型,如 objArray[0] = "hello";,就会抛出 ArrayStoreException异常。

  • Varargs警告:public static void addAll(Collection coll, T... ts) 这个方法定义会抛出警告,因为其中的一个参数为可变参数,本质上是泛型数组,这就违反了上一条规则,Java SE 7后可以使用 @SafeVarargs 进行消除警告。

  • 不能实例化类型变量:

    不能使用 new T(...), new T[...], T.class这些表达式,也不能使用如下构造器:

    public Pair {
      first = new T();
      second = new T();
    }
    

    比较好的解决方法为使用构造器表达式,如

    public static  Pair makePair(Supplier constr) {
      return new Pair<>(constr.get(), constr.get());
    }
    
    // 调用
    Pair p = Pari.makePair(String::new);
    

    比较传统的方法是通过反射调用 Class.newInstance 方法来构造泛型对象

    first = T.class.newInstance(); // Error,因为存在泛型擦除, T.class会被擦除为 Object.class
    
    public static  Pair makePair(Class c1) {
      try {
        return new Pair<>(c1.newInstance(), c1.newInstance());
      } catch(Exception ex) {
        return null;
      }
    }
    
    // 调用
    Pair p = Pari.makePair(String.class);
    
  • 泛型类的静态上下文中类型变量无效:即不能在静态域或方法中引用类型变量。

  • 不能抛出和捕获泛型类的实例

  • 可以消除对受查异常的检查

  • 注意擦除后的冲突

泛型类的继承

若 Manager 是 Employee 的子类,那么 Pair 不是 Pair 的子类,这一限制主要是出于类型安全的考虑,考虑一下代码:

Pair manager = new Pair<>(cto, cfo);
Pair employee = manager;
employee.setFirst(staff); // 将普通员工与管理者放在一起明显破坏了程序设计的意图

永远可以将一个参数化类型转化为一个原始类型,例如 Pair 是原始类型的子类型,并且泛型类可以扩展或实现其他的泛型类,如 ArrayList 实现 List 接口,这意味着一个 ArrayList 可以转换为List, 但是一个 ArrayListArrayListList之间没有关系。

GenericExtendsRelation

通配符类型

public static void test(Pair test) // 表示任何泛型 Pair 类型,它的类型参数是 Employee 的子类 以及其本身。
genericExtendsRelation2
public static void test(Pair test) // 表示任何泛型 Pair 类型,它的类型参数是 Employee 的父类 以及其本身。

直观得讲,带有超类型限定的通配符可以向泛型对象写入(可以用构造器方法),带有子类型限定的通配符对象可以从泛型对象读取。[1]

Pair 无限定通配符

本文章与github上同步,欢迎来玩,提交issue。

Reference

1.Java核心技术·卷 I(原书第10版)

2.Effects of Type Erasure and Bridge Methods

你可能感兴趣的:(Java泛型知识点总结)