干货拿走:深入解析Kotlin 泛型

Kotlin开发实战:高频痛点解析与避坑指南 在软件开发中,构建工具和语言特性的正确使用是保障项目稳定性的基础。Gradle作为主流的构建工具,其版本管理与依赖配置直接影响编译结果;而Kotlin作为现代编程语言,其协程、空安全等特性在提升开发效率的同时,也带来了新的挑战。理解Gradle的依赖解析机制和Kotlin协程的结构化并发原理,对于构建可靠应用具有重要技术价值。在实际开发中,尤其是在多模块项目或与Java生态互操作时,版本冲突、协程泄漏等问题频发。本文聚焦于Kotlin开发中的高频痛点,如元数据版本不兼容、协程泄漏等,提供从根因分析到一劳永逸 阅读详情

0. 引子

Kotlin 100% 与 Java 兼容,所以抛开语言表面上面的种种特质之外,背后的语言逻辑或者说“灵魂”与 Java 总是想通的。本文只涉及 Kotlin Jvm,Kotlin Js、Kotlin Native 的具体实现可能有差异。

最近一段时间在网上发了一套 Kotlin 的入门视频,涵盖了基础语法、面向对象、高阶函数、DSL、协程等比较有特色的知识点,不过有朋友提出了疑问:这门课为什么不专门讲讲泛型、反射和注解呢?

我最早听到这个问题的时候,反应比较懵逼,因为我居然没有感觉到 Kotlin 的反射、泛型特别是注解有专门学习的必要,因为他们跟 Java 实在是太像了。

实际上,从社区里面学习 Kotlin 的朋友的反应来看,大家大多对于函数式的写法,DSL,协程这些内容比较困惑,或者说不太适应,这与大家的知识结构是密切相关的,面向对象的东西大家很容易理解,因为就那么点儿内容,你懂了 C++ 的面向对象,Java 的也很容易理解,Kotlin 的也就不在话下了;而你没有接触过 Lua 的状态机,没有接触过 Python 的推导式,自然对于协程也就会觉得比较陌生。

所以我想说的是,泛型这东西,只要你对 Java 泛型有一定的认识,Kotlin 的泛型基本可以直接用。那我们这篇文章要干嘛呢?只是做一个简单的介绍啦,都很好理解的。

1. 真·泛型和伪·泛型

Java 的泛型大家肯定都知道了,1.5 之后才加入的,可以为类和方法分别定义泛型参数,就像下面这样:

 public class Generics<T>{ 
    private T t; 
    ... 

    public <R> R getResult(){ 
        ... 
    } 
 } 

Kotlin 的写法呢?完全一样:

 class Generics<T>{ 
    private val t: T 
    ... 

    fun <R> getResult(): R{ 
        ... 
    } 
 } 

Java/Kotlin 的泛型实现采用了类型擦除的方式,这与 C# 的实现不同,后者是真·泛型,前者是伪·泛型。当然这么说是从运行时的角度来看的,在编译期,Java 的泛型对于语法的约束也是真实存在的,所以你愿意的话,也可以管 Java 的泛型叫做编译期真·泛型。

那么什么是真·泛型呢?我们给大家看一段 C# 的代码:

 using System; 

 public class Program{ 
    public static void Main(String[] args){ 
        testGeneric<string>(); 
    } 

     public static void testGeneric<T>(){ 
         Console.WriteLine(typeof(T)); 
     } 
 } 

testGeneric 的泛型参数 string 可以在运行时获取到,俨然一个真实可用的类型啊。下面是输出的结果:

 System.String 

那伪·泛型呢?如果同样的代码放到 Java 或者 Kotlin 当中,结果会怎样呢?

 public static <T> void testGenerics(){ 
     System.out.println(T.class); 
 } 

这段代码无法编译,因为 T 是个泛型参数,你不能用它去获取 class 对象。为了更清楚地说明问题,我们看下下面的代码:

 public static <T> T testGenerics(){ 
     T t = null; 
     return t; 
 } 

