Java泛型

356 阅读14分钟

0 前言

本文主要是对参考资料的一些整理,以及一些个人见解,难免会有理解错误的地方,欢迎大家指正。

1 概述

Java语言于1995年发布第一个版本,自1991年Sun(1982-2009,被Oracle收购)成立Green项目进行开发以来,花费近四年时间。Java语言主要脱胎于C++语言(诞生于贝尔实验室并于1983年开始发行),在发展过程中与众多面向对象语言(Object-Oriented Language)相互影响,尤其是C#语言(Microsoft于2000年开始发行),泛型是Java1.5发布的一个重要feature。

2 背景

简化代码,增强安全性

举个例子,在日常开发中,接口响应的结果一般包含code,msgdata三个部分。在引入泛型之前,可以写成下面这种形式:

    public class Response {
        private int code;
        private String msg;
        private Object data;
        // 省略Getter、Setter
    }

我们知道不同的接口会对应不同类型的data,那么代码中必然充斥着各种类型转换:

    Respnse rsp = new Response();
    // 模拟传值
    rsp.setData(new Account());
    Account account = (Account)rsp.getData(); 

这会造成什么后果呢?在实现类型转换的过程中,可能出现的转型异常迫使我们每次类型转换前都要进行类型判断,无形中增加了相当多的冗余代码。

3 泛型类和泛型方法

泛型(generics)并不是Java独有的,C++,C#等语言也都支持。

3.1 泛型类

让我们将Response类通过泛型来实现

    public class Response<T> {
        private int code;
        private String msg;
        private T data;
        public T getData() {
            return data;
        }
        public void setData(T data) {
            this.data = data;
        }
        ...
    }

