博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
死磕Java泛型(一篇就够)
阅读量:6162 次
发布时间:2019-06-21

本文共 8134 字,大约阅读时间需要 27 分钟。

Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛型实现机制的限制,如果不能深入全面的掌握泛型知识,就不能较好的驾驭使用泛型,同时在阅读开源项目时也会处处碰壁,这一篇就带大家全面深入的死磕Java泛型。

泛型擦除初探

相信泛型大家都使用过,所以一些基础的知识点就不废话了,以免显得啰嗦。 先看下面的一小段代码

public class FruitKata {    class Fruit {}    class Apple extends generic.Fruit {}        public void eat(List fruitList) {}    public void eat(List
fruitList) { } // error, both methods has the same erasure}复制代码

我们在FruitKata类中定义了二个eat的方法,参数分别是List和List<> 类型,这时候编译器报错了,并且很智能的给出了“ both methods has the same erasure” 这个错误提示。显然,编译器在抱怨,这二个方法具有同样的签名,嗯~~,这就是泛型擦除存在的一个证据,要进一步验证也很简单。我们通过ByteCode Outline这个插件,可以很方便的查看类被编译后的字节码,这里我们只贴出eat方法的字节码。

// access flags 0x1  // signature (Ljava/util/List
;)V // declaration: void eat(java.util.List
) public eat(Ljava/util/List;)V复制代码

可以看到参数确实已经被擦除为List类型,这里要明确一点是,这里擦除的只是方法内部的泛型信息,而泛型的元信息还是保存在类的class字节码文件中,相信细心的同学已经发现了上面我特意将方法的注释一并贴了出来

// signature (Ljava/util/List
;)V复制代码

这个signature字段大有玄机,后面会详细说明。 这里只是以泛型方法来做个说明,其实泛型类,泛型返回值都是类似的,兄弟们可以自己动手试试看。

为什么用擦除来实现泛型

要回答这个问题,需要知道泛型的历史,Java的泛型是在Jdk 1.5 引入的,在此之前Jdk中的容器类等都是用Object来保证框架的灵活性,然后在读取时强转。但是这样做有个很大的问题,那就是类型不安全,编译器不能帮我们提前发现类型转换错误,会将这个风险带到运行时。 引入泛型,也就是为解决类型不安全的问题,但是由于当时java已经被广泛使用,保证版本的向前兼容是必须的,所以为了兼容老版本jdk,泛型的设计者选择了基于擦除的实现。 由于Java的泛型擦除,在运行时,只有一个List类,那么相对于C#的基于膨胀的泛型实现,Java类的数量相对较少,方法区占用的内存就会小一点,也算是一个额外的小优点吧。

泛型擦除带来的问题

由于泛型擦除,下面这些代码都不能编译通过

T t = new T();T[] arr = new T[10];List
list = new ArrayList
();T instanceof Object复制代码

通配符

作为泛型擦除的补偿,Java引入了通配符

List
fruitList;List
appleList;复制代码

这二个通配符很多同学都存在误解。

? extends

?extends Fruit 表示Fruit是这个传入的泛型的基类(Fruit是泛型的上界),还是以上面的Fruit和Apple为例,看下面这段代码

List
fruitList = new ArrayList<>();fruitList.add(new Fruit()); //error复制代码

按照我们上面对? extends的理解,fruitList应该是可以添加一个Fruit的,但是编译器却给我们报错了。我第一次看到这里时也感觉不太好理解,我们来看个例子就能理解了。

List
fruitList = new ArrayList<>();List
appleList = new ArrayList<>();fruitList = appleList;fruitList.add(new Fruit()); //error复制代码

如果fruitList允许添加Fruit,我们就将Fruit添加到了AppleList中了,这肯定是不能接受的。

? super

再来看个?super的例子

List
superAppleList = new ArrayList<>();superAppleList.add(new Apple());superAppleList.add(new Fruit()); // error复制代码

向superAppleList中添加Apple是可以的,添加Fruit还是会报错,好,上面我们说的这些就是 PECS 原则。

PECS

