Kotlin基础篇

277 阅读8分钟

为个人《朱涛·Kotlin编程第一课》笔记

Kotlin到Java的思维转变

  • 不变性思维
  • 空安全思维
  • 表达式思维
  • 函数思维
  • 协程思维

1. 基础语法

Kotlin基础语法改进

  • 万物皆对象(Int、Float等)
  • 支持类型推导
  • 不需要分号
  • 字符串模板
  • 原始字符串
  • 表达式函数
  • 函数默认值
  • if和when可以作为表达式

Kotlin语言层面改进

  • 区分“可空变量类型”和“不可空变量类型”
  • 推崇不可变性val
  • 不支持隐式类型转换
  • 数组与集合访问行为统一
  • 函数调用支持命名参数
  • when表达式

Kotlin将许多设计模式和开发中常见会因为疏忽出现的错误的解决方法都融入到语言层面中

1.jpg

2. 面向对象

面向对象的改变

  • 类默认public
  • 类继承语法和接口实现语法相同
  • 类默认封闭(final)
    • 默认不可继承,除非open修饰
    • open修饰的普通类,内部方法和属性默认不可重写,除非也被open修饰
  • 接口可以有成员属性和默认实现
  • 嵌套类默认静态,使用inner关键字标识可以解除
  • 数据类
    • 解构声明:一个对象赋值给多个变量,重写componentN()实现,常用于函数返回两个变量、遍历映射
  • 密封类
    • 有固定数目子类的类,结合when表达式的分支晚辈特性更加方便

自定义getter和setter:...

2.jpg

3. Kotlin原理

问:Kotlin在语法层面摒弃了原始类型,但有时候为了性能考虑需要使用原始类型,此时怎么办?

答:会根据性能和使用自动选择转换为包装类还是原始类型,比如使用了方法或者被赋值了null,会转换为包装类

问:接口的成员属性是Kotlin特有的,Java的接口方法的默认实现是Java 1.8之后支持的,为了兼容Java 1.6,Kotlin了做了哪些事情

答:通过Kotlin字节码反编译为Java代码可以看出,Kotlin接口成员属性会转换为一个抽象方法,接口方法的默认实现会生成一个静态类中,静态类中会生成一个同名方法,参数为接口的类型,方法内为默认实现的逻辑。在对接口成员属性的实现,会生成一个同名的属性,在接口属性转换成的方法中返回这个属性

// Kotlin版本
interface Behavior {
    val canWalk: Boolean    // 1
    
    fun walk() {
        // 2
        if (canWalk) {
            println(canWalk)
        }
    }
}

class Man : Behavior {
    override val canWalk: Boolean = true  // 3
}
// 转换后的Java代码
public interface Behavior {
    boolean getCanWalk();     // 1
    
    void walk();
    
    public static final class DefaultImple {
        public static void walk(Behavior $this) {
            // 2
            if ($this.getCanWalk()) {
                System.out.println($this.getCanWalk());
            }
        }
    }
}

public final class Man implements Behavior {
    private final boolean canWalk = true;
    
    public boolean getCanWalk() {     // 3
        return canWalk;
    }
    
    public void walk() {
        Behavior.DefaultImpls.walk(this);
    }
}

3.jpg

4. object关键字

object关键字使用场景:匿名内部类、单例模式、伴生对象

以上三种情况的本质都是生成一个对象

匿名内部类

new Thread(object : Runnable() {...} )

可以在继承一个抽象类的同时,继承多个接口,可以看作创建了一个object的类,去继承抽象类和实现接口

单例模式

object修饰的类,使用和class一样

调用内部方法的方式和调用静态方法类似(类名.方法名),一般作为工具类使用,但底层还是单例模式

伴生对象

class A {
    compaion object [Classname] {
        ...
    }
}

本质是在宿主类中创建了一个伴生对象,伴生对象内部的方法属性的调用和静态方法一样,但其和单例模式都不是真正的静态方法,Kotlin要实现静态方法可以使用顶层方法或者@JvmStatic注解实现

4.jpg

5. 扩展

fun ReceiverClass.function(para: Type) {...}

扩展的本质是创建一个静态方法,然后传入接收者类型的参数

主要用于替换工具类

