关于Java泛型机制无非就这7个问题

泛型机制是我们开发中的常用技巧,也是面试常见问题
不过泛型机制这个知识点也比较繁杂又不成体系,学了容易忘
本文从几个问题出发梳理Java泛型机制知识点,如果对你有用,欢迎点赞~

本文主要包括以下内容
1.我们为什么需要泛型?
2.什么是泛型擦除及泛型擦除带来的一些问题,如retrofit怎么获得擦除后的类型,Gson怎么获得擦除后的类型?
3.什么是PECS原则

本文目录如下

1.我们为什么需要泛型?

我们为什么需要泛型,即泛型有什么用?
首先举两个例子

1.1 求和函数

实际开发中,经常有数值类型求和的需求,例如实现int类型的加法, 有时候还需要实现long类型的求和 如果还需要double类型的求和,又需要重新在重载一个输入是double类型的add方法。

public int addInt(int x,int y){
    return x+y;
}

public float addFloat(float x,float y){
    return x+y;
}
复制代码

如果没有泛型,我们需要写不少重复代码

1.2 List中添加元素

List list = new ArrayList();
list.add("mark");
list.add("OK");
list.add(100);

for (int i = 0; i < list.size(); i++) {
        String name = list.get(i); // 1
        System.out.println("name:" + name);
    }
复制代码

1.list默认是Object类型,因此可以存任意类型数据
2.但是当取出来时,我们并不知道取出元素的类型,就需要进行强制类型转换了,并且容易出错

1.3 泛型机制的优点

从上面的两个例子我们可以直观的得出泛型机制的优点
1.使用泛型可以编写模板代码来适应任意类型,减少重复代码
2.使用时不必对类型进行强制转换,方便且减少出错机会

2.泛型擦除

2.1 什么是泛型擦除?

大家都知道,Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。
Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程就是泛型擦除。

举个例子:

public class Test {

    public static void main(String[] args) {

        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");

        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(123);

        System.out.println(list1.getClass() == list2.getClass());
    }

}
复制代码

如上list1.getClass==list2.getClass返回true,说明泛型类型StringInteger都被擦除掉了,只剩下原始类型
Java的泛型也可以被称作是伪泛型

  • 真泛型:泛型中的类型是真实存在的。
  • 伪泛型:仅于编译时类型检查,在运行时擦除类型信息。

看到这里我们可以自然地引出下一个问题,为什么Java中的泛型是伪泛型,为什么要这样实现?

2.2 为什么需要泛型擦除?

泛型擦除看起来有些反直觉,有些奇怪。为什么Java不能像C#一样实现真正的泛型呢?为什么Java的泛型要用"擦除"实现
单从技术来说,Java是完全100%能实现我们所说的真泛型,而之所以选择使用泛型擦除主要是从API兼容的角度考虑的
导致Java 5引入的泛型采用擦除式实现的根本原因是兼容性上的取舍,而不是“实现不了”的问题。

举个例子,Java1.4.2都没有支持泛型,而到Java 5突然支持泛型了,要让以前编译的程序在新版本的JRE还能正常运行,就意味着以前没有的限制不能突然冒出来。
假如在没有泛型的Java里,我们有程序使用了java.util.ArrayList类,而且我们利用了它可以存异质元素的特性:

ArrayList things = new ArrayList();
things.add(Integer.valueof(42));
things.add("Hello World")
复制代码

为了这段代码在Java 5引入泛型之后还必须要继续可以运行,有两种设计思路
1.需要泛型化的类型(主要是容器(Collections)类型),以前有的就保持不变,然后平行地加一套泛型化版本的新类型;
2.直接把已有的类型泛型化,让所有需要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版。

.NET1.1 -> 2.0的时候选择了上面选项的1,而Java则选择了2。

Java设计者的角度看,这个取舍很明白。
.NET1.1 -> 2.0的时候,实际的应用代码量还很少(相对Java来说),而且整个体系都在微软的控制下,要做变更比较容易;
Java1.4.2 -> 5.0的时候,Java已经有大量程序部署在生产环境中,已经有很多应用和库程序的代码。
如果这些代码在新版本的Java中,为了使用Java的新功能(例如泛型)而必须做大量源码层修改,那么新功能的普及速度就会大受影响。

2.3 泛型擦除后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来获取类型信息的
jdkClassMethodField 类提供了一系列获取 泛型类型的相关方法。
Method为例,getGenericReturnType获取带泛型信息的返回类型 、 getGenericParameterTypes获取带泛型信息的参数类型。

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

通过javap命令 可以看到在Constant pool#5 Signature记录了泛型的类型。

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)
}
复制代码

这就是我们retrofit中能够获取泛型类型的原因

2.4 Gson解析为什么要传入内部类

Gson是我们常用的json解析库,一般是这样使用的

    // Gson 常用的情况
    public  List<String> parse(String jsonStr){
        List<String> topNews =  new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
        return topNews;
    }
复制代码

我们这里可以提出两个问题
1.Gson是怎么获取泛型类型的,也是通过Signature吗?
2.为什么Gson解析要传入匿名内部类?这看起来有些奇怪

2.4.1 那些泛型信息会被保留,哪些是真正的擦除了?

上面我们说了,声明侧泛型会被记录在Class文件的Constant pool中,使用侧泛型则不会

声明侧泛型主要指以下内容
1.泛型类,或泛型接口的声明 2.带有泛型参数的方法 3.带有泛型参数的成员变量