英文全称,Producer Extends Consumer Super,

  1. 如果需要一个只读的泛型集合,使用?extends T
  2. 如果需要一个只写的泛型集合,使用?super T

我自己是这样来理解通配符的

  1. 因为? extends T给外界的承诺语义是,这个集合内的元素都是T的子类型,但是到底是哪个子类型不知道,所以添加哪个子类型,编译器都认为是危险的,所以直接禁止添加。
  2. 因为? super T 给外界的承诺语义是,这个集合内的元素的下界是T,所以向集合中添加T以及T的子类型是安全的,不会破坏这个承诺语义。
  3. List, List 都是List<? super Apple>的子类型。 List 是List<? extends Apple>的子类型。

关于泛型的使用,Jdk中有很多经典的应用范例,比如Collections的copy方法

public static 
void copy(List
dest, List
src) { int srcSize = src.size(); if (srcSize > dest.size()) throw new IndexOutOfBoundsException("Source does not fit in dest"); if (srcSize < COPY_THRESHOLD || (src instanceof RandomAccess && dest instanceof RandomAccess)) { for (int i=0; i
di=dest.listIterator(); ListIterator
si=src.listIterator(); for (int i=0; i

泛型擦除了,我们还能拿到泛型信息吗

前面我们提到过class字节码中会有个signature字段来保存泛型信息。我们新建一个泛型方法

public 
T plant(T fruit) { return fruit; }复制代码

查看class文件的二进制信息,发现里面确实有Signature字段信息。

Signature%
(TT;)TT;复制代码

既然泛型信息还是在class文件中,那我们有没有办法在运行时拿到呢? 办法肯定是有的。 来看一个例子

Class clazz = HashMap
(){}.getClass(); Type superType = clazz.getGenericSuperclass(); if (superType instanceof ParameterizedType) { ParameterizedType parameterizedType = (ParameterizedType) superType; Type[] actualTypes = parameterizedType.getActualTypeArguments(); for (Type type : actualTypes) { System.out.println(type); } }// 打印结果class java.lang.Stringclass generic.FruitKata$Apple复制代码

可以看到我们拿到并打印了泛型的原始类型信息。为了加深对泛型使用的理解,我接下来再看几个小例子。

泛型在Gson解析中的使用
String jsonString = ".....";  // 这里省略json字符串Apple apple = new Gson().fromJson(jsonString, Apple.class);复制代码

这是一段很简单的Gson解析使用代码,我们进一步去看它fromJson的方法实现

public 
T fromJson(String json, Class
classOfT) throws JsonSyntaxException { Object object = fromJson(json, (Type) classOfT); return Primitives.wrap(classOfT).cast(object); }复制代码

最终会执行到

TypeToken
typeToken = (TypeToken
) TypeToken.get(typeOfT); TypeAdapter
typeAdapter = getAdapter(typeToken); T object = typeAdapter.read(reader);复制代码

通过我们传入的Class类型构造TypeToken,然后通过TypeAdapter将json字符串转化为对象T,中间的细节这里就不继续深入了。

泛型在retrofit中的使用

我们在使用retrofit时,一般都会定义一个或多个ApiService接口类

@GET("users/{user}/repos")Call
> listRepos(@Path("user") String user);复制代码

接口方法的返回值都使用了泛型,所以注定在编译期是要被擦除的,那retrofit是如何得到原始泛型信息的呢。其实有上面的泛型知识以及Gson的使用说明,相信大家以及有答案了。 retrofit框架本身设计的很优雅,细节这里我们不深入展开,这里我们只关心泛型数据转换为返回值的过程。 我们需要定义如下几个类

// ApiService.classpublic interface ApiService {    Observable
> getAppleList();}// Apple.classclass Apple extends Fruit { private int color; private String name; public Apple() {} public Apple(int color, String name) { this.color = color; this.name = name; } @Override public String toString() { return "color:" + this.color + "; name:" + name; }}复制代码

接下来,我定义一个动态代理,

InvocationHandler handler = new InvocationHandler() {       @Override       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {            Type returnType = method.getGenericReturnType();            if (returnType instanceof ParameterizedType) {               ParameterizedType parameterizedType = (ParameterizedType) returnType;               Type[] types = parameterizedType.getActualTypeArguments();               if (types.length > 0) {                   Type type = types[0];                   Object object = new Gson().fromJson(mockAppleJsonString(), type);                   return Observable.just(object);             }           }          return null;     }  };// mock json数据public static String mockAppleJsonString() {   List
apples = new ArrayList<>(); apples.add(new Apple(1, "红富士")); apples.add(new Apple(2, "青苹果")); return new Gson().toJson(apples);}复制代码

接下来就是正常的调用了,这里模拟了retrofit数据转换的过程。

ApiService apiService = (ApiService) Proxy.newProxyInstance(ProxyKata.class.getClassLoader(),                new Class[] {ApiService.class}, handler);Observable
> call = apiService.getAppleList();if (call != null) { call.subscribe(apples -> { if (apples != null) { for (Apple apple : apples) { System.out.println(apple); } } });}// 输出结果color:1; name:红富士color:2; name:青苹果复制代码
泛型在MVP中的应用

MVP模式相信做Android开发的没人不知道,假设我们有这样几个类

public class BaseActivity
> extends AppCompatActivity { protected P mPresenter; //....}public class MainActivity extends BaseActivity
implements MainView { //....}复制代码

由于泛型擦除的关系,我们不能在BaseActivity中直接新建Presenter来初始化mPresenter,所以一般通常的做法是暴露一个createPresenter方法让子类重写。但是今天我们介绍另外一种方法,直接看代码

// BaseActivity.class        Type superType = getClass().getGenericSuperclass();        if (superType instanceof ParameterizedType) {            ParameterizedType parameterizedType = (ParameterizedType) superType;            Type[] types = parameterizedType.getActualTypeArguments();            for (Type type : types) {                if (type instanceof Class) {                    Class clazz = (Class) type;                    try {                        mPresenter = (P) clazz.newInstance();                        mPresenter.bindView((V) this);                    } catch (IllegalAccessException e) {                        e.printStackTrace();                    } catch (InstantiationException e) {                        e.printStackTrace();                    }                }            }        }复制代码

我们通过在BaseActivity中是能够拿到泛型的原始信息的,通过反射初始化出来mPresenter,并调用bindView来绑定我们的视图接口。通过这种方式,我们利用泛型的能力,基类包办了所有的初始化任务,不但逻辑简单,而且也体现了高内聚,在实际项目中可以尝试使用。

总结

深入理解Java泛型是工程师进阶的必备技能,希望你看了这篇文章,在今后,不论是面试还是其他的时候,谈到Java泛型时都能够云淡风轻,在使用泛型编写代码时也能够信手拈来。

转载于:https://juejin.im/post/5d03380ae51d455a694f9510

你可能感兴趣的文章
虚机不能启动的特例思考
查看>>
SQL Server编程系列(1):SMO介绍
查看>>
在VMware网络测试“专用VLAN”功能
查看>>
使用Formik轻松开发更高质量的React表单(三)<Formik />解析
查看>>
也问腾讯:你把用户放在什么位置?
查看>>
CSS Sprites 样式生成工具(bg2css)
查看>>
[转]如何重构代码--重构计划
查看>>
类中如何对list泛型做访问器??
查看>>
C++解析XML--使用CMarkup类解析XML
查看>>
P2P应用层组播
查看>>
Sharepoint学习笔记—修改SharePoint的Timeouts (Execution Timeout)
查看>>
CSS引入的方式有哪些? link和@import的区别?
查看>>
Redis 介绍2——常见基本类型
查看>>
asp.net开发mysql注意事项
查看>>
(转)Cortex-M3 (NXP LPC1788)之EEPROM存储器
查看>>
ubuntu set defult jdk
查看>>
[译]ECMAScript.next:TC39 2012年9月会议总结
查看>>
【Xcode】编辑与调试
查看>>
用tar和split将文件分包压缩
查看>>
[BTS] Could not find stored procedure 'mp_sap_check_tid'
查看>>