编译后的字节码:

   public static testGenerics()Ljava/lang/Object; 
    L0 
     LINENUMBER 13 L0 
     ACONST_NULL 
     ASTORE 0 
    L1 
     LINENUMBER 14 L1 
     ALOAD 0 
     ARETURN 
    L2 
     LOCALVARIABLE t Ljava/lang/Object; L1 L2 0 
     // signature TT; 
     // declaration: T 
     MAXSTACK = 1 
     MAXLOCALS = 1 

我们看到,编译之后 T 变成了 Object,简单来说就相当于:

 public static Object testGenerics(){ 
     Object t = null; 
     return t; 
 } 

这就是传说中的类型擦除了。而 Kotlin 在 JVM 之上,编译之后也是字节码,机制与 Java 是一样的。也正是因为这个原因,我们在使用 Gson 反序列化对象的时候除了制定泛型参数,还需要传入一个 class :

 public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException { 
    ... 
 } 

显然 Gson 没有办法根据 T 直接去反序列化。

下面我们说一点儿不太一样的。在 Kotlin 当中有一个关键字叫做 reified,还有一个叫做 inline,后者可以将函数定义为内联函数,前者可以将内联函数的泛型参数当做真实类型使用,我们先来看例子:

 inline fun <reified T> Gson.fromJson(json: String): T{ 
     return fromJson(json, T::class.java) 
 } 

这是一个 Gson 的扩展方法,有了这个之后我们就无须在 Kotlin 当中显式的传入一个 class 对象就可以直接反序列化 json 了。

这个会让人感觉到有点儿迷惑,实际上由于是内联的方法调用,T 的类型在编译时就可以确定的:

 class Person(var id: Int, var name: String) 

 fun test(){ 
     val person: Person = Gson().fromJson("""{"id": 0, "name": "Jack" }""") 
 } 

反编译之后:

 public static final void test() { 
   Gson $receiver$iv = new Gson(); 
   String json$iv = "{\"id\": 0, \"name\": \"Jack\" }"; 
   Person person = (Person)$receiver$iv.fromJson(json$iv, Person.class); 
 } 

注意,在这里,inline 是必须的。

2. 型变

2.1 Java 的型变

如果 Parent 是 Child 的父类,那么 List<Parent> 和 List<Child> 的关系是什么呢?对于 Java 来说,没有关系。

也就是说下面的代码是无法编译的:

 List<Number> numbers = new ArrayList<Integer>(); //ERROR! 

不过 numbers 中可以添加 Number 类型的对象,所以我添加个 Integer 可以不呢?可以的:

 numbers.add(1); 

那么我要想添加一堆 Integer 呢?用 addAll 是吧?注意看下 addAll 的签名:

 boolean addAll(Collection<? extends E> c); 

这个泛型参数又是什么鬼?如果我把这个签名写成下面这样:

 boolean addAll(Collection<E> c); 

我想要在 numbers 当中 addAll 一个 ArrayList<Integer>,那就不可能了,因为我们说过,ArrayList<Number> 和 ArrayList<Integer> 是两个不同的类型,毛关系都没有。

? extends E 其实就是使用点协变,允许传入的参数可以是泛型参数类型为 Number 子类的任意类型。

当然,也有 ? super E 的用法,这表示元素类型为 E 及其父类,这个通常也叫作逆变。

2.2 Kotlin 的型变

型变包括协变、逆变、不变三种。

下面我们看看 Kotlin 是怎么支持这个特性的。Kotlin 支持声明点型变,我们直接看 Collection 接口的定义:

 public interface Collection<out E> : Iterable<E> { 
    ... 
 } 

out E 就是型变的定义,表明 Collection 的元素类型是协变的,即 Collection<Number> 也是 Collection<Int> 的父类。