只需在类名尖括号处声明一个类型参数T(T是类型标志,可以是其他字母),当给类传入具体类型如Integer时,类中所有的T都相当于替换为Integer。 具体使用如下:

    // 增加泛型声明
    Respnse<Account> rsp = new Response<>();
    // 模拟传值
    rsp.setData(new Account());
    // !!! rsp.setData(new Order())无法编译通过,因为它和指定类型Account不一致
    // 此处无需强制转换
    Account account = rsp.getData();`

可以看到我们调用getData()时无需转型,直接就得到了Account对象,看来Java编译器默默地替我们做了一些微小的工作。

3.2 泛型方法

除了泛型类,还有泛型接口,它的用法跟泛型类相差无几,都是要在名称的尖括号处声明泛型参数。

public interface Generator<T>{
        T next();
}

Java可以让某个方法实现泛型,这种情况只需在该方法签名上声明类型参数即可,无需在创建类时声明泛型参数。

public class GenericMethod {
    public <K,V> void f(K k,V v) {
        System.out.println(k.getClass().getSimpleName());
        System.out.println(v.getClass().getSimpleName());
    }
    public static void main(String[] args) {
        GenericMethod gm = new GenericMethod();
        gm.f(new Integer(0),new String("generic"));
    }
}

打印结果

Integer
String

4 泛型的本质

只要提到泛型,那么肯定会带出类型擦除的问题。类型擦除究竟是怎么回事呢,我们通过几个实验来讨论一下。

4.1 泛型的运行时类型

        Response<Account> rsp1  = new Response<>();
        Response<Order> rsp2  = new Response<>();

        System.out.println(rsp1.getClass().getSimpleName());
        System.out.println(rsp2.getClass().getSimpleName());

打印结果

Response
Response

看起来类型参数 AccountOrder 在运行时并未体现出现。这表明JVM不会持有泛型参数的信息。

4.2 泛型是如何实现的

        Response<Account> rsp = new Response<>();
        rsp.setData(new Account());
        Account account = rsp.getData();

对上述代码进行反编译,结果如下

        Response<Account> rsp = new Response();
        rsp.setData(new Account());
        // class文件新增转型代码
        Account account = (Account)rsp.getData();

原来是编译器自动为字节码文件新增了转型代码。所以泛型内部实现仍未摆脱转型的工作。既然还是存在转型, 那么我们就好奇rsp.getData()的实际类型是什么呢?下面查看一下类型参数的运行时类型。

        Response<Account> rsp  = new Response<>();
        rsp.setData(new Account());
        Field[] fields = rsp.getClass().getDeclaredFields();
        for (Field f:fields) {
            System.out.println(String.format("Field name [%s],type is [%s]",f.getName(),f.getType().getName()));
        }

打印结果

Field name [code],type is [int]
Field name [msg],type is [java.lang.String]
Field name [data],type is [java.lang.Object]

可以看到 Response#data 的类型在运行时为 Object,这跟未引入泛型前的思路一致。那么setData(T data)中的参数呢?

        Method[] methods = rsp.getClass().getDeclaredMethods();
        for (Method m : methods) {
            System.out.println(m);
        }

打印结果

public java.lang.Object generics.Response.getData()
public void generics.Response.setData(java.lang.Object)

可以证明泛型类中的泛型参数T都将替换为 Object 类型,只有我们需要得到T的引用时,编译器才会为我们自动插入转型代码。这也就是所谓的类型擦除

4.3 与其他语言泛型实现的比较

  • C++,对于每种类型都会在编译前生成一份代码,例如Response<Account>,Response<Order>会分别生成类似Response_Account,Response_Order的代码,这称为编译时多态技术(还有运行时多态技术)。它的特点是如果存在100种不同类型的Response,就会生成100份代码。
  • Java,通过类型擦除实现。所谓的类型检查就是在边界处(对象进入和离开的地方),检查类型是否符合某种约束。包括以下几个条件:
    1. 赋值语句的左右两边类型必须兼容。
    2. 方法调用的实参和形参必须兼容。
    3. return表达式的类型和方法定义的返回值必须兼容。
    4. 以及多态类型检查,向上转型可以直接通过,但向下转型必须强制转换(前提是有继承关系)。

Java泛型的自动转型是通过编译器插入checkcast指令完成的。类型擦除导致所有的类型参数都以Object存在,在运行时根本就无法确定这个对象到底是什么类型,例如Response<Account>,Response<Order>都会被擦除为和Response<Object>等价。所以,你不能把T真正的当作一个类型来使用,new T()这样的语句是不会编译通过的。注意,这里的‘无法确定’说的是对象的引用,对象的类型永远是确定的,对于一个Account对象,它在堆内存中的类型是不会变的,可以通过RTTI获取到它的真实类型。

  • C#,在编译时将泛型编译成元数据,在运行时根据需要生成相应类型的特化代码。既不会像c++那样生成多份代码,又不会像java那样,丢失了泛型的类型。基本做到了两全其美。
// 增加上界
public class Response<T extends Account> {
    private int code;
    private String msg;
    private T data;

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

}

Field name [code],type is [int]
Field name [msg],type is [java.lang.String]
Field name [data],type is [generics.Account]
public generics.Account generics.Response.getData()
public void generics.Response.setData(generics.Account)

我们泛型参数指定了上限 Account,那么泛型擦除会自动将参数替换为对应的上限类型。未指定上限时直接替换为超类 Object。对于 ?super Account,上限是 Object

5 泛型和数组

在讲通配符之前,我们先看一个多态的例子,了解一下Java是如何自动转型的。

5.1 多态

    class A {
        public String show(D obj) {
            return ("A and D");
        }

        public String show(A obj) {
            return ("A and A");
        }
    }
    class B extends A {
        public String show(A obj) {
            return ("B and A");
        }

        public String show(B obj) {
            return ("B and B");
        }
    }
    class C extends B {}
    class D extends B {}
        A a1 = new A();
        A a2 = new B();
        B b = new B();
        C c = new C();
        D d = new D();
        System.out.println(a1.show(b));   // A and A
        System.out.println(a1.show(c));   // A and A
        System.out.println(a1.show(d));   // A and D
        System.out.println(a2.show(b));   // B and A
        System.out.println(a2.show(c));   // B and A
        System.out.println(a2.show(d));   // A and D
        System.out.println(b.show(b));    // B and B
        System.out.println(b.show(c));    // B and B
        System.out.println(b.show(d));    // A and D

对于System.out.println(a2.show(b)): a2 是 A 的一个引用,只能调用 A 中声明过的方法。 但 A 中不存在一个参数是 B 类型的方法,将 b 向上转型得到 A,A 中存在一个show( A obj)方法,看来应该输出 "A and A"。 但这是错的,因为 a2 实际上是指向 B 的,B 覆写了这个方法,那么最终调用的是B#show(A obj)。最终输出 "B and A"。

对于System.out.println(b.show(d)): B 中不存在一个show(D obj)方法,但B 的父类存在这个方法 所以最终输出 "A and D" 。

对于 a2.show(a2):a2被作为参数传递时,它的类型为 A。所以 a2.show(a2) 会调用 A#show(A obj) 方法,又因为a2的实际类型为B并且B覆写了这个方法。最终输出 "B and A"。

5.2 数组与协变性

数组作为最为重要的一种数据结构,Java底层众多实现都要依赖数组。

    public class Fruit {
    }
    public class Apple extends Fruit {
    }
    public class Orange extends Fruit {
    }

对于以上继承关系的类,我们看如下的代码

    Fruit[] fruits = new Apple[10];        // 编译通过,运行通过。
    fruits[0] = new Fruit();        // 编译通过,运行抛异常【java.lang.ArrayStoreException】
    fruits[0] = new Orange();        // 编译通过,运行抛异常【java.lang.ArrayStoreException】

对第一行代码而言,FruitApple存在继承关系不代表Fruit[]Apple[]也存在同样的关系,但Java允许这么做,即类的继承关系延伸到了数组,这就是数组的协变性。

第二行代码运行时才抛异常,表明数组会记住内部元素的具体类型,并且在运行时做检查,这里数组的具体类型时Apple,但却试图存储Orange类型,所以一定会报错。

为什么号称强类型的Java要引用这种不太安全的设计呢,James Gosling回答说,他们希望有一个处理数组的通用方法,这个方法签名能够接收所有的数组元素。比如

public static boolean equals(Object[] a, Object[] b) {
             ...
}

引入数组协变后,这个目的也就达到了。后来Java引入泛型,方法可以传入类型参数,这个特性也就没有意义,考虑到兼容问题这个特性依然保留着。

5.3 泛型与不变性

与数组的协变性不同,泛型是不变的,也就是说下面的代码编译报错

        List<Apple> appleList = new ArrayList<Apple>();
        List<Fruit> fruitList = appleList;        // 错误: 不兼容的类型: List<Apple>无法转换为List<Fruit>

为什么要这么设计呢?首先我们知道Java泛型是为避免ClassCastException异常的,如果我们让泛型支持协变,那么看下面的代码

        fruitList.add(new Orange());        // Orange作为Fruit的子类型,ok
        Apple apple = appleList.get(0);        // 取出的对象实际类型是Orange,泛型却试图转型为Apple,一定会抛ClassCastException异常

综上,为了保证泛型的正确转型,泛型是不变的。

5.4 泛型数组

首先明确一点,Java是不允许直接创建泛型数组的。下面的代码会编译失败

        List<Fruit>[] fruitList= new ArrayList<Fruit>[10];        // idea直接报错,Error java:创建泛型数组

Java为什么要这么约束呢,这一个Java泛型设计点原则有关

如果一段代码在编译时系统没有产生“[unchecked]未经检查的转换”警告,则程序在运行时不会引发ClassCastException异常。 换句话说,直接创建泛型数组是有可能抛出未经检查的异常。那么什么情况下会发生这种事呢,看下面代码

        List<String>[] stringListArray = new ArrayList<String>[10];        // 假设可以创建泛型数组
        Object[] objectArray = stringListArray;        // 数组协变,Object[]可接受任何类型的数组
        List<Integer> intList = Arrays.asList(42);
        objectArray[0] = intList;        // 任何对象都可赋给Object
        String s = stringListArray[0].get(0);

分析最后一行代码String s = stringListArray[0].get(0);

stringListArray[0]是一个List对象,List取出元素前,编译器会通过checkcast指令进行类型检查,如果没有完成类型检查,则会报出类似ClassCastException。

这里想得到String类型,但它的实际类型却是Integer,抛出ClassCastException,违背Java泛型设计原则。

那么问题出在哪儿呢?我们的泛型约束呢?为何形同虚设。最关键的地方是第二行代码,数组协变使泛型数组摆脱了严格的类型检查,这个漏洞可以允许你添加任何对象,泛型变得毫无意义,而数组协变特性作为历史遗留问题暂时不可能移除,所以Java决定不允许创建泛型数组是合理的。

5.5 创建泛型数组

如果我们确实需要创建泛型数组,是可以实现的。

  • 泛型类数组
        public static class Fruit<T> {}
        Fruit<Integer>[] fruitArray;        // 声明
        //fruitArray = (Fruit<Integer>[]) new Object[10];        // ClassCastException,[Ljava.lang.Object; cannot be cast to [LFruit;
        fruitArray = (Fruit<Integer>[]) new Fruit[10];        // 关键代码,创建一个原生类型的数组并转型。
        System.out.println(fruitArray.getClass().getSimpleName());        // 打印:Fruit[]
        fruitArray[0] = new Fruit<>();        // 插入new Object(),new Double()均会报错
        Fruit<Integer> fruit = fruitArray[0];        // 此处也无需我们转型
  • 类型参数数组
    public static class Fruit<T> {
        private T[] categories;
        public Fruit(int size) {
            categories = (T[])new Object[size];
        }
        public void put(int index, T item) {
            categories[index] = item;
        }
        public T get(int index) { return categories[index]; }
        public T[] all() { return categories; }
        public static void main(String[] args) {
            Fruit<String>  fruit = new Fruit<>(10);
            fruit.put(0,"banana");        // ok
            System.out.println(fruit.get(0));        // ok
            String[] categories = fruit.all();        // ClassCastException:[Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
        }
    }

因为泛型的擦除机制,T[]的实际类型是Object[]。all()方法将Object[]转型为String[]当然会报错。

不是说擦除吗,这个String类型怎么还存在?因为编译器已经将它写入了字节码中,当调用all()方法时,checkcast指令检测到异常会抛ClassCastException。

为什么构造器中 (T[])new Object[size]不会抛异常呢,因为类型擦除,相当于 (Object[])new Object[size]

注意:如果Fruit该为Fruit,那么 (T[])new Object[size]相当于 (String[])new Object[size],肯定会抛ClassCastException

如果将Fruit类改造成下面的形式呢

    public static class Fruit<T> {
        private Object[] categories;
        public Fruit(int size) {
            categories = new Object[size];
        }
        public void put(int index, T item) {
            categories[index] = item;
        }
        public T get(int index) {
            return (T) categories[index];
        }
        public T[] all() {
            return (T[]) categories;
        }
    }

调用它的all()方法还是会抛ClassCastException异常,因为数组的类型还是Object[]

  • 类型标志
    public static class Fruit<T> {
        private T[] categories;
        public Fruit(Class<T> clz,int size) {
            categories = (T[]) Array.newInstance(clz,size);
        }
        public void put(int index, T item) {
            categories[index] = item;
        }
        public T get(int index) {
            return categories[index];
        }
        public T[] all() {
            return categories;
        }
        public static void main(String[] args) {
            Fruit<String> fruit = new Fruit<>(String.class,10);
            fruit.put(0, "banana");
            System.out.println(fruit.get(0));
            String[] categories = fruit.all();
        }
    }

推荐

6 泛型的有界类型

6.1 <? extends T>

List<? extends Fruit>为例,不能添加,只能取值。

不知道它的具体类型,只知道它属于Fruit的子类

  1. 如果你添加一个Orange类,错!它可以指向List<Apple>
  2. 你可能想到添加Fruit总可以吧,错!它可以指向List<Apple>,等你取出这个对象时,Fruit转型为Apple会抛ClassCastException异常的。

但是你可以取值,因为取出的所有对象都可以向上转型为Fruit。

依我来看,List<? extends Fruit>的上界类型表示的是一种能力,想要表达的是它既可以指向List<Apple>,也可以指向List<Orange>,但不会指代某一具体类型。就好比我喜欢喝饮料,可乐也行,雪碧也行,但你不能把饮料窄化成某一种具体的饮料。

6.2 <? super T>

List<? super Apple>为例,可以添加,也可以取值,但取出的都是Object。

不知道它的具体类型,只知道它属于Apple的某个父类。

  1. 一开始我以为可以添加Apple的任意父类对象,但这是错的。why,这个父类是不确定的,你添加Fruit,但它可以指向List<Food>
  2. 你可以添加Apple及其子类,因为不管它是什么类型,Apple类总可以向上转成这种类型。

虽然你并不清楚它的具体类型,但你可以保证它是一个Object

6.2 <?>

这玩意儿既不能读也不能取,貌似没什么作用。但它可以直观的提醒我们,这个参数的用途跟它的具体类型没有关系,比如List<?>,可能只是计算它的size。

7 结语

参考资料:

分析一下为什么JAVA不支持泛型类型的数组

java中,数组为什么要设计为协变?

java为什么要用类型擦除实现泛型?

Java 泛型,你了解类型擦除吗?

java泛型 泛型的内部原理:类型擦除以及类型擦除带来的问题

Java范型中 ? extends T 和 ? super T 的区别

协变、逆变与不变:数组、泛型、与返回类型

Java 泛型总结

java多态讲解