换个姿势,十分钟拿下Java/Kotlin泛型

Posted 嘴巴吃糖了

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了换个姿势,十分钟拿下Java/Kotlin泛型相关的知识,希望对你有一定的参考价值。

0x1、引言

解完BUG,又有时间摸鱼学点东西了,最近在复习Kotlin,跟着朱涛的 《Kotlin 编程第一课》查缺补漏。

看到泛型这一章时,想起之前面一家小公司时的面试题:

说下你对泛型协变和逆变的理解

读者可以试试在不查资料的情况下能否答得上来?

反正我当时是没想起来,尽管写过一篇《Kotlin刨根问底(三):你真的懂泛型,会用吗?》,我以为自己对泛型了然于胸。

究其根源,对概念名词的理解浮于表面,模棱两可,知道有这个东西,但本质是什么?为啥要用?怎么用?并没有二次加工形成自己的思考和理解,所以印象不深刻。加之 泛型平时开发用的不多记和忆 两个要素都没做到,久了自然会忘。

而网上关于泛型讲解的文章大都千篇一律:集合 存取元素类型异常引出泛型,不变、协变、逆变、型变一把梭,什么能读不能写,能写不能读,读者看完好像懂了,又好像没懂,这很正常,毕竟作者自己都可能弄不明白,2333。就问你一句:泛型只能用在集合上吗

综上原因,有了这篇文章,本节换个角度,从根上理解泛型,少说废话掐要点,这次一定拿下Java/Kotlin泛型。


0x2、泛型到底是什么?

直接说结论:

泛型的本质类型参数化要操作的数据类型 可以通过 参数的形式来指定

说人话:把数据类型变成参数

难理解?类比 函数/方法,定义时指定 参数类型(形参),调用时传入 具体参数实例(实参)

泛型 也是如此,定义时指定 数据类型(形参),调用时传入 具体数据类型(实参)

非常相似,只是 数据类型 的定义和传递都是通过 <>,而不是(),那 泛型的作用是什么呢?直接说结论:

语法糖 → Java制定了一套规则 (书写规范),按照这套规则编写代码,编译器会在生成代码时自动完成类型转换,避免手动编写代码引起的类型转换问题。

有点抽象?没关系,来个直观例子对照学习,以解析接口返回数据伪代码为例,先不使用泛型:

public class Article 
    public void parseJson(String content) 
        System.out.println(content + ":Json → Article");
    


public class Banner 
    public void parseJson(String content) 
        System.out.println(content + ":Json → Banner");
    


public class HotKey 
    public void parseJson(String content) 
        System.out.println(content + ":Json → HotKey");
    


public class Response 
    private Object entity;

    public Response(Object entity)  this.entity = entity; 

    public void parseResponse(String response) 
        // 手动编写:类型判定 + 强转
        if (entity instanceof Article) 
            ((Article) entity).parseJson(response);
         else if (entity instanceof Banner) 
            ((Banner) entity).parseJson(response);
         else if (entity instanceof HotKey) 
            ((HotKey) entity).parseJson(response);
        
    

可以看到,为了避免 类型转换异常,需要手动进行 类型判定和强转。毕竟,不判定直接强转,来个null直接就崩了。

面向对象思想,可以抽个父类Entity给Article、Banner、HotKey继承,Response可以少写个强转:

public class Response 
    private Entity entity;

    public Response(Entity entity) 
        this.entity = entity;
    

    public void parseResponse(String response) 
        if (entity instanceof Article
                || entity instanceof Banner
                || entity instanceof HotKey) 
            entity.parseJson(response);
        
    

代码稍微清爽了一点,但依旧存在隐患,增删解析实体类型,都要手动修改此处代码。而人是容易犯错的,漏掉类型不自知很正常,编译器也不报错,可能要到 运行时才发现问题

能否 对数据类型进行范围限定,传入范围外的类型,编译器直接报错,在 编译期 就发现问题呢?

可以,用好 泛型 这枚语法糖,能帮我们提前规避这种风险,稍微改动下代码:

public class Response<T extends Entity> 
    private final T entity;

    public Response(T entity) 
        this.entity = entity;
    

    public void parseResponse(String response) 
        // 预先知道类型是Entity或其子类,无需类型判断即可放心调用方法
        if (entity != null) entity.parseJson(response);
    


// 调用处:
public class ResponseTest 
    public static void main(String[] args) 
        new Response<Article>(new Article()).parseResponse("请求文章接口");
        new Response<Banner>(new Banner()).parseResponse("请求Banner接口");
        new Response<HotKey>(new HotKey()).parseResponse("请求热词接口");
    

此时,修改实体类 (删除、修改继承关系、传入非Enitiy及其子类) 编译器直接报错,而增加实体类,直接传类型参数:

new Response<UserInfo>(new UserInfo()).parseResponse("请求");

增删实体类均无需修改 parseResponse() 方法,还避免了 运行时由于对象类型不匹配引发的异常

泛型这种 把数据类型的确定 推迟到 创建对象或调用方法时 的玩法跟 占位符 很像。

好处也很明显,逻辑复用灵活性强,而所谓的泛型边界、不变、型变等,就是围绕着这个 “占位符” 制定的一系列 语法规则 而已。所以,泛型不是非用不可!!!

  • 用了 → 可以少写一些代码,可以在编译期提前发现类型转换问题;
  • 不用 → 得多写一些类型判定和强转代码,可能存在类型转换问题;

0x3、泛型规则

了解完泛型是啥?有什么用?接着来理解它的规则,即 指定目标数据类型 的一些语法。

① 边界限制