扩展的限制

  • 扩展无法被重写(本质上并不在接收者类内)
  • 扩展属性无法存储状态(本质上是静态方法)
  • 扩展作用域有限(只能使用定义处和接收者类型公开属性和方法)
  • 无法访问私有成员

5.jpg

6. 高阶函数

将函数的参数类型和返回值类型抽象出来就得到了函数类型

高阶函数:参数或返回值是函数类型的函数

函数的引用:val fun: (Int, Int) -> Float = ::addfun是一个函数类型,接收的::add就是对add函数的引用,本质上根据函数内容生成一个函数对象,然后进行赋值

函数对象:FunctionX接口的实现对象(X:0~22,表示从无参到22个参数的函数类型),函数内部实现逻辑会在接口方法invoke()中实现,以上一条定义中的变量fun为例,使用fun(1, 1)调用函数,是Kotlin的语法糖,本质上是调用函数对象的invoke()

Lambda表达式:...

函数式接口:单抽象方法接口、SAM,只有一个抽象方法的接口,作为参数可以直接传入lambda表达式

Lambda表达式的一些书写简化:...

实现原理:高阶函数接收的是一个FunctionX接口的参数,在调用高阶函数的时候,会根据lambda表达式或匿名函数创建一个函数对象传入高阶函数

个人理解:高价函数目前只是可以使得代码可读性增强,对性能等没有太大优化

inline、noinline、crossinline

  • inline相当于将inline函数内容直接拷贝到使用处,提高效率
    • 官方建议只在高阶函数上使用inline,普通方法上使用效率提高不明显,而且由于直接拷贝到使用处,所以私有方法不能使用
    • 根据高阶函数的实现原理,每次我使用高阶函数的时候都会创建一个FunctionX接口的实现对象,如果在循环或者多次调用高级函数的情况下,会多次创建对象,而这些对象用一次便会删除(可以理解为类似匿名内部类),此时使用inline会减少创建对象的损耗
    • 官方文档中也有一个inline的用法,使用inline调用Java的静态函数(比如Math.min()),目的就是给函数“重命名”,起到宏定义的作用,此时inline修饰的多是普通函数
  • noinline:修饰参数,局部关掉inline优化,解决不能把函数类型的参数当对象使用的限制
    • 高阶函数的实现原理是传入一个函数对象,inline修饰时,直接将函数内容拷贝到使用处,便没有生成这个函数对象,但当我们inline需要返回其中一个函数对象的时候,需要使用noinline,使得这个函数依旧以对象的形式传入noline修饰的参数
  • crossinline:修饰参数,局部加强优化,使得内联函数的函数类型的参数可以被简洁调用,但不能在lambda表达式中使用return
  • 可关注扔物线视频 Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?

6.jpg

7. 函数式编程思想

Kotlin Standard-library

Kotlin标准库

函数式对比命令式

  • 函数式就是使用内置函数来完成需求
  • 只需要声明我们想要什么,不需要关心底层实现
  • 代码更简洁,可读性更高

函数式编程

  • 函数是一等公民
    • 函数可以独立于类之外(顶层函数)
    • 函数可以作为参数和返回值(lambda)
    • 函数可以向变量一样(函数引用)
    • 函数功能强大,使用函数可以解决大部分问题
  • 纯函数
    • 不变性(对函数作用域外的数据不进行修改)
    • 幂等性
    • 引用透明
    • 无状态

8. 委托

委托模式

  • 不属于23中设计模式,是一种面向对象设计模式
  • 持有被委托人引用
  • “老板下达给经理任务,经理委托员工去做”
  • 和代理模式区别:不关心过程,只关心结果

委托属性

Kotlin委托类委托的是接口方法,而委托属性委托的是属性的getter、setter

包含标准委托

标准委托

  • 直接委托:两个属性建立委托关系,两者的getter和setter就绑定在一起了,一般用于版本迭代造成的属性名不同
  • 懒加载委托:使用lazy{}实现懒加载
  • 观察者委托:使用Delegates.observable(n){},传入参数n为属性初始值,当属性值发生改变的时候便会回调到lambda表达式中,lambda接收三个参数:属性本身、旧值和新值
  • 映射委托:将属性委托给一个map,通过映射关系给属性进行赋值
    • 被委托的map中存在键值对name:Cindy就会给委托的name属性赋值Cindy

自定义委托