使用侧泛型
也就是方法的局部变量,方法调用时传入的变量。

Gson解析时传入的参数属于使用侧泛型,因此不能通过Signature解析

2.4.2 为什么Gson解析要传入匿名内部类

根据以上的总结,方法的局部变量的泛型是不会被保存的
Gson是如何获取到List<String>的泛型信息String的呢?
Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type
也就是说javaclass文件会保存继承的父类或者接口的泛型信息。

所以Gson使用了一个巧妙的方法来获取泛型类型:
1.创建一个泛型抽象类TypeToken <T> ,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类。
2.创建一个 继承自TypeToken的匿名内部类, 并实例化泛型参数TypeToken<String>
3.通过class类的public Type getGenericSuperclass()方法,获取带泛型信息的父类Type,也就是TypeToken<String>

总结:Gson利用子类会保存父类class的泛型参数信息的特点。 通过匿名内部类实现了泛型参数的传递。

3.什么是PECS原则?

3.1 PECS介绍

PECS的意思是Producer Extend Consumer Super,简单理解为如果是生产者则使用Extend,如果是消费者则使用Super,不过,这到底是啥意思呢?

PECS是从集合的角度出发的
1.如果你只是从集合中取数据,那么它是个生产者,你应该用extend
2.如果你只是往集合中加数据,那么它是个消费者,你应该用super
3.如果你往集合中既存又取,那么你不应该用extend或者super

让我们通过一个典型的例子理解一下到底什么是ProducerConsumer

public class Collections { 
  public static <T> void copy(List<? super T> dest, List<? extends T> src)   {  
      for (int i=0; i<src.size(); i++) { 
          dest.set(i, src.get(i)); 
      }
  } 
}
复制代码

上面的例子中将src中的数据复制到dest中,这里src就是生产者,它「生产」数据,dest是消费者,它「消费」数据。

3.2 为什么需要PECS

使用PECS主要是为了实现集合的多态
举个例子,现在有这样一个需求,将水果篮子中所有水果拿出来(即取出集合所有元素并进行操作)

public static void getOutFruits(List<Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}

List<Fruit> fruitBasket = new ArrayList<Fruit>();
getOutFruits(fruitBasket);//成功

List<Apple> appleBasket = new ArrayList<Apple>();
getOutFruits(appleBasket);//编译错误
复制代码

如上所示:
1.将List<Apple>传递给List<Fruit>会编译错误。
2.因为虽然FruitApple的父类,但是List<Apple>List<Fruit>之间没有继承关系
3.因为这种限制,我们不能很好的完成取出水果篮子中的所有水果需求,总不能每个类型都写一遍一样的代码吧?

使用extend可以方便地解决这个问题

/**参数使用List<? extends Fruit>**/
public static void getOutFruits(List<? extends Fruit> basket){
    for (Fruit fruit : basket) {
        System.out.println(fruit);
        //...do something other
    }
}
public static void main(String[] args) {
    List<Fruit> fruitBasket = new ArrayList<>();
    fruitBasket.add(new Fruit());
    getOutFruits(fruitBasket);

    List<Apple> appleBasket = new ArrayList<>();
    appleBasket.add(new Apple());
    getOutFruits(appleBasket);//编译正确
}
复制代码

List<? extends Fruit>,同时兼容了List<Fruit>List<Apple>,我们可以理解为List<? extends Fruit>现在是List<Fruit>List<Apple>的超类型(父类型)
通过这种方式就实现了泛型集合的多态

3.3 小结

  • List<? extends Fruit>的泛型集合中,对于元素的类型,编译器只能知道元素是继承自Fruit,具体是Fruit的哪个子类是无法知道的。 所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自Fruit,所以从这个泛型集合中取Fruit类型的元素是可以的。
  • List<? super Apple>的泛型集合中,元素的类型是Apple的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。 插入元素时可以插入AppleApple的子类,因为这个集合中的元素都是Apple的父类,子类型是可以赋值给父类型的。

有一个比较好记的口诀:
1.只读不可写时,使用List<? extends Fruit>:Producer
2.只写不可读时,使用List<? super Apple>:Consumer

总得来说,List<Fruit>List<Apple>之间没有任何继承关系。API的参数想要同时兼容2者,则只能使用PECS原则。这样做提升了API的灵活性,实现了泛型集合的多态
当然,为了提升了灵活性,自然牺牲了部分功能。鱼和熊掌不能兼得。

总结

本文梳理了Java泛型机制这个知识点,回答了如下几个问题
1.我们为什么需要泛型?
2.什么是泛型擦除?
3.为什么需要泛型擦除?
4.泛型擦除后retrofit怎么获得类型的?
5.Gson解析为什么要传入内部类
6.什么是PECS原则?
7.为什么需要PECS原则?

更多Android筑基学习知识,可以后台私信我获取pdf学习笔记!

架构师筑基基础目录

本文如果对您有所帮助,欢迎点赞,谢谢~

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

推荐阅读更多精彩内容

  • Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛...
    三好码农阅读 640评论 1 4
  • 泛型的目的 在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类...
    风月寒阅读 347评论 0 2
  • 一、泛型 泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。 什么是泛型?为什么要使...
    脆皮鸡大虾阅读 261评论 0 0
  • 1 泛型基础 1.1 什么是泛型(Generics) 官方是这样介绍的: JDK 5.0 introduces s...
    Eager01阅读 254评论 0 0
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 123,962评论 2 7