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

最后

看完美团、字节、腾讯这三家的面试问题,是不是感觉问的特别多,可能咱们又得开启面试造火箭、工作拧螺丝的模式去准备下一次的面试了。

开篇有提及我可是足足背下了1000道题目,多少还是有点用的呢,我看了下,上面这些问题大部分都能从我背的题里找到的,所以今天给大家分享一下互联网工程师必备的面试1000题

注意不论是我说的互联网面试1000题,还是后面提及的算法与数据结构、设计模式以及更多的Java学习笔记等,皆可分享给各位朋友

最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

互联网工程师必备的面试1000题

而且从上面三家来看,算法与数据结构是必备不可少的呀,因此我建议大家可以去刷刷这本左程云大佬著作的《程序员代码面试指南 IT名企算法与数据结构题目最优解》,里面近200道真实出现过的经典代码面试题

最新“美团+字节+腾讯”一二三面问题,挑战一下你能走到哪一面?

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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>

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 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 basket){

for (Fruit fruit : basket) {

System.out.println(fruit);

//…do something other

}

}

List fruitBasket = new ArrayList();

getOutFruits(fruitBasket);//成功

List appleBasket = new ArrayList();

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

fruitBasket.add(new Fruit());

getOutFruits(fruitBasket);

List 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 开发相关的学习文档、面试题、Android 核心笔记等等文档,希望能帮助到大家学习提升,如有需要参考的可以直接去我 CodeChina地址:https://codechina.youkuaiyun.com/u012165769/Android-T3 访问查阅。

写在最后

作为一名即将求职的程序员,面对一个可能跟近些年非常不同的 2019 年,你的就业机会和风口会出现在哪里?在这种新环境下,工作应该选择大厂还是小公司?已有几年工作经验的老兵,又应该如何保持和提升自身竞争力,转被动为主动?

就目前大环境来看,跳槽成功的难度比往年高很多。一个明显的感受:今年的面试,无论一面还是二面,都很考验Java程序员的技术功底。

最近我整理了一份复习用的面试题及面试高频的考点题及技术点梳理成一份“Java经典面试问题(含答案解析).pdf和一份网上搜集的“Java程序员面试笔试真题库.pdf”(实际上比预期多花了不少精力),包含分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货!

由于篇幅有限,为了方便大家观看,这里以图片的形式给大家展示部分的目录和答案截图!

Java经典面试问题(含答案解析)

阿里巴巴技术笔试心得

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

了不少精力),包含分布式架构、高可扩展、高性能、高并发、Jvm性能调优、Spring,MyBatis,Nginx源码分析,Redis,ActiveMQ、Mycat、Netty、Kafka、Mysql、Zookeeper、Tomcat、Docker、Dubbo、Nginx等多个知识点高级进阶干货!

由于篇幅有限,为了方便大家观看,这里以图片的形式给大家展示部分的目录和答案截图!
[外链图片转存中…(img-6X4I7l1Z-1715809055252)]

Java经典面试问题(含答案解析)

[外链图片转存中…(img-QcJALj6s-1715809055252)]

阿里巴巴技术笔试心得

[外链图片转存中…(img-xJQqh2RL-1715809055253)]

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值