深入理解 Java 泛型

[TOC]

深入理解 Java 泛型

概述

泛型的本质是参数化类型,通常用于输入参数、存储类型不确定的场景。相比于直接使用 Object 的好处是:编译期强类型检查、无需进行显式类型转换

类型擦除

Java 中的泛型是在编译器这个层次实现的,在生成的Java字节代码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除 type erasure。

public class Test {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        List<Integer> intList = new ArrayList<>();
        System.out.println(strList.getClass().getName());
        System.out.println(intList.getClass().getName());
    }
}

上面这一段代码,运行后输出如下,可知在运行时获取的类型信息是不带具体类型的:

java.util.ArrayList
java.util.ArrayList

类型擦除也是 Java 的泛型实现方式与 C++ 模板机制实现方式之间的重要区别。这就导致:

泛型类并没有自己独有的Class类对象,只有List.class。
运行时无法获得泛型的真实类型信息。

比如在 反序列化 Json 串至 List 字符串时,需要这么做:

public class Test {
    public static final ObjectMapper mapper = new ObjectMapper();
    public static void main(String[] args) throws IOException {
        JavaType javaType = getCollectionType(ArrayList.class, String.class);
        List<String> lst = mapper.readValue("[\"hello\",\"world\"]", javaType);
        System.out.println(lst);
    }
    // 获取泛型的Collection Type
    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
        return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }
}

Debug 发现 getCollectionType 方法输出的是 CollectionType 对象,里面存储了元素类型 _elementType。这就相当于把 List 的元素类型 String.class 作为参数,提供给了 Jackson 去反序列化。而下面的做法会编译失败:

public class Test {
    public static final ObjectMapper mapper = new ObjectMapper();
    public static void main(String[] args) throws IOException {
        List<String> lst = mapper.readValue("[\"hello\",\"world\"]", List<String>.class); // 编译错误
        System.out.println(lst);
    }
}

泛型不是协变的

在 Java 语言中,数组是协变的,也就是说,如果 Integer 扩展了 Number,那么不仅 Integer 是 Number,而且 Integer[] 也是 Number[],在要求 Number[] 的地方完全可以传递或者赋予 Integer[]。(更正式地说,如果 Number是 Integer 的超类型,那么 Number[] 也是 Integer[]的超类型)。您也许认为这一原理同样适用于泛型类型 —— List< Number> 是 List< Integer> 的超类型,那么可以在需要 List< Number> 的地方传递 List< Integer>。不幸的是,情况并非如此。为啥呢?这么做将破坏要提供的类型安全泛型。

对于数组来说,下面的代码会有运行时错误:

public class Test {
    public static void main(String[] args) {
        String[] strArray = new String[3];
        Object[] objArray = strArray;

        objArray[0] = 1;// 运行时错误:Exception in thread "main" java.lang.ArrayStoreException: java.lang.Integer  
    }
}

而集合这么写就会有编译错误:

public class Test {
    public static void main(String[] args) {
        List<String> strList = new ArrayList<>();
        // 编译 Error:(14, 32) java: 不兼容的类型: java.util.List<java.lang.String>无法转换为java.util.List<java.lang.Object>
        List<Object> objList = strList; 
    }
}

数组能够协变而泛型不能协变的另一个后果是,不能实例化泛型类型的数组(new List< String>[3] 是不合法的),除非类型参数是一个未绑定的通配符(new List< ?>[3]是合法的)。具体可以运行下面的代码看看:

public class Test {
    public static void main(String[] args) {
        // 编译正常
        List<?>[] lsa2 = new List<?>[10];
        // 编译 Error:(14, 30) java: 创建泛型数组
        List<String>[] lsa = new List<String>[10];
    }
}

构造延迟

因为运行时不能区分 List< String> 和 List< Integer>(运行时都是 List),用泛型类型参数标识类型的变量的构造就成了问题。运行时缺乏类型信息,这给泛型容器类和希望创建保护性副本的泛型类提出了难题。比如:

不能使用类型参数访问构造函数

您不能使用类型参数访问构造函数,因为在编译的时候还不知道要构造什么类,因此也就不知道使用什么构造函数。使用泛型不能表达“T必须拥有一个拷贝构造函数(copy constructor)”(甚至一个无参数的构造函数)这类约束,因此不能使用泛型类型参数所表示的类的构造函数。

public class Test {
    public <T> void doSomething(T param) {
        T copy = new T(param);  // 编译错误:Error:(13, 22) java: 意外的类型,需要: 类,找到: 类型参数T
    }
}

不能使用 clone 方法

