泛型在Java和Kotlin中的细微区别,你Get到了吗?

2024-03-21 09:59

本文主要是介绍泛型在Java和Kotlin中的细微区别,你Get到了吗?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述
码农 | 章磊

“人应当相信,不了解的东西总是可以了解的,否则他就不会再去思考。”

哥德

在文章《泛型漫谈》中我们曾讲过泛型的基础知识,这里不再赘述。

那么这篇文章主要讲泛型在Java和Kotlin中区别,可能你在Java中泛型玩得很6,在Kotlin中就不好使了。好了,费话不多说,开讲啦~

一、泛型参数的缺省

我们知道,Java中泛型可以不指定泛型参数,比如以下代码是可以编译通过的。

List list = new ArrayList();

其效果与下面一样:

List list = new ArrayList<Object>();

而Java之所以这样搞,据小道消息,是为了兼容1.5以前还没有泛型的版本。

那Kotlin中也可以这样玩吗?明显不行!!!
在这里插入图片描述
根据提示,必须显示地提供泛型参数信息。

那为什么Kotlin不用往后兼容捏?因为它是基于Java1.6,即天然拥有泛型,可以从源头控制必须指定泛型。

虽然说Kotlin中必须指定泛型参数,但由于它可以类型推导,所以以下写法也是允许的。

val arrayList = arrayListOf("A", "B") // 等价于val arrayList = arrayListOf<String>("A", "B")

二、不变、协变、逆变

关于这几个概念,我们从其他地方复制下:

逆变与协变用来描述类型转换(type transformation)后的继承关系,其定义:如果𝐴、𝐵表示类型,𝑓(⋅)表示类型转换,≤表示继承关系(比如,𝐴≤𝐵表示𝐴是由𝐵派生出来的子类);
𝑓(⋅)是逆变(contravariant)的,当𝐴≤𝐵时有𝑓(𝐵)≤𝑓(𝐴)成立;
𝑓(⋅)是协变(covariant)的,当𝐴≤𝐵时有𝑓(𝐴)≤𝑓(𝐵)成立;
𝑓(⋅)是不变(invariant)的,当𝐴≤𝐵时上述两个式子均不成立,即𝑓(𝐴)与𝑓(𝐵)相互之间没有继承关系。

看晕了没关系,举个粟子就清楚了,比如在Java中:

形如Type<T>就是不变;
形如Type<? extends T>就是协变;
形如Type<? super T>就是逆变;

那Kotlin中其实也有对应的表示:

    class KC1<T> {} // 不变class KC2<out T> {} // 协变class KC3<in T> {} // 逆变

乍一看,把写法替换下就好了,其实还是有差别的。
比如,在Java的协变中是可以直接以T作为入参:

class JC2<T extends Number> {/*** 可以有T类型的形参* @param t*/void f(T t) {System.out.println(t);}
}

而在Kotin中是默认不允许的:

class KC2<out T: Number> {/*** 不可以有T类型形参*/fun f(t: T) {print(t)}
}

如果一定要这样的话,可以加上UnsafeVariance的注解。

 fun f(t: @UnsafeVariance T) {print(t)}

其实从out这个关键字就知道,T仅作为返回值输出,输入是不应该的。
同理,逆变中Kotlin使用了in这个关键字,所以可以用作参数,但不应该用作返回,具体大家可以试试。

关于数组的协变,Java和Kotlin中也是不同的。

在Java中可以写出这样的代码:

Integer[] values = new Integer[5];
Number[] numbers = values;

说明Java中数组是支持协变的。

如果我们用同样的思维在Kotlin这样写的话,就会出错:

val values = arrayOf(1, 2, 3, 4)
val numbers: Number = values // Error: type mismatch.

然后对于集合Collection,Java是不支持协变,Kotlin中反倒是支持的,比如下面代码也没编译问题。

val values = ArrayList<Int>()
val numbers: List<Number> = values

我们分别查看List的源码:

public interface List<E> extends Collection<E> { // Java E
public interface List<out E> : Collection<E> // Kotlin out E

那么Kotlin中不担心下面代码引起的ArrayStoreException吗?

val values = ArrayList<Int>()
val numbers: List<Number> = values
numbers.add(3.14F) // Kotlin中List不支持add

其实不必担心,因为Kotlin中的List不支持写操作,所以压根不存在所谓的存储异常。
在这里插入图片描述

三、多约束

在Java中,如果要求泛型参数T继承ClassA,并且实现接口InterfaceB的话,我们只能再写一个类ClassC去继承ClassA,实现InterfaceB,如图。

interface InterfaceB {
}class ClassC extends ClassA implements InterfaceB {
}class JC2<T extends ClassC> {
}

所以可以大胆地说,Java对泛型参数不支持多约束。

那号称“更好的Java”的Kotlin呢?

fun <T> f(t: @UnsafeVariance T) where T:ClassA, T: InterfaceB{print(t)
}

立马感觉简洁了很多!

四、获取泛型参数类型

我们知道泛型参数信息在编译后是会被擦除的,也就是说理论上在运行时是无法拿到泛型参数类型的。

然而……擦除并不是被抹得一干二净,在class的常量仍然可以拿到泛型相关信息。
在这里插入图片描述
而匿名内部类在初始化时会绑定父类或父接口相关信息,所以无论在Java还是Kotlin中我们都可以通过这种方式拿到泛型参数类型,只是写法不同罢了。

// Java版本
class JGenericsToken<T> {Type type;public JGenericsToken() {type = ((ParameterizedType)getClass().getGenericSuperclass()).getActualTypeArguments()[0];
//        System.out.println(type);}
}// 测试
@Test
public void testGeneric() {// JavaSystem.out.println(new JGenericsToken<String>(){}.type);
}
// 输出
class java.lang.String
// Kotlin版本
open class GenericToken<T> {var type: Type? = nullinit {var superClass = this.javaClass.genericSuperclasstype = (superClass as ParameterizedType).actualTypeArguments[0]}
// 测试@Testfun testGeneric() {val obj = object : GenericToken<String>(){}print(obj.type)}

其实Gson中就是这样实现的。

除此之外,在Kotlin中我们还可以通过内联函数配合reified关键字获取参数类型。

inline fun <reified T> getType() {return T:class:java
}

五、通配符

关于通配符没什么好说的,就是Java中用"?",Kotlin中用"*"。
只要记住Java中Type<?>相当于Type<? extends Object>.
Kotlin中,Type<*>相当于Type<out Any?>。

好了,今天就讲这么多啦,码字不易,看到这里的朋友记得点个赞哈~

这篇关于泛型在Java和Kotlin中的细微区别,你Get到了吗?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/832378

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input

Spring Gateway动态路由实现方案

《SpringGateway动态路由实现方案》本文主要介绍了SpringGateway动态路由实现方案,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随... 目录前沿何为路由RouteDefinitionRouteLocator工作流程动态路由实现尾巴前沿S