// 标准写法
class Owner {
    var text: String by StringDelegate()
}
class StringDelegate(private var s: String = "Hello") {
// 1.运算符重载             2.被委托属性所属类或其父类          3.被委托属性类型
//     ↓                             ↓                               ↓
    operator fun getValue(thisRef: Owner, property: KProperty<*>): String {
        retunrn s
    }
    
    operator fun setValue(thisRef: Owner, property: KProperty<*>, String value) {
        s = value
    }
}

// 借助Kotlin的ReadWriteProperty或ReadOnlyProperty接口(被委托属性为val时使用ReadOnlyProperty)
//                                                                          2       3
//                                                                          ↓       ↓
class StringDelegate1(private var s: String = "Hello"): ReadWriteProperty<Owner, String> {
    // 内部getValue和setValue快捷键生成
}

提供委托

在委托属性的同时进行一些额外的逻辑判断,使用provideDelegate实现

class SmartDelegate {
    operator fun provideDelegate(
        thisRef: Owner,
        prop: KProperty<*>
    ): ReadWriteProperty<Owner, String> {
        // 根据变量名判断输出
        return if (prop.name.contains("log")) {
            StringDelegate1("log")
        } else {
            StringDelegate1("normal")
        }
    }
}

class Owner {
    var normalText: String by SmartDelegate()
    var logText: String by SmartDelegate()
}

委托实际应用

  1. 属性的可见性封装
class Model {
    val data: List<String> by ::_data
    private val _data: MutableList<String> = mutableListOf()
    ...
}
  1. 数据与View绑定
// 给控件扩展一个provideDelegate函数,将数据委托给控件实现两者绑定
var message: String? by textView
  1. ViewModel委托
private val mainViewModel: MainViewModel by viewModels()

7.jpg

9. 泛型

泛型型变

  • 使用处型变:在调用位置使用
fun function(trans: Transformer<in Student>) {...}
  • 声明处型变:在泛型源码处修改
class Transformer<in T> {...}
  • 逆变
    • 简单来讲就是父类泛型时子类泛型的子类,使用关键字in
    • 例:Transformer<Person>Transformer<Student>的子类
  • 协变
    • 简单来说就是父类泛型是子类泛型的父类,使用关键字out
    • 例:Transformer<Person>Transformer<Student>的父类

Java与Kotlin的型变:Java只有使用处型变

使用出协变使用处逆变
Kotlin<out Person><in Person>
Java<? extends Person><? super Person>

协变和逆变的使用场景

Consumer in, Produce out

对于逆变,泛型T会以函数参数的形式被传入函数,往往是一种写入行为,使用in

对于协变,泛型T会以函数返回值的形式被传出函数,往往是一种读取行为,使用out

注意:有时候传入不一定意味着写入

// 官方List部分源码
public interface List<out E> : Collection<E> {
    public operator fun get(index: Int): E
//                          这个注解使编译器忽略这个型变冲突
//                                        ↓
    override fun contains(element: @UnsafeVariance E): Boolean
    
    public fun indexOf(element: @UnsafeVariance E): Int
}

contains()indexOf()不会修改泛型的值,没有写入操作,所以可以作为参数

还有一些特殊情况,比如有时候被val修饰的参数不会被修改也可以使用out修饰,或者private修饰的属性不会生成getter和setter,也可以使用协变或者逆变

星投影

当我们对泛型具体类型不感兴趣的时候,直接传入一个*作为泛型的实参,对应Java中的<?>

Java中的<?>相当于<? extends Object>,Kotlin的<*>相当于<out Any>(如果类型声明中有了上界,那么在使用处的<*>这个上界依旧有效)

关于泛型的上界,Java中使用extends后接上界,多重上界使用&连接,Kotlin中使用:,多重上界的时候这样书写class MyList<T> where T : Number, T: String {}

8.jpg

10. 注解

自定义注解

// 手写废弃追加二Deprecated
//元注解
// ↓
@Target(CLASS, FUNCTION,...)
@MustBeDocumented
public annotation class Deprecated (
    val message: String,
    val replaceWith: ReplaceWith = ReplaceWith(""),
    val level: DeprecationLevel = DeprecationLevel.WARNING
)