为什么呢?因为 clone() 在 Object 中是 protected 保护访问的,调用 clone() 必须通过将 clone() 改写为 public 公共访问的类方法来完成。但是 T 的 clone() 是否为 public 是无法确定的,因此调用其 clone 也是非法的。

public class Test {
    public <T> void doSomething(T param) {
        T copy = (T) param.clone();  // 编译 Error:(13, 27) java: clone()在java.lang.Object中访问protected
    }
}

不能创建泛型数组

不能实例化用类型参数表示的类型数组。编译器不知道 T 到底表示什么类型,因此不能实例化 T 数组。

public class Test {
    public <T> void doS() {
        T[] t = new T[5];
    }
}

那么 ArrayList 是如何存储数据的呢?请看下面的源代码,是用 Object 数组存储的,所以在获取元素时要做显示类型转换(在 elementData 方法中):

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
    transient Object[] elementData; // Object 数组存储数据
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
        }
    }    

    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }    
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }    
}

通配符 extends 和 super

在泛型不是协变中提到,在使用 List< Number> 的地方不能传递 List< Integer>,那么有没有办法能让他两兼容使用呢?答案是:有,可以使用通配符。

泛型中 ? 可以用来做通配符,单纯 ? 匹配任意类型。< ? extends T > 表示类型的上界是 T,参数化类型可能是 T 或 T 的子类:

public class Test {
    static class Food {}
    static class Fruit extends Food {}
    static class Apple extends Fruit {}

    public static void main(String[] args) throws IOException {
        List<? extends Fruit> fruits = new ArrayList<>();
        fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile error
        fruits.add(new Apple());    // compile error

        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile success
        fruits = new ArrayList<Food>(); // compile error
        fruits = new ArrayList<? extends Fruit>(); // compile error: 通配符类型无法实例化  

        Fruit object = fruits.get(0);    // compile success
    }
}

从上面代码中可以看出来,赋值是参数化类型为 Fruit 和其子类的集合都可以成功,通配符类型无法实例化。为啥上面代码中的 add 全部编译失败了呢?因为 fruits 集合并不知道实际类型是 Fruit、Apple 还是 Food,所以无法对其赋值。

除了 extends 还有一个通配符 super,< ? super T > 表示类型的下界是 T,参数化类型可以是 T 或 T 的超类:

public class Test {
    static class Food {}
    static class Fruit extends Food {}
    static class Apple extends Fruit {}

    public static void main(String[] args) throws IOException {
        List<? super Fruit> fruits = new ArrayList<>();
        fruits.add(new Food());     // compile error
        fruits.add(new Fruit());    // compile success
        fruits.add(new Apple());    // compile success

        fruits = new ArrayList<Fruit>(); // compile success
        fruits = new ArrayList<Apple>(); // compile error
        fruits = new ArrayList<Food>(); // compile success
        fruits = new ArrayList<? super Fruit>(); // compile error: 通配符类型无法实例化      

        Fruit object = fruits.get(0); // compile error
    }
}

看上面代码可知,super 通配符类型同样不能实例化,Fruit 和其超类的集合均可赋值。这里 add 时 Fruit 及其子类均可成功,为啥呢?因为已知 fruits 的参数化类型必定是 Fruit 或其超类 T,那么 Fruit 及其子类肯定可以赋值给 T。

归根到底,还是“子类对象可以赋值给超类引用,而反过来不行”这一规则导致 extends 和 super 通配符在 add 操作上表现如此的不同。同样地,也导致 super 限定的 fruits 中 get 到的元素不能赋值给 Fruit 引用,而 extends 则可以。

总结一下就是:

  1. extends 可用于的返回类型限定,不能用于参数类型限定。
  2. super 可用于参数类型限定,不能用于返回类型限定。
  3. 带有 super 超类型限定的通配符可以向泛型对易用写入,带有 extends 子类型限定的通配符可以向泛型对象读取。

运行时泛型参数类型获取

虽然 Java 的泛型在编译期间有类型擦除,但是如果真的需要在运行时知道泛型参数的类型,应该如何做呢?

额外保存参数类型

在上面“类型擦除”中提到了 Jackson 反序列化泛型类型,将参数类型信息显式保存下来。

public class TestJackson {
    public static final ObjectMapper mapper = new ObjectMapper();
    public static void main(String[] args) throws IOException {
        JavaType javaType = getCollectionType(ArrayList.class, String.class);
        List<String> lst = mapper.readValue("[\"hello\",\"world\"]", javaType);
        System.out.println(lst);
    }
    // 获取泛型的Collection Type
    public static JavaType getCollectionType(Class<?> collectionClass, Class<?>... elementClasses) {
        return mapper.getTypeFactory().constructParametricType(collectionClass, elementClasses);
    }
}

