Java基础:泛型擦除

思维导图:

  • 什么是泛型擦除?
  • 泛型擦除会带来什么样的问题,如何解决?
  • 应用场景
  • PECS 原则
  • 泛型擦除后 retrofit 是怎么获取类型的

Java泛型(generics)就是参数化类型,适用于多种数据类型执行相同代码,在使用时才确定真实类型。泛型有泛型类、泛型接口、泛型方法。

泛型擦除:泛型信息只存在于代码编译阶段,在进入 JVM 之前,与泛型相关的信息会被擦除掉。

在泛型类被类型擦除的时候,之前泛型类中的类型参数部分如果没有指定上限,如 <T> 则会被转译成普通的 Object 类型,如果指定了上限如<T extends String> 则类型参数就被替换成类型上限。
会在字节码保留泛型类型,使用时也就是运行到这段字节码的时候,再将 Object 强制转换成对应类型。

需要注意的一点:
泛型不能声明在泛型类里面的静态方法和静态变量中,
因为泛型类里面的静态方法和静态变量可能比构造方法先执行,导致泛型没有实例化就调用。

在理解了泛型擦除的概念,咱们看下面的例子:

List list = new ArrayList();
List listString = new ArrayList<String>();
List listInteger = new ArrayList<Integer>();

这几段代码简单、粗暴、又带有很浓厚的熟悉感是吧。那我接下来要把一个数字 1插入到这三段不一样的代码中了。

作为读者的你可能现在已经黑人问号了????你肯定有很多疑问,这明显不一样啊,怎么可能。

public class Main {
    public static void main(String[] args) {
        List list = new ArrayList();
        List listString = new ArrayList<String>();
        List listInteger = new ArrayList<Integer>();
 
        try {
            list.getClass().getMethod("add", Object.class).invoke(list, 1);
            listString.getClass().getMethod("add", Object.class).invoke(listString, 1);
            // 给不服气的读者们的测试之处,你可以改成字符串来尝试。
            listInteger.getClass().getMethod("add", Object.class).invoke(listInteger, 1);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("list size:" + list.size());
        System.out.println("listString size:" + listString.size());
        System.out.println("listInteger size:" + listInteger.size());
    }
}
结果

不好意思,有图有真相,我就是插进去了,要是你还不信,我还真没办法了。
上述的就是泛型擦除的一种表现了,但是为了更好的理解,当然要更深入了是吧。虽然List很大,但却也不是不能看看。

两个关键点,来验证一下:

  • 数据存储类型
  • 数据获取
// 先来看看画了一个大饼的List
// 能够过很清楚的看到泛型E
public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{       
    // 第一个关键点    
    // 还没开始就出问题的存储类型
    // 难道不应该也是一个泛型E?
    transient Object[] elementData;
 
    public E get(int index) {
        rangeCheck(index);
 
        return elementData(index); // 1---->
    }
 
    // 由1直接调用的函数
    // 第二个关键点,强制转化得来的数据
    E elementData(int index) {
        return (E) elementData[index];
    }
}

我想,其实你也能够懂了,这个所谓的泛型T最后会被转化为一个 Object,最后又通过强制转化来进行一个转变。从这里我们也就能够知道为什么我们的数据从前面过来的时候,String 类型数据能够直接被 Integer 进行接收了。

带来什么样的问题?

(1) 强制类型转化

这个问题的结果我们已经在上述文章中提及到了,通过反射的方式去进行插入的时候,我们的数据就会发生错误。

如果我们在一个 List<Integer> 中在不知情的情况下插入了一个 String 类型的数值,那这种重大错误,我们该找谁去说呢。

(2)引用传递问题

上面的问题中,我们已经说过了 T 将在后期被转义成 Object,那我们对引用也进行一个转化,是否行得通呢?

List<String> listObject = new ArrayList<Object>();
List<Object> listObject = new ArrayList<String>();

如果你这样写,在我们的检查阶段,会报错。但是从逻辑意义上来说,其实你真的有错吗?

假设说我们的第一种方案是正确的,那么其实就是将一堆 Object 数据存入,然后再由上面所说的强制转化一般,转化成 String 类型,听起来完全 ok,因为在 List 中本来存储数据的方式就是 Object。但其实是会出现 ClassCastException 的问题,因为 Object 是万物的基类,但是强转是为子类向父类准备的措施。

再来假设说我们的第二种方案是正确的,这个时候,根据上方的数据 String 存入,但是有什么意义存在呢?最后都还是要成 Object 的,你还不如就直接是 Object

解决方案

其实很简单,如果看过一些公开课想来就见过这样的用法。

public class Part<T extends Parent> {
 
    private T val;
 
    public T getVal() {
        return val;
    }
 