// 注解的使用
@Deprecated(
    message = "Use CalculatorV3"
    replaceWith = ReplaceWith("CalculatorV3)
    level = DeprecationLevel.ERROR
)
class Calculator {...}

class CalculatorV3 {...}
// 在使用Calculator时,编译器会报错,显示message信息,快捷键修改,自动替换成CalculatorV3

// 精确目标
object Singleton {
//精确标注注解@Inject作用在setter上
//   ↓
    @set:Inject
    lateinit var person: Person
}

精确目标:file、property、field、get、set、receiver(作用于扩展的接收者参数)、param(作用于构造函数参数)、setparam、delegate(作用域委托字段)

元注解

可以修饰其他注解的注解

  • Target 指定注解可以修饰的位置
public enum class AnnotationTarget {
    CLASS,              类、接口、object、注解类
    ANNOTATION_CLASS,   注解类
    TYPE_PARAMETER,     泛型参数
    PROPERTY,
    FIELD,              字段、幕后字段
    LOCAL_VARIABLE,     局部变量
    VALUE_PARAMETER,    函数参数
    CONSTRUCTOR,
    FUNCTION,
    PROPERTY_GETTER,
    PROPERTY_SETTER,
    TYPE,               类型
    EXPRESSION,
    FILE,               文件
    TYPEALIAS           类型别名
}
  • Retention 指定注解是否编译后可见、是否运行时可见
public enum class AnnotationRetention {
    SOURCE,     只存在于源码,编译后不可见
    BINARY,     编译后可见,运行时不可见
    RUNTIME     编译后可见,运行时可见
}
  • Repeatable 允许在同一个地方,多次使用相同的被修饰的注解
  • MustBeDocumented 指定注解应该包含在生成的API文档中显示

11. 反射

  • 感知:反省自己当前状态
  • 修改:主动改变自己的状态
  • 调整:根据状态做出不同的决策
// readMembers可以读取参数所有成员属性的名称和值
fun readMembers(obj: Any) {
//   类引用,获取KClass对象
//      ↓
    obj::class.memberProperties.forEach {
//                                     KProperty1类型
//                                          ↓
        println("${obj::class.simpleName}.${it.name} = ${it.getter.call(obj)}")
    }
}

// modifyMember改变属性值,以属性address为例
fun modifyMember(obj: Any) {
    obj::class.memberProperties.forEach {
        if (it.name == "address" &&     // 判断属性名
            it is KMutableProperty1 &&  // 判断属性是否可变,var修饰的属性类型为KMutableProperty1
            it.setter.parameters.size == 2 &&   // 判断setter的参数是否为两个
            it.getter.returnType.classifier == String::class) { // 根据getter的返回值类型判断属性是否为String类型
                it.setter.call(obj, "China")
            }
    }
}

反射关键的API和类

  • KClass代表一个Kotlin类
重要成员类型作用
simpleName
qualifiedName完整类名
membersCollection<KCallable<*>>
constructorsCollection<KFunction<T>>
nestedClassedCollection<KClass<*>>所有嵌套类
visibilityKVisibility?PUBLIC、PROTECTION、INTERNAL【1】、PRIVATE
isFinal、isOpen、isAbstract、isSeale
isData、isInner、isCompanion、isFun这么分开纯粹因为好看,没有别的意义
isValue(是否为Value Class【2】)
  • KCallable代表Kotlin中所有可调用的元素(函数、属性、构造函数)
重要成员类型作用
name
parametersList<KParameter>
returnTypeKType
typeParametersList<TypeParameter>所有类型参数(比如泛型)
call()KCallable对应的调用方法,第一个参数永远式对象本身
visibility、isSuspend
  • KParameter代表了KCallable中的参数
重要成员作用
index参数索引位置
name、type
kind参数种类(INSTANCE对象实例、EXENSION_RECEIVER扩展接收者、VALUE实际参数值)
  • KType代表了Kotlin中的类型
重要成员类型作用
classifierKClass对应Kotlin类
arguments类型的类型参数(也就是泛型参数)
isMarkedNullable是否标记为可空类型

【1】可见性的internal,只有这个模块可以调用,Kotlin的可见性没有default

【2】关于value class可见链接 Kotlin 宣布一个重磅特性

Java反射更接近底层,所以效率比Kotlin更高,但Kotlin对反射进行了自己的封装,实现了一些特有的功能,比如isData判断数据类等

12. 实现一个网络请求框架KtHttp

准备工作

// 数据类描述服务器返回内容
data class RepoList(
    var count: Int?,
    var items: List<Repo>?,
    var msg: String?
)

data class Repo(
    var added_stars: String?,
    var avatars: List<String>?,
    var desc: String?,
    var forks: String?,
    var lang: String?,
    var repo: String?,
    var repo_link: String?,
    var stars: String?
)

// 自定义注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class GET(val value: String)

@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.RUNTIME)
annotation class Field(val value: String)