经过 Debug 发现,getCollectionType 返回的对象实际类型是 CollectionType:


Debug JavaType 信息

CollectionType 和 JavaType 之间的继承关系,可以看下面的代码:

public final class CollectionType extends CollectionLikeType {
}

public class CollectionLikeType extends TypeBase {
    protected final JavaType _elementType;  
}   

public abstract class TypeBase extends JavaType implements JsonSerializable {
    protected final JavaType _superClass;
    protected final JavaType[] _superInterfaces;
    protected final TypeBindings _bindings;
}    

注解处理器

我们可以使用注解处理器,在编译期间获取泛型真实类型,并保存到类文件中,详见 Java 注解:注解处理器获取泛型真实类型

这个方法的本质也是“额外保存参数类型”,只不过方法不同罢了。

signature 属性

Java泛型的擦除并不是对所有使用泛型的地方都会擦除的,部分地方会保留泛型信息。比如 java.lang.reflect.Field 类中有一个 signature 属性保存了泛型的参数类型信息,通过 Field 的 getGenericType 方法即可得到。当然,这种方法仅限于类中的 属性,对于方法中的局部变量无能为力。

public final class Field extends AccessibleObject implements Member {
    private transient String    signature;
    private String getGenericSignature() {return signature;}
    public Type getGenericType() {
        if (getGenericSignature() != null)
            return getGenericInfo().getGenericType();
        else
            return getType();
    }
}

运行时能够获取泛型参数类型,根源在于字节码中还是包含了这些信息的,对于下面这样一个类:

public class Pojo {
    private String str;
    private List<Integer> intList;
    private int i;
}

使用 javac Pojo.java 命令编译之后,使用 javap -verbose Pojo.class 命令查看其字节码信息,可以看到常量池中,紧跟 intList 属性存储的就是其 Signature。

Constant pool:
   #1 = Methodref          #3.#21         // java/lang/Object."<init>":()V
   #2 = Class              #22            // Pojo
   #3 = Class              #23            // java/lang/Object
   #4 = Utf8               str
   #5 = Utf8               Ljava/lang/String;
   #6 = Utf8               intList
   #7 = Utf8               Ljava/util/List;
   #8 = Utf8               Signature
   #9 = Utf8               Ljava/util/List<Ljava/lang/Integer;>;
  #10 = Utf8               i
  #11 = Utf8               I

Field 可以获取到泛型参数信息,类似地 Class 也是可以的。下面直接上代码看如何获取吧。

示例: Field

public class Pojo {
    private List<Integer> intList;
}
public class Test {
    public static void main(String[] args) throws NoSuchFieldException {
        Field intListField = Pojo.class.getDeclaredField("intList");
        Type genericType = intListField.getGenericType();
        Class<?> parameterType = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
        System.out.println(parameterType);
    }
}

执行以后,输出:

class java.lang.Integer

示例:Class

public abstract class AbsClass<T> {
    protected final Type _type;
    public AbsClass() {
        Type superClass = getClass().getGenericSuperclass();
        _type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }
    public Type getParameterizeType() {
        return _type;
    }
}
public class ParaClass extends AbsClass<Long> {
}
public class Test {
    public static void main(String[] args) throws NoSuchFieldException {
        ParaClass paraClass = new ParaClass();
        System.out.println(paraClass.getParameterizeType());
    }
}

执行以后,输出:

class java.lang.Long

这里 ParaClass 继承的是 AbsClass< Long>,而非 AbsClass< T>。于是,对 ParaClass.class 调用 getGenericSuperclass(),就可以进一步获取到 T 所绑定的 Long 类型。

有木有发现,这两个示例的共同点是,都用到了 ParameterizedType.getActualTypeArguments()[0] 这一句,因为泛型的参数类型也就是存在了这里。

参考文献

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

推荐阅读更多精彩内容

  • 简介 泛型的意思就是参数化类型,通过使用参数化类型创建的接口、类、方法,可以指定所操作的数据类型。比如:可以使用参...
    零度沸腾_yjz阅读 3,306评论 1 15
  • 本文大量参考Thinking in java(解析,填充)。 定义:多态算是一种泛化机制,解决了一部分可以应用于多...
    谷歌清洁工阅读 456评论 0 2
  • 接着上封信的内容,谈到巴菲特的投资理念。关于价值投资,就是要寻找有发展前景的公司。不过在读了邓普顿的逆向投资后,我...
    鹿鹿无畏阅读 560评论 0 49
  • 与你交谈,谈何容易!与你相约,面孔给了谁? 我们的见面与交谈怎会变的如此陌生而熟悉,叫人惊叹! 时光匆匆,30岁,...
    关于说happy阅读 317评论 0 0