就上面例子里的 <T extends Entity>,要求传入的泛型参数必须是 Entity类或它的子类,又称 泛型上界

限制上界的好处:可以直接 调用父类或父接口的方法,如上面直接调 entity.parseJson();

Tips:Kotlin中用冒号:代替extends → <T:Entity>

② 不变、协变、逆变

泛型是不变的!这句话怎么理解?看下这段代码:

咋回事?Entity和Article不是有 继承关系 吗?为啥不能互相替代?因为能替换的话 的时候有问题:

为了避免这两个问题,编译器直接认为 Response<Entity> 和 Response<Article> 不存在继承关系,无法相互替代,即 只能识别具体的类型,这就是 泛型的不变性

而在有些场景,这样的特性会给我们带来一些不便,可以通过 型变扩展参数的类型范围,有下面两种形式:

协变父子关系一致子类也可以作为参数传进来<? extends Entity>上界通配符

Tips:Kotlin中使用 out 关键字表示协变 → Response

逆变父子关系颠倒父类也可以作为参数传进来<? super Article>下界通配符

Tips:Kotlin中使用 in 关键字表示逆变 → Response

可以看到,型变 虽然拓展了参数的类型范围,但也导致 不能按照泛型类型读取元素

除此之外,还有一个 无限定通配符<?>,等价于 <? extends Object>不关心泛型的具体类型时,可以用它。

Tips:Kotlin中使用 星投影<*> 表示,等价于

再补充一点,根据 定义型变的位置,分为 使用处型变 (对象定义)声明处型变 (类定义)

Java只有使用处型变 (例子就是),而Kotlin两种都有,示例如下:

// Kotlin 使用处型变
fun printArticleResponse(response: Response<in Article>) 
    response.parseResponse("开始请求接口")


// Kotlin 声明处型变
class KtResponse<in T>(private val entity: T)
    fun getOut(): T = t

③ 何时用协变?何时用逆变?

看到这里,读者可能会疑惑:使用两种 型变 不是为了 扩展参数的类型范围 么?

让子类也能传协变(extends out)让父类也能传逆变(super in)

难不成还有更详细的规则?是的!先提一嘴介个:

  • 向上转型 → 子类转换成父类 (隐式),安全,可以访问父类成员;
  • 向下转型 → 父类转换成子类 (显式),存在安全隐患,子类可能有一些父类没有的方法;

接着改下例子:

先是 协变能读不能写 (能用父类型去获取数据,不确定具体类型,不能传)

接着是 逆变能写不能读 (能传入子类型,不确定具体类型,不能读,但可以用Object读)

没看懂的话,多看几遍,实在不行,那就背:PECS法则 (Producer Extends,Consumer Super)

  • 生产者extends/out协变对象只作为返回值传出
  • 消费者super/in逆变对象只作为参数传入

Tips:Kotlin官方文档写的 Consumer in, Producer out!,好像更容易理解和记忆~

另外,在某些特殊场景,泛型参数 同时作为参数和返回值,可以使用 @UnsafeVariance 注解来解决 型变冲突,如 Kotlin\\Collections.kt 中的:

到此,泛型的规则就讲解完毕了,纸上得来终觉浅,绝知此事要躬行,建议自己写点代码试试水,加深印象,如:

当然阅读源码也是一个很好的巩固方式,Java\\Kotlin集合类相关代码大量使用了泛型~


0x4、一些补充

① Java假泛型

和C#等编程语言的泛型不同,Java和Kotlin中的泛型都是 假泛型,原理 → 类型擦除(Type Erasure)

生成Java字节码中是 不包含泛型类型信息的,它只存在于代码编译阶段,进JVM前会被擦除~

写个简单例子验证:

可以看到,此时的 类类型 皆为 Response,那定义的泛型类型都哪去了?

答:被替换成 原始类型,没指定 限定类型 就是 Object,有则为 限定类型

反编译字节码看看 (安装 bytecode viewer 插件,然后点 View -> Show Bytecode)

可以看到都被替换成 Object,试试加上 泛型上界

反编译字节码:

可以看到变成了 限定类型 (父类型Entity)。

另外,我们可以通过 反射 的反射绕过Java的假泛型:

到此,你可能还有一个疑问:为什么Java不实现真泛型

答:向前兼容,使得Java 1.5前未使用泛型类的代码,不用修改仍可以继续正常工作。

详细历史原因讲解可自行查阅:《Java 不能实现真正泛型的原因是什么?》


② Java为什么不支持泛型数组

在Java中,允许把子类数组赋值给父类数组变量,所以下面的代码是可行的:

如果我们往Object数组里放一个Entity实例,编译器提示,但不报错:

但运行时会检查假如数组的对象类型,然后抛出异常:

回到问题,假如 Java支持泛型数组,那下面的代码会怎样?

Response<Article>[] articles = new Response<Article>[10];
Response<Entity>[] entities = articles;
entities[0] = new Response<Banner>();

类型擦除,Article、Entity、Banner都变成Object,这个时候,只要是Response,编译器都不会报错。

本来定义的Response

,但现在什么Response都能放,代码还按原有方式取值,就很有可能异常了。

以上是关于换个姿势,十分钟拿下Java/Kotlin泛型的主要内容,如果未能解决你的问题,请参考以下文章

Java 泛型泛型用法 ( 泛型编译期擦除 | 上界通配符 <? extends T> | 下界通配符 <? super T> )

换个姿势访问图片

换个姿势 飞速下载Gradle

换个姿势,躺个平

换个姿势,躺个平

换个姿势学设计模式:策略模式