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<String> files = new ArrayList<String>();

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

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

泛型类

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

public class Pair<T> {
    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<String> 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<String> 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> T getMiddle(T... a) {
        return a[a.length / 2];
    }
}
String middle = ArrayAlg.<String>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<LocalDate> mm = ArrayAlg.minmax(birthdays);
        System.out.println("min = " + mm.getFirst());
        System.out.println("max = " + mm.getSecond());
    }
}

class ArrayAlg {
    public static <T extends Comparable> Pair<T> 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 extends BoundingType> 表示 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,如 Pair<String>Pair<LocalDate>, 而擦除类型之后就变成原始的 Pair 类型。

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

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

翻译泛型表达式

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

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

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

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

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

Employee buddy = buddies.first;

翻译泛型方法

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

public static <T extends Comparable> T min(T[] a)

泛型擦除后:

public static Comparable min(Comparable[] a)

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

// before erasure
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);
    }
}
// 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<Integer> 的对象,此时因为桥方法中的强制类型转换而抛出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<double>,只能 Pair<Double>原因很简单,当类型擦除之后,只剩下 Object 类型的域, 而 Object 不能存储 double 的值,这样做与 Java 语言中基本类型的独立状态相一致。

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

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

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

  • 不能实例化类型变量:

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

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

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

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

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

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

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

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

  • 注意擦除后的冲突

泛型类的继承

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

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

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

GenericExtendsRelation

通配符类型

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

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

Pair<?> 无限定通配符

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

Reference

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

2.Effects of Type Erasure and Bridge Methods

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容