    public void setVal(T val) {
        this.val = val;
    }
}

相比较于之前的 Part 而言,他多了 <T extends Parent> 的语句,其实这就是将基类重新规划的操作,就算被编译,虚拟机也会知道将数据转化为 Parent 而不是直接用 Object 来直接进行替代。

应用场景

该部分的思路来自于Java泛型中extends和super的区别?

上面我们说过了解决方案,使用 <T extends Parent>。其实这只是一种方案,在不同的场景下,我们需要加入不同的使用方法。另外官方也是提倡使用这样的方法的,但是我们为了避免我们上述的错误,自然需要给出一些使用场景了。

基于的其实是两种场景,一个是扩展型 super,一个是继承型 extends。下面都用一个列表来举例子。

统一继承顺序
// 承载者
class Plate<T>{
    private T item;
    public Plate(T t){item=t;}
    public void set(T t){item=t;}
    public T get(){return item;}
}
 
// Lev 1
class Food{}
 
// Lev 2
class Fruit extends Food{}
class Meat extends Food{}
 
//Lev 3
class Apple extends Fruit{}
class Banana extends Fruit{}
class Pork extends Meat{}
class Beef extends Meat{}
 
//Lev 4
class RedApple extends Apple{}
class GreenApple extends Apple{}

<T extends Parent>

继承型的用处是什么呢?

其实他期待的就是这整个列表的数据的基础都是来自我们的 Parent,这样获取的数据全部人的父类其实都是来自于我们的 Parent 了,你可以叫这个列表为 Parent 家族。所以也可以说这是一个适合频繁读取的方案。


Plate<? extends Fruit> p1=new Plate<Apple>(new Apple());
Plate<? extends Fruit> p2=new Plate<Apple>(new Beef()); // 检查不通过
 
// 修改数据不通过
p1.set(new Banana());
 
// 数据获取一切正常
// 但是他只能精确到由我们定义的Fruit
Fruit result = p1.get();

<? extends Fruit> 会使往盘子里放东西的 set( )方法失效。但取东西 get( ) 方法还有效。
原因是编译器只知道容器内是 Fruit 或者它的派生类,但具体是什么类型不知道。可能是Fruit?可能是 Apple?也可能是 Banana,RedApple,GreenApple?编译器在看到后面用 Plate 赋值以后,盘子里没有被标上有“苹果”。而是标上一个占位符:CAP#1,来表示捕获一个 FruitFruit的子类,具体是什么类不知道,代号 CAP#1。然后无论是想往里插入 Apple 或者 Meat 或者 Fruit 编译器都不知道能不能和这个 CAP#1 匹配,所以就都不允许。

<T super Parent>

扩展型的作用是什么呢?

你可以把它当成一种兼容工具,由 super 修饰,说明兼容这个类,通过这样的方式比较适用于去存放上面所说的 Parent 列表中的数据。这是一个适合频繁插入的方案。


// 填写Food的位置,级别一定要大于或等于Fruit
Plate<? super Fruit> p1=new Plate<Food>(new Apple());
// 和extends 不同可以进行存储
p1.set(new Banana());
// get方法
Banana result1 = p1.get(); // 会报错,一定要经过强制转化,因为返回的只是一个Object
Object result2 = p1.get(); // 返回一个Object数据我们已经属于快要丢失掉全部数据了,所以不适合读取

使用下界 <? super Fruit> 会使从盘子里取东西的 get( ) 方法部分失效,只能存放到 Object 对象里。set( )方法正常。
因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是 Fruit 的基类,那往里存粒度比 Fruit 小的都可以。但往外读取元素就费劲了,只有所有类的基类 Object 对象才能装下。但这样的话,元素的类型信息就全部丢失。

PECS原则

最后看一下什么是 PECS(Producer Extends Consumer Super)原则,已经很好理解了:

  • 频繁往外读取内容的,适合用上界 Extends
  • 经常往里插入的,适合用下界 Super

泛型擦除后 retrofit 是怎么获取类型的?

Retrofit 是如何传递泛型信息的?

public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

使用 jad 查看反编译后的 class 文件:

import retrofit2.Call;

public interface GitHubService
{

    public abstract Call listRepos(String s);
}

可以看到 class 文件中已经将泛型信息给擦除了,那么 Retrofit 是如何拿到 Call<List> 的类型信息的?

我们看一下 retrofit的源码:

static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    ...
    Type returnType = method.getGenericReturnType();
    ...
  }

    public Type getGenericReturnType() {
       // 根据 Signature 信息 获取 泛型类型 
      if (getGenericSignature() != null) {
        return getGenericInfo().getReturnType();
      } else { 
        return getReturnType();
      }
    }

可以看出,retrofit 是通过 getGenericReturnType 来获取类型信息的

jdkClass 、Method 、Field类提供了一系列获取 泛型类型的相关方法。

Method 为例,getGenericReturnType获取带泛型信息的返回类型 、 getGenericParameterTypes获取带泛型信息的参数类型。

问:泛型的信息不是被擦除了吗?

答:是被擦除了, 但是某些(声明侧的泛型,接下来解释) 泛型信息会被 class 文件 以 Signature 的形式 保留在 Class 文件的 Constant pool 中。

Constant pool:
   #1 = Class              #16            //  com/example/diva/leet/GitHubService
   #2 = Class              #17            //  java/lang/Object
   #3 = Utf8               listRepos
   #4 = Utf8               (Ljava/lang/String;)Lretrofit2/Call;
   #5 = Utf8               Signature
   #6 = Utf8               (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
   #7 = Utf8               RuntimeVisibleAnnotations
   #8 = Utf8               Lretrofit2/http/GET;
   #9 = Utf8               value
  #10 = Utf8               users/{user}/repos
  #11 = Utf8               RuntimeVisibleParameterAnnotations
  #12 = Utf8               Lretrofit2/http/Path;
  #13 = Utf8               user
  #14 = Utf8               SourceFile
  #15 = Utf8               GitHubService.java
  #16 = Utf8               com/example/diva/leet/GitHubService
  #17 = Utf8               java/lang/Object
{
  public abstract retrofit2.Call<java.util.List<com.example.diva.leet.Repo>> listRepos(java.lang.String);
    flags: ACC_PUBLIC, ACC_ABSTRACT
    Signature: #6                           // (Ljava/lang/String;)Lretrofit2/Call<Ljava/util/List<Lcom/example/diva/leet/Repo;>;>;
    RuntimeVisibleAnnotations:
      0: #8(#9=s#10)
    RuntimeVisibleParameterAnnotations:
      parameter 0:
        0: #12(#9=s#13)
}

参考:

面试官问我:“泛型擦除是什么,会带来什么问题?”
Android开发面试——Java泛型机制7连问

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

推荐阅读更多精彩内容