// 定义一个网络请求接口
interface ApiService {
    @GET("/repo")
    fun repos(
        @Field("lang") lang: String,
        @Field("since") since: String
    ): RepoList
}

// 主要流程
fun main() {
    // 动态代理船舰ApiService实例
    val api: ApiService = KtHttpV1.create(ApiService::class.java)
    // 传入参数
    val data: RepoList = api.repos(lang = "Kotlin", since = "weekly")
    println(data)
}

主要流程就是将我们网络请求接口参数与基础URL进行拼接,得到完整的URL进行访问

使用Java思维编写1.0版本

// KtHttpV1
object KtHttpV1 {
    // 借助OkHttp和GSON两个第三库实现类似Retrofit的功能
    private var okHttpClient = OkHttpClient()
    private var gson = Gson()
    
    var baslURL = "https://baseurl.com"
    
    fun <T> create(service: Class<T>): T {
        // public static Object newProxyInstance(ClassLoader x, Class<?>[] interfaces, InvocationHandler h) {...}
        /* 
        public interfaceInvocationHandle {
            public Object invoke(Object proxy, Method method, Object[] args) throw Throwable;
        }
        */
        
        return Proxy.newProxyInstance(
            service.classLoader,
            arrayOf<Class<*>>(service)
            // 反射后的method,这里表示repo方法
            //       ↓   方法的参数列表
            //       ↓      ↓
        ) { proxy, method, args -> 
            val annotations = method.annotations()
            for (annotation in annotations) {
                if (annotation is GET) {
                    val url = baseURL + annotation.value
                    // lambda表达式的局部返回语句
                    //          ↓
                    return@newProxyInstance invoke(url, method, args!!)
                }
            }
            return@newProxyInstance null
        } as T
    }
    
    private fun invoke(path: String, method: Method, args: Array<Any>): Any? {
        if (method.parameterAnnotations.size != args.size)  return null
        var url = path
        //                       返回方法参数的注解(Annotation[][] 类型)
        //                       Annotation[i][j]表示第i个参数的第j个注解
        //                                       ↓
        val parameterAnnotations = method.parameterAnnotations
        //                        返回数组的有效下标范围
        //                                ↓
        for (i in parameterAnnotations.indices) {
            for (parameterAnnotation in parameterAnnotations[i]) {
                if (parameterAnnotation is Field) {
                    val key = parameterAnnotation.value
                    val value = args[i].toString()
                    if (!url.contains("?")) {
                        url += "?$key=$value"
                    } else {
                        url += "&$key=$value"
                    }
                }
            }
        }
        
        val request = Request.Builder()
            .url(url)
            .build()
        val response = okHttpClient.newCall(request).execute()
        
        val genericReturnType = method.genericReturnType      // 获取repos()返回值类型
        val body = response.body
        val json.body?.toString()
        val result = gson.fromJson<Any?>(json, genericReturnType)
        return result
    }
}

该代码只处理了一个简单情况的URL地址https://baseurl.com/repo?lang=ZN_ch&since=kotlin

函数式思维编写的2.0版本

// KtHttpV2
object KtHttpV2 {
    // 使用懒加载
    private val okHttpClient by lazy { OkHttpClient() }
    private val gson by lazy { Gson() }
    
    var baseURL = "https://baseurl.com"
    
    //   使用泛型实化
    //  ↓           ↓
    inline fun <reified T> create(): T {
        return Proxy.newProxyInstance(
            T::class.java.classLoader,
            arrayOf<Class<*>>(T::Class.java)
        ) { proxy, method, args ->
            return@newProxyInstance method.annotations
                .filterIsInstance<GET>()   // 筛选出GET注解,升级版的fiter{}
                .takeIf { it.size == 1 }   // 判断注解数量,这里相当于if
                ?.let { invoke("$baseURL${it[0].value}", method, args) }
        }
    }
    
