Android开发面试——Java泛型机制7连问

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

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 list1 = new ArrayList();

list1.add(“abc”);

ArrayList list2 = new ArrayList();

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> 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 ServiceMethod 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 parse(String jsonStr){

List topNews = new Gson().fromJson(jsonStr, new TypeToken<List>() {}.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>

最后

学习视频:

大厂面试真题:

List的泛型信息String`的呢?

Class类提供了一个方法public Type getGenericSuperclass() ,可以获取到带泛型信息的父类Type

也就是说javaclass文件会保存继承的父类或者接口的泛型信息。

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

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

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

最后

学习视频:

[外链图片转存中…(img-SVzTMNI8-1719249485674)]

大厂面试真题:

[外链图片转存中…(img-tN0cK6Sb-1719249485675)]

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值