我们为什么需要泛型,即泛型有什么用?
首先举两个例子
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.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
,说明泛型类型String
和Integer
都被擦除掉了,只剩下原始类型
Java
的泛型也可以被称作是伪泛型
-
真泛型:泛型中的类型是真实存在的。
-
伪泛型:仅于编译时类型检查,在运行时擦除类型信息。
看到这里我们可以自然地引出下一个问题,为什么Java
中的泛型是伪泛型,为什么要这样实现?
2.2 为什么需要泛型擦除?
泛型擦除看起来有些反直觉,有些奇怪。为什么Java
不能像C#
一样实现真正的泛型呢?为什么Java
的泛型要用"擦除"实现
单从技术来说,Java
是完全100%
能实现我们所说的真泛型
,而之所以选择使用泛型擦除主要是从API
兼容的角度考虑的
导致Java 5
引入的泛型采用擦除式实现的根本原因是兼容性上的取舍,而不是“实现不了”的问题。
举个例子,Java
到1.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.直接把已有的类型泛型化,让所有需要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版。
.NET
在1.1 -> 2.0
的时候选择了上面选项的1,而Java
则选择了2。
从Java
设计者的角度看,这个取舍很明白。
.NET
在1.1 -> 2.0
的时候,实际的应用代码量还很少(相对Java
来说),而且整个体系都在微软的控制下,要做变更比较容易;
而Java
在1.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
来获取类型信息的
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
中能够获取泛型类型的原因
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
。
也就是说java
的class
文件会保存继承的父类或者接口的泛型信息。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
总结
写到这里也结束了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个关于Flutter的学习思路及方向,从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。
由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的
还有高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
/2024/03/13/H4lCoPEF.jpg" />
总结
写到这里也结束了,在文章最后放上一个小小的福利,以下为小编自己在学习过程中整理出的一个关于Flutter的学习思路及方向,从事互联网开发,最主要的是要学好技术,而学习技术是一条慢长而艰苦的道路,不能靠一时激情,也不是熬几天几夜就能学好的,必须养成平时努力学习的习惯,更加需要准确的学习方向达到有效的学习效果。
由于内容较多就只放上一个大概的大纲,需要更及详细的学习思维导图的
还有高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术资料,并且还有技术大牛一起讨论交流解决问题。
[外链图片转存中…(img-XMNcCmGj-1712427748145)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!