    fun invoke(url: String, method: Method, args: Array<Any>): Any? =
        method.parameterAnnotations
            .takeIf { it.size == args.size }
            // 将参数注解与传入的参数值进行映射,本质式map的升级,多了一个index
            // 返回List<Pair<Array<Annotation>, Any>>?类型
            .mapIndexed { index, annotations -> Pair(annotations, args[index]) }
            // 高阶函数版的for循环,第二个参数为循环逻辑,返回paresUrl函数返回值类型String?
            ?.fold(url, ::paresUrl)
            ?.let { Request.Builder().url(it).build() }  // 返回Request?
            ?.let { okHttpClient.newCall(it).execute().body?.string() }  // 返回String?
            ?.let { gson.fromJson<Any?>(it, method.genericReturnType) }
            
    private fun paresUrl(acc: String, pair: Pair<Array<Annotation>, Any>) =
        pair.first                    // 访问对的第一个元素,返回Array<Annotation>  
            .filterInstance<Field>()  // 返回List<Field>
            .first()                  // 取出List第一个值
            .let { field ->
                if (acc.contains("?")) {
                    "$acc&${field.value}=${pair.second}"
                } else {
                    "$acc?${field.value}=${pair.second}"
                }
            }
}

Kotlin的函数式编程更适合集合操作,对于注解、反射相关场景,函数式编程范式可能就没有那么明显的便捷了

13. 表达式思维

在Kotlin中if、when、throw、try-catch都是表达式

类型系统

根类型AnyAny?Object

  • AnyAny?没有继承关系,但Any类型可以被赋值给Any?
  • 也就是可空类型可以看作是不可空类型的 “子类”
  • AnyObject不完全等价,Any没有wait()notfity()这样的方法
  • 将Java代码转换为Kotlin代码可以看到Object会被转换为Any?,而具体转换会因为注解@NotNull@Nullable变化

UnitVoidvoid

  • Voidvoid不是一回事,前者为Java的类
  • Unit更接近voidUnit有助于实现函数类型
  • Unit就是一个普通的单例类,只不过Kotlin编译器会对Unit作为返回值的函数,不需要return

Nothing

  • Nothing是Kotlin所有类型的子类型,也称为底类型(函数式编程)
  • throw表达式返回值就是Nothing类型的,可以被赋值给任意类型
  • Nothing构造函数私有,所以无法构造实例
  • 当一个表达式返回值式Nothing的,意味着后面语句不再被执行

Unit?Nothing?

  • Unit就需要return语句了,但没有什么应用场景,返回值只能是null、Unit单例和Nothing
  • Nothing?作为参数可以接收null和Nothing,但方法不会执行

1.jpg

表达式思维

Kotlin中大部分代码都是表达式,是可以产生返回值的

Kotlin的类型系统让大部分的语句都变成了表达式,同时也让无返回值的函数有了类型,补充了Java的类型系统,使得可以拥有函数类型

14. 不变性思维

  • 尽可能使用条件表达式消灭var

  • 数据类存储数据,消除可变性

data class Person(val name: String, val age: Int)
// 此处都是用的不可变变量
// 如果要修改其中的值,Kotlin更推崇使用数据类的copy()拷贝一份
fun changeUserName(Person: Person, newName: String) {
    person.copy(name = newName)
}
  • 尽可能对外暴露只读集合
  • 只读集合底层不一定是不可变的,要警惕Java代码中只读集合访问行为(Java没有只读集合)
  • val并不意味绝对不可变(委托、自定义getter)

15. 空安全思维

  • 警惕Kotlin和外界的交互
    • 平台类型:Java没有标注可空注解,不知道可不可以为空,在Kotlin会以平台类型表示,比如String!,如何处理平台类型
      • 对于工程中的Java源代码,尽可能为参数和返回值加上可空注解(与Kotlin交互的时候)
      • 对于工程中的Java SDK,可以在SDK与业务代码之间建立一个抽象层,对其进行封装
  • 绝不使用非空断言!!
  • 尽可能使用非空类型(借助lateinit和懒加载消灭可空类型)
  • 明确泛型的可空性

2.jpg