美文网首页
Java 泛型机制

Java 泛型机制

作者: Drew_MyINTYRE | 来源:发表于2022-03-29 11:43 被阅读0次

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

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

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

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

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

import retrofit2.Call;

public interface GitHubService
{
    public abstract Call listRepos(String s);
}

我们看一下 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() 来获取类型信息的,jdk 的 Class 、Method 、Field 类提供了一系列获取泛型类型的相关方法。以 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 中能够获取泛型类型的原因。

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

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

public List<String> parse(String jsonStr) {
    List<String> topNews =  new Gson().fromJson(jsonStr, new TypeToken<List<String>>() {}.getType());
    return topNews;
}

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

Gson 是如何获取到 List<String> 的泛型信息 String 的呢?

Class 类提供了一个方法 public Type getGenericSuperclass() ,可以获取到带泛型信息的父类 Type。也就是说 Java 的 class 文件会保存 父类 或者 接口 的泛型信息。

所以 Gson 使用了一个巧妙的方法来获取泛型类型:

  • 创建一个泛型抽象类 TypeToken <T>,这个抽象类不存在抽象方法,因为匿名内部类必须继承自抽象类或者接口。所以才定义为抽象类;

  • 创建一个 继承自 TypeToken 的匿名内部类, 并实例化泛型参数TypeToken<String>

  • 通过 class 类的 public Type getGenericSuperclass() 方法,获取带泛型信息的父类 Type,也就是 TypeToken<String>

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

PECS 介绍

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

PECS 是从集合的角度出发的:

  • 如果你只是从集合中取数据,那么它是个生产者,你应该用 extend

  • 如果你只是往集合中加数据,那么它是个消费者,你应该用 super

  • 如果你往集合中既存又取,那么你不应该用 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)); 
      }
  } 
}

为什么需要 PECS?

使用 PECS 主要是为了实现集合的多态。

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

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

小结

  • List<? extends Fruit> 的泛型集合中,对于元素的类型,编译器只能知道元素是继承自 Fruit,具体是 Fruit 的哪个子类是无法知道的。 所以「向一个无法知道具体类型的泛型集合中插入元素是不能通过编译的」。但是由于知道元素是继承自 Fruit,所以从这个泛型集合中取 Fruit 类型的元素是可以的。

  • 在List<? super Apple>的泛型集合中,元素的类型是 Apple 的父类,但无法知道是哪个具体的父类,因此「读取元素时无法确定以哪个父类进行读取」。 插入元素时可以插入 AppleApple 的子类,因为这个集合中的元素都是 Apple 的父类,子类型是可以赋值给父类型的。

有一个比较好记的口诀:

  • 只读不可写时,使用 List<? extends Fruit>: Producer

  • 只写不可读时,使用 List<? super Apple>: Consumer

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

参考:

关于Java与Kotlin泛型你应该知道的知识点

相关文章

  • 泛型中 ? super T和 ? extends T的区别

    首先, 说到 Java 的泛型, 我们必须要提到的是Java 泛型的类型擦除机制: Java中的泛型基本上都是在编...

  • Java SE 3

    Java SE 1.Java泛型机制 泛型是Java SE1.5引入的特性,泛型的本质是参数化类型。在类、接口和方...

  • Java的泛型类型擦除及类型擦除带来的问题

    1、泛型的类型擦除 Java的泛型是伪泛型,不同于C++的模板机制,这是因为Java的泛型只存在编译期间,在编译完...

  • Java反射(三)泛型

    一、泛型和Class类从JDK 1.5 后,Java中引入泛型机制,Class类也增加了泛型功能,从而允许使用泛型...

  • java的泛型机制

    <1>:java的泛型机制是javase1.5引入的 <2>:泛型擦除的概念:java中的泛型在编译之后生成的字节...

  • 近期的感想

    最近一直看Java的泛型机制,但是总是感觉这种泛型机制非常的乱。很多细节需要思考,不像多态机制那么的成熟。

  • Java反射机制总结(三)

    一、泛型和Class类 从JDK 1.5 后,Java中引入泛型机制,Class类也增加了泛型功能,从而允许使用泛...

  • 【阿里P8大牛教你Android入门之路(java篇)】Java

    一、泛型和Class类 从JDK 1.5 后,Java中引入泛型机制,Class类也增加了泛型功能,从而允许使用泛...

  • Kotlin-泛型和委托

    泛型 泛型的使用是为了程序有更好的扩展性。泛型类和泛型方法 泛型的高级特性java的泛型是通过类型擦除机制来实现的...

  • Java 与 Kotlin 泛型

    Java Java 泛型(generics)是 JDK 5 中引入的一个新特性,泛型提供了编译时类型安全检测机制,...

网友评论

      本文标题:Java 泛型机制

      本文链接:https://www.haomeiwen.com/subject/qysvjrtx.html