而对于 MutableList 来说,它的元素类型就是不变的:

 public interface MutableCollection<E> : Collection<E>, MutableIterable<E> { 
    ... 
    public fun addAll(elements: Collection<E>): Boolean 
    ... 
 } 

换言之,MutableCollection<Number> 与

MutableCollection<Int> 没有什么关系。

那么请注意看 addAll 的声明,参数是 Collection<E>,而 Collection 是协变的,所以传入的参数可以是任意 E 或者其子类的集合。

逆变的写法也简单一些: Collection<in E>

那么 Kotlin 是否支持使用点型变呢?当然支持。

我们刚才说 MutableCollection 是不变的,那么如果下面的参数改成这样:

 public fun addAll(elements: MutableCollection<E>): Boolean 

结果就是,当 E 为 Number 时,addAll 无法接类受似 ArrayList<Int> 的参数。而为了接受这样的参数,我们可以修改一下签名:

 public fun addAll(elements: MutableCollection<out E>): Boolean 

这其实就与 Java 的型变完全一致了。

2.3 @UnsafeVariance

型变是一个让人费解的话题,很多人接触这东西的时候一开始都会比较晕,我们来看看下面的例子:

 class MyCollection<out T>{ 
     fun add(t: T){ // ERROR! 
        ... 
     } 
 } 

为什么会报错呢?因为 T 是协变的,所以外部传入的参数类型如果是 T 的话,会出问题,不信你看:

 var myList: MyCollection<Number> = MyCollection<Int>() 
 myList.add(3.0) 

上面的代码毫无疑问可以编译,但运行时就会比较尴尬,因为 MyCollection<Int> 希望接受的是 Int,没想到来了一个 Double。。

对于协变的类型,通常我们是不允许将泛型类型作为传入参数的类型的,或者说,对于协变类型,我们通常是不允许其涉及泛型参数的部分被改变的。这也很容易解释为什么 MutableCollection 是不变的,而 Collection 是协变的,因为在 Kotlin 当中,前者是可被修改的,后者是不可被修改的。

逆变的情形正好相反,即不可以将泛型参数作为方法的返回值。

但实际上有些情况下,我们不得已需要在协变的情况下使用泛型参数类型作为方法参数的类型:

 public interface Collection<out E> : Iterable<E> { 
    ... 
     public operator fun contains(element: @UnsafeVariance E): Boolean 
    ... 
 } 

比如这种情形,为了让编译器放过一马,我们就可以用 @UnsafeVariance 来告诉编译器:“我知道我在干啥,保证不会出错,你不用担心”。

最后再给大家提一个点,现在你们知道为什么 in 表示逆变,out 表示协变了吗?

3. 通配符

在Java 中,当我们不知道泛型具体类型的时候可以用 ?来代替具体的类型来使用,比如下面的写法:

 Class<?> cls = numbers.getClass(); 

Kotlin 也可以有类似的写法:

 val cls: Class<*> = list.javaClass 
 val cls2: Class<*> = List::class.java 

Kotlin 可以根据 * 所指代的泛型参数进行相应的映射,下面是官方的说法:

  • 对于 Foo <out T>,其中 T 是一个具有上界 TUpper 的协变类型参数,Foo <*> 等价于 Foo <out TUpper>。 这意味着当 T 未知时,你可以安全地从 Foo <*> 读取 TUpper 的值。

  • 对于 Foo <in T>,其中 T 是一个逆变类型参数,Foo <*> 等价于 Foo <in Nothing>。 这意味着当 T 未知时,没有什么可以以安全的方式写入 Foo <*>

  • 对于 Foo <T>,其中 T 是一个具有上界 TUpper 的不型变类型参数,Foo<*> 对于读取值时等价于 Foo<out TUpper> 而对于写值时等价于 Foo<in Nothing>

那么 * 在哪些场合下可以或者不可以使用呢?

我们来看几个例子:

 val list = ArrayList<*>()// ERROR! 

* 不允许作为函数和变量的类型的泛型参数!

 fun <T> hello(args: Array<T>){ 
     ... 
 } 

 ... 
 hello<*>(args) // ERROR!! 

* 不允许作为函数和变量的类型的泛型参数!

 interface Foo<T> 

 class Bar : Foo<*> // ERROR! 

* 不能直接作为父类的泛型参数传入!

 interface Foo<T> 

 class Bar : Foo<Foo<*>> 

这是正确的。注意,尽管 * 不能直接作为类的泛型参数,Foo<*> 却可以,按照前面官方给出的说法,它在读时等价于Foo<out Any> 写时等价于 Foo<in Nothing>

 fun hello(args: Array<*>){ 
     ... 
 } 

同样,这表示接受的参数的类型在读写时分别等价于Array<out Any> 和 Array<in Nothing>

4. 其他

4.1 Raw 类型

Raw 类型就是对于定义时有泛型参数要求,但在使用时指定泛型参数的情况,这个只在 Java 中有,显然也是为了前向兼容而已。

例如:

 List list = new ArrayList(); 

这类用法在 Kotlin 当中是不被允许的。上面的代码大致相当于:

 val list = ArrayList<Any?>() 

不过,在 Java 中,raw 类型可以有这种写法:

 List<Integer> integers = new ArrayList<>(); 
 List list = new ArrayList(); 
 list = integers; 

但 Kotlin 中,单纯的 ArrayList<Any?> 并不是协变的,所以下面的写法是错误的:

 var list = ArrayList<Any?>() 
 val integers = ArrayList<Int>() 
 list = integers // ERROR! 

Java,你这样做很危险呀。

4.2 泛型边界

在 Java 中,我们同样可以用 extends 为泛型参数指定上限:

 class NumberFormatter<T extends Number>{ 
    ... 
 } 

这表示使用时,泛型参数必须是 Number 及其子类的一种。

而在 Kotlin 中,写法与继承类似:

 class NumberFormatter<T: Number>{ 
    ... 
 } 

如果有多个上界,那么:

 class NumberFormatter<T> where T: Number, T: Cloneable{ 
    ... 
 } 

5. 小结

通过上面的讨论,其实大家会发现 Kotlin 的泛型相比 Java 有了更严格的约束,更简洁的表述,更灵活的配置,但背后的思路和具体的实现总体来说是一致的。

小编整理了一些关于面试类的文章,含有从基础到进阶。含有BATJ.字节跳动面试专题,算法专题,高端技术专题,混合开发专题,java面试专题,Android,Java小知识,到性能优化.线程.View.OpenCV.NDK等应有尽有。还有辅之相关的视频+学习笔记

话不多请看下图↓↓↓ (把所有的知识+每日一题整理了)

进扣裙回复【333】获取完整资料

 

 

Vue3结合vue-office实现Word文档上传与预览全流程解析 本文详细解析了如何在Vue3项目中集成vue-office插件实现Word文档上传与预览功能。从项目初始化、插件安装到文件上传处理(包括ArrayBuffer转换)、预览优化及常见问题解决方案,提供了一套完整的实现流程,帮助开发者快速实现高效的文档预览功能。 阅读详情

相关推荐

大模部署指南:LM Studio

本文介绍使用LM Studio部署大模

举世誉之而不加劝,举世非之而不加沮,定乎内外之分,辩乎荣辱之境,斯已矣。 1906

Kotlin(十四)

kotlin中的类、函数、协变、逆变。

牛永志的专栏 1116

matlab语音识别系统(源代码)高分大作业&毕业设计

matlab语音识别系统(源代码)高分大作业&毕业设计,含有代码注释,新手也可看懂,个人手打98分项目,导师非常认可的高分项目,毕业设计、期末大作业和课程设计高分必看,下载下来,简单部署,就可以使用。matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统(源代码)高分大作业&毕业设计matlab语音识别系统源码。

如何获取的class_Kotlin 实化

实化Kotlin 中内联函数中的代码会在编译的时候自动被替换到调用它的地方,这样也就不存在擦除的问题,因为代码在编译之后会直接使用实际的类型来替代内联函数中的声明,所以,Kotlin 中是可以将内联函数中的进行实化的。实化有两个条件:函数必须是内联函数,就是用 inline 关键字来修饰的函数在声明的地方必须加上 reified 关键字来表示该要进行实化示例如...

weixin_39561004的博客 4740

Kotlin使用反射获取信息

在java中,使用反射的代码实例如下: class Student { String name; Integer age; public Student(String name, Integer age) { this.name = name; this.age = age; } public String getN...

Lud的博客 6195

kotlin获取集合的类型信息

关键字和内联函数来实现。

yufumatou的博客 951

Kotlin

Kotlin摘要:Kotlin支持类型参数化,分为声明处变和使用处变,包含协变(out)、逆变(in)和不变三种关系。协变保持父子关系,逆变反转父子关系,默认是不变关系。可用于类、函数,支持类型边界约束和多重约束(where)。Kotlin通过reified关键字和inline函数解决类型擦除问题,实现运行时类型保留。生产者/消费者模建议:输出用out,输入用in,同时作为参数和返回值时可用@UnsafeVariance解决冲突。星投影(*)表示接收任意类型

xiangxiongfly 2131

干货详解:kotlin(4)-java转kotlin潜规则

作为一名即将求职的程序员,面对一个可能跟近些年非常不同的 2019 年,你的就业机会和风口会出现在哪里?在这种新环境下,工作应该选择大厂还是小公司?已有几年工作经验的老兵,又应该如何保持和提升自身竞争力,转被动为主动?就目前大环境来看,跳槽成功的难度比往年高很多。一个明显的感受:今年的面试,无论一面还是二面,都很考验Java程序员的技术功底。最近我整理了一份复习用的面试题及面试高频的考点题及技术点梳理成一份“Java经典面试问题(含答案解析).pdf和一份网上搜集的“Java程序员面试笔试真题库.pdf。

m0_60567881的博客 1140

java:数字转string 报空指针_超干货详解:kotlin(4) java转kotlin潜规则

前言以一个java老鸟的角度,如何去看 kotlin。 Java源代码应该如何用Kotlin重构。 如何正确学习kotlin并且应用到实际开发中。本文将会探究。本文分两大块,重难点和潜规则。重难点:Kotlin中可以独立出来讲解的大块知识点。提供单独Demo。这部分大多数是Kotlin开创的新概念(相比于Java)。潜规则:Kotlin是谷歌用来替换Java的,它和java百分百完全兼容,但是实际...

weixin_39645343的博客 282

Kotlin将继续助力Android开发,作为正在开发的你了解过哪些,附Android面经

四、高阶函数 高阶函数的基本概念 常见高阶函数(forEach,map,flatmap,fold,reduce,filter,takeWhile,let,apply,with,use) 尾递归优化 (tailrec) 闭包(函数式编程的福音) 函数复合(f(g(x)),infix) 柯里化(Currying)-函数调用链 偏函数 五、Lambdas编程 lambdas简介 lambdas和集合 lambda表达式 访问作用域中的变量 成员引用 集合的函数式API之filter和map all、any、

m0_66264938的博客 1936

探秘Gank:优雅的AndroidKotlin干货神器

探秘Gank:优雅的AndroidKotlin干货神器 Gank干货集中营 app 安卓实现,基于 RxFlux 架构使用了 RxJava、Retrofit、Glide、Koin等项目地址:https://gitcode.com/gh_mirrors/gan/Gank 项目介绍 如果你是一位热衷于学习新技术的Android开发者,那么【Gank】绝对不容错过。这是一款专为Android和Kot...

gitblog_00025的博客 409

kotlin可选参数,全靠这份Android知识点PDF大全

由于本文罗列的知识点是根据我自身总结出来的,并且由于本人水平有限,无法全部提及,欢迎大神们能补充~将来我会对上面的知识点一个一个深入学习,也希望有童鞋跟我一起学习,一起进阶。提升架构认知不是一蹴而就的,它离不开刻意学习和思考。**这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家,**梳理了多年的架构经验,筹备近1个月最新录制的,相信这份视频能给你带来不一样的启发、收获。!《安卓学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》浏览器打开。

m0_61331573的博客 950

Spring Boot Java 项目中诡异的 Kotlin NoClassDefFoundError 深度解析

本文从一次真实生产启动故障出发,系统性分析了 Java 项目中引入 Kotlin 相关依赖后触发 NoClassDefFoundError 的根本原因。核心结论是:问题并不来自业务代码,而是 Spring Boot 自动装配机制与 Jackson Kotlin 模块之间的隐式耦合,再叠加老版本依赖生态与新组件之间的年代不兼容,最终导致启动阶段直接失败。

曾经“等你生日那天”都遥远得像未来,如今却可欢愉的挥手说“下个十年见” 4万+

Kotlin版 极简 干货集中营 APP

项目地址 首先这是我的一个Kotlin练手项目,规模很小,显示干货集中营的几个分类模块,使用的还是我习惯使用的java框架,虽然遇到了一些问题,不过提高了Kotlin的熟练度,踩了一些Kotlin的坑。 上效果图 项目说明 项目比较简单,主界面是TabLayout&amp;ViewPager&amp;Fragment,Fragment懒加载模式,Fragment里就一个Recycl...

z979451341的博客 1448

KotlinKotlin For Gank.io (干货集中营Kotlin实现)

把之前的项目用Kotlin重构一下 原项目地址:https://github.com/onlyloveyd/GankIOClient 对应Kotlin版:https://github.com/onlyloveyd/GankIOWithKotlin

AndroidKt 1677

Kotlin数据类型(一:数据类型

一、Boolean /** * Boolean类型有两种类型的 * true * flase */ val a: Boolean = true val b: Boolean = false 二、Number数据类型 package net.println.kotlin /** * @author:wangdong * @description: */ /** *...

向小凯同学学习 1478

kotlin t class.java_如何在Kotlin获取参数类

对于具有参数T的类,您无法执行此操作,因为您没有T的类型信息,因为JVM会删除类型信息 . 因此这样的代码不起作用:class Storage {val snapshot: Snapshot? = ...fun retrieveSomething(): T? {return snapshot?.getValue(T::class.java) // ERROR "only classes can...

weixin_28859781的博客 1678

八、Kotlin 反射

反射是允许在运行时期访问程序结构的一类特性(程序结构包括:类、接口、方法、属性等)。

zengkaiqiang562的博客 1589

Kotlin——初级篇(三):数据类型详解

任意一种开发语言都有其数据类型,并且数据类型对于一门开发语言来说是最基本的构成,同时也是最基础的语法。当然,Kotlin也不例外。Kotlin的数据类型和Java是大致相同的,但是他们的写法不同,并且类型之间的转换也存在着差异。下面为大家详细说明并举例。 目录 一、数值类型 1、Kotlin中的数字的内置类型(接近与Java),其关键字为: Byte => 字节 => 8位 Short => 短整 => 16位 Int => 整 => 32位 Long =&gt.

李杭坤的博客 954

Kotlin 学习

class TypedClass<T>(parameter:T) { var value:T=parameter } fun main(args:Array<String>) { val t1=TypedClass<String>("Hello World") val t2=TypedClass<Int>(1231) println("String:${t1.value}") println("Int:${t2.va

weixin_37077736的博客 326

Kotlin - <T>

用处:消除类型强转的烦恼,保证类型安全。 约束 形变 & 协变 Kotlin Java in T 逆变 ? super T 下限通配符 out T 协变 ? extends T 上限通配符 ...

HugMua的博客 4584
上一篇: 当上阿里面试官后,才知道原来简历这么写
下一篇: 理解Android硬件加速原理的小白文
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值