文章目录
- 前言
- 主要参考资料:
- 语法基础
- 修饰符
- class
- fun
- param
- 原生字符串
- 迭代器
- item ==in== array
- array==.indices==
- array==.withIndex()==
- array==.forEach { }==
- array.==forEachIndexed { }==
- Range条件
- 正向区间
- 逆向区间
- 步长
- 字符串
- 换行输出
- replace
- 等号
- 遍历
- 数值类
- → \rightarrow →
- Double格式化
- 集合类
- 类型
- 常用集合类
- 继承层次
- List
- 去重
- list.removeIf{ … }
- Map
- 创建
- 安全索引
- 函数式编程在集合类中的应用
- 映射函数 map、flatMap
- 过滤函数 filter
- 组合函数 zip、fold
- 生成指定序列
- 运算符重载
- 常用操作符
- 示例
- 在集合类中的使用
- 解构语法特性
- list
- 数据类
- 函数
- 可变参数
- lambda
- 背景
- 内联
- 背景
- 示例
- 未用内联
- 使用内联
- 注意
- 标准函数
- apply、also、let
- run、with
- takeIf、takeUnless
- 扩展函数
- 示例
- 实战
- 场景:
- 用法:
- 扩展属性
- 面向对象
- 声明
- 初始化顺序
- 继承
- 不可继承 —— 默认
- 可继承 —— open
- 实现
- object对象
- 对象声明
- 对象表达式
- 伴生对象
- 内部类 inner class
- 普通嵌套
- 内部嵌套
- 数据类 data class
- 枚举类 enum class
- 简单使用
- 进阶使用
- ❗❗注意
- 密封类 sealed class
- 泛型
- 简单使用
- 泛型类型约束
- 类型上界
- 类型擦除
- 擦除的基本过程
- 获取擦除的类型 —— 实化类型 reified
- in & out
- 综述
- 意义
- 示例
- 协变 out:生产
- 逆变 in:消费
- 不变:产 + 消
- 通配符
- 星投影
前言
以下仅记录了对我个人而言有需要的部分,并不详尽。
主要参考资料:
书籍:
《Kotlin从基础到实战》黑马程序员 ISBN:9787115494405
《Kotlin从入门到进阶实战》陈光剑 ISBN:9787302508724
语法基础
修饰符
class
符 | 说明 |
abstract | 抽象 |
final | 不可被继承 |
enum | 枚举 |
open | 可继承 |
annotation | 注解 |
sealed | 密封 |
data | 数据 |
fun
符 | 说明 |
tailrec | 尾递归 |
operator | 运算符重载 |
infix | 中缀 |
inline | 内联 |
external | 外部 |
suspend | 挂起协程 |
param
符 | 说明 |
vararg | 变长 |
noinline | 不内联 |
crossinline |
原生字符串
使用3对引号括起来,输出字符串中全部原有内容,不会发生转义。
fun main() {
val str = "hello \nworld!"
println(str)
}
迭代器
item in array
for (item in array) {
println(item)
}
array==.indices==
for (i in array.indices) {
println(array[i])
}
array==.withIndex()==
for ((index, value) in array.withIndex()) {
println("the element at ${index} is ${value}")
}
array==.forEach { }==
forEach是一个单参数的匿名函数,故用it关键词。
array.forEach {
println(it)
}
array.forEachIndexed { }
array.forEachIndexed { index, i -> println("$index, $i") }
Range条件
判断包含关系
正向区间
1 <= a <= 4
// 1 2 3 4
a in 1..4
a in 1.rangTo(4)
1 <= a < 4
// 1 2 3
a in 1 until 4
逆向区间
// 4 3 2 1
a in 4 downTo 1
步长
//1 3
a in 1..4 step 2
//4 2
a in 4 downTo 1 step 2
字符串
换行输出
val text = """
|first line
|second line""".trimMargin()
replace
var str = "she is a outgoing girl"
val str1 = str.replace(Regex("[aeiou]"),"0")
val str2 = str.replace(Regex("[aeiou]")){
when (it.value) {
"a" -> "1"
"e" -> "2"
"i" -> "3"
"o" -> "4"
"u" -> "5"
else -> "0"
}
}
println(str1)
println(str2)
等号
Kotlin | Java | ||
== | 两个字符串中的字符是否匹配 | == | 做引用比较 |
=== | 两个变量是否指向内存堆上同一对象 | equals | 做结构比较 |
var str = "ABCD"
val str1 = "ABCD"
val str2 = "aBCD".capitalize()
println("$str == $str1 is ${str == str1}")
println("$str === $str1 is ${str === str1}") //JVM 字符串常量池
println("$str == $str1 is ${str == str2}")
println("$str === $str1 is ${str === str2}")
遍历
可使用forEach遍历
"a b c d e".forEach {
println(it)
}
数值类
1.23.roundToInt() //四舍五入
1.23.toInt() //去尾法
"1.23".toInt() //抛出异常
"1.23".toIntOrNull() //输出null
Double格式化
"%.2f".format(1.2345) //四舍五入,保留两位(String类型)
集合类
类型
分为两大类型:只读、不可变
常用集合类
List:集合元素可重复
Set:集合元素不可重复
Map:键值对
var set = setOf(11,22,33,22)
print(set) // [11, 22, 33]
继承层次
1
Collection
MutableList
List
MutableSet
Set
MutableCollection
MutableIterable
Iterable
2
Map
MutableMap
List
去重
list.distinct() //去重,原理见下图
list.removeIf{ … }
Map
创建
var m = mapOf("linr" to 10, "uuln" to 20) //key to value
print(m) //{linr=10, uuln=20}
安全索引
函数 | 说明 | 示例 |
getOrElse() | list、map | l.getOrElse(index) { value } m.getOrElse(key) { value } |
getOrNull() | list | l.getOrNull(index) |
getOrDefault() | map | m.getOrDefault(key, value) |
elementAtOrElse() | set | s.elementAtOrElse(index, value) |
elementAtOrNull() | set | s.elementAtOrNull(index) |
使用1:
// getOrElse
var list0 = listOf<Int>(11,22,33,44,55)
for (i in 0..8){
println("i = $i: " + list0.getOrElse(i){
list0[i % list0.size]
})
}
使用2:
// 两者等价
list0.getOrElse(5) {"there is noting"}
list0.getOrNull(5)?: "there is noting"
函数式编程在集合类中的应用
设计理念:
不可变数据 的副本,在链上的 函数间 传递(即,不会改变原对象)
映射函数 map、flatMap
有点儿像scala
fun main() {
val pets = listOf("rabbit", "spider", "snake", "gecko")
pets.also { println("pets: $it") } //pets: [rabbit, spider, snake, gecko]
.map { pet -> "a baby $pet" }
.also { println("pets: $it") } //pets: [a baby rabbit, a baby spider, a baby snake, a baby gecko]
.run { println("pets: $pets") } //pets: [rabbit, spider, snake, gecko]
}
从第一个also和最后的run输出的结果可以看出,没有改变原对象。函数式编程都是如此,后续函数不在赘述。
flatMap{ it } could be simplified to flatten()
fun main() {
val pets = listOf(
listOf("rabbit"), listOf("spider", "snake", "gecko")
)
pets.also { println("pets: $it") } //pets: [[rabbit], [spider, snake, gecko]]
.flatten()
.also { println("pets: $it") } //pets: [rabbit, spider, snake, gecko]
}
过滤函数 filter
fun main() {
val pets = listOf("rabbit", "spider", "snake", "gecko")
pets.also { println("pets: $it") } //pets: [rabbit, spider, snake, gecko]
.filter { it.contains("r") }
.also { println("pets: $it") } //pets: [rabbit, spider]
}
和flatMap组合使用:
fun main() {
val pets = listOf(
listOf("rabbit"), listOf("spider", "snake", "gecko")
)
pets.also { println("pets: $it") } //pets: [[rabbit], [spider, snake, gecko]]
.flatMap{ it.filter { it.contains("r") }}
.also { println("pets: $it") } //pets: [rabbit, spider]
}
和map组合使用,找素数:
fun main() {
val numbers = listOf(7, 4, 8, 33, 17, 1, 2, 94, 83)
numbers.also { println("numbers: $it") } //numbers: [7, 4, 8, 33, 17, 1, 2, 94, 83]
.filter { num ->
(2 until num).map { num % it }.none { it == 0 } && num >= 2
}
.also { println("numbers: $it") } //numbers: [7, 17, 2, 83]
}
组合函数 zip、fold
fun main() {
val pets = listOf("rabbit", "spider", "snake", "gecko")
val num = listOf(10, 20, 30, 40)
pets.also { println("pets: $it") } //pets: [rabbit, spider, snake, gecko]
.zip(num).also { println("pets: $it") } //pets: [(rabbit, 10), (spider, 20), (snake, 30), (gecko, 40)]
.toMap().also { println("pets: $it") } //pets: {rabbit=10, spider=20, snake=30, gecko=40}
}
fun main() {
val num = listOf(1, 2, 3, 4)
num.also { println("num: $it") }
.fold(11) { res, n ->
println("current res is $res")
res + (n * 10)
} //res:初始值为fold()中所给的值,n:num中取出的元素值
.also { println("res = $it") }
}
生成指定序列
generateSequence( 起始值 ) { 后续值的规则 } .自定义迭代器
val num = generateSequence(3) {
it + 1
}.filter { it % 2 == 0 }.take(5).toList() // [4, 6, 8, 10, 12]
运算符重载
常用操作符
操作符 | 函数名 |
+ | plus |
+= | plusAssign |
== | equals |
> | compareTo |
[ ] | get |
… | rangeTo |
in | contains |
以“==”为例: | |
左侧是kotlin源码,右侧为反编译后的Java代码 | |
从图中可知,“==”被编译为了Intrinsics.areEqual()方法。再点进去,可以看到用的就是equals()方法 | |
Intrinsics是kotlin的一个内部类,包括了判空、判等、断言等方法。
示例
sealed class Ball {
class BlueBall(val color: String?) : Ball()
class RedBall(val color: String?) : Ball()
class GreenBall(val color: String?) : Ball()
}
class Box<T : Ball>(vararg item: T) {
var available = false
private var subject: Array<out T> = item
operator fun get(index: Int): T? = subject[index]?.takeIf { available }
}
fun main() {
val b0: Ball = Ball.BlueBall("#111111")
val b1: Ball = Ball.RedBall("#222222")
val b2: Ball = Ball.GreenBall("#333333")
val mBox = Box<Ball>(b0, b1, b2)
mBox.available = true
println("you get ${mBox[1]}") //you get Ball$RedBall@6e0be858
}
在集合类中的使用
可使用 +=、-= 修改
var l = listOf(11,22,33,44)
//var l = mutableListOf(11,22,33,44)
l += 0
反编译后的java代码:>
解构语法特性
list
list支持解构语法特性:在一个表达式里给多个变量赋值。
var str = "he is a,student"
val (s1,s2,s3,s4) = str.split(" ",",") //split返回List集合
print("$s1: $s4") //输出student
此处无需用s2、s3,可用 “ _ ” 代替:
数据类
data class LoginUser(val name: String, val password: String)
变量权限默认为private编译时会自动添加一些方法:get()、copy()、toStirng()、hashCode()、equals()…
函数
可变参数
实质:长度可变的数组
特点:参数类型确定,参数个数不确定
fun main() {
countTotalScore("张三", 83, 97, 91)
countTotalScore("李四", 99)
}
fun countTotalScore (name: String, vararg scores: Int) {
var totalScore = 0
scores.forEach {
totalScore += it
}
println("${name}的总成绩:${totalScore}")
}
若要直接传递数组为参数,需使用”*“前缀操作符,意为将数组展开(不可用于集合)。
val scores = intArrayOf(83, 97, 91)
countTotalScore("张三", *scores)
lambda
背景
lambda也可用希腊字符λ表示,是lambda演算的简称。
lambda演算是一套数理演算逻辑,由数学家Alonzo Church于20世纪30年代提出,在定义匿名函数时,使用了lambda演算记法。
内联
背景
在JVM中,Lambda表达式都会被编译成一个匿名类,每次调用Lambda表达式时,都会创建新的对象实例。
故,JVM会为所有同lambda打交道的变量分配内存,这就造成来额外的内存开销,进而导程序性能降低。
为此,Kotlin提供了一种优化机制——内联,使用”inline“关键字修饰。
有了内联,编译器会将函数体复制粘贴到调用的地方,直接执行。不再使用lambda对象实例,避免出入栈的操作。
在不影响程序可读性的同时优化了性能,但在实际运行时会增加代码量。
(可类比C语言中的预编译指令,宏定义。)
示例
未用内联
Kotlin代码:
fun main() {
showDiscount("纸巾"){ goodName: String, hour: Int ->
"今年${goodName}促销还剩:${hour}小时"
}
}
fun showDiscount(goodName: String, getDiscountInfo: (String,Int)->String){
val hour = (1..24).shuffled().last()
println(getDiscountInfo(goodName, hour))
}
反编译后的java代码:
使用内联
Kotlin代码:
fun main() {
showDiscount("纸巾"){ goodName: String, hour: Int ->
"今年${goodName}促销还剩:${hour}小时"
}
}
inline fun showDiscount(goodName: String, getDiscountInfo: (String,Int)->String){
val hour = (1..24).shuffled().last()
println(getDiscountInfo(goodName, hour))
}
反编译后的java代码:
注意
使用lambda的递归函数内联,编译会发出警告,因为会导致复制粘贴无限循环。
标准函数
apply、also、let
定义:
使用1:3者等效使用
var list0 = ArrayList<String>().apply {
add("A")
add("B")
add("C")
}
var list1 = ArrayList<String>()
list1.also {
it.add("A")
it.add("B")
it.add("C")
}
var list2 = ArrayList<String>()
list2.let {
it.add("A")
it.add("B")
it.add("C")
}
println("apply: $list0")
println("also: $list1")
println("let: $list2")
使用2:对比let、also
val res = 2.let{
it + 1
}
val res1 = 2.also{
it + 1
}
println("let: $res")
println("also: $res1")
run、with
定义:
使用:
val res = "1234567".run {
length >= 10
}
val res1 = with("1234567"){
length > 0
}
print("res = $res ; res1 = $res1")
takeIf、takeUnless
目标对象.takeIf { 条件判断句 }.后续操作
takeIf效果类似if,但可直接在对象实例上调用,故有如下优势:
- 避免创建临时变量并赋值
- 可链式调用
ture 返回接收者对象(即,执行后续操作)
flase 返回null
takeUnless效果类似takeIf,但只在结果为false时执行后续操作
扩展函数
fun 目标类型.扩展函数名( ) { … }
为现有类自由添加自定义的函数,即使是使用private、final修饰,也可以扩展。
现有类:自定义的、标准库里的。
示例
/** 在字符串后增加n个"!" */
fun String.addExt(n: Int = 1) = this + "!".repeat(n)
fun main() {
println("abc".addExt(3)) //abc!!!
}
标准函数都是扩展函数,使用的是泛型方法。
fun <T> T.functionName() { ...}
实战
场景:
类似用java写个String工具类
用法:
- 单独建立扩展文件Strings.kt,在需要用到地方导入。
- 命名:“目标类名+s后缀”。
- 改名:import …. as …
扩展属性
var 类型参数 目标类型.属性名: 扩展属性的类型
定义在:
- class ✔
- file ✔
- function ✘
面向对象
声明
声明类的同时,声明构造函数。
初始化顺序
- 主构造函数
- 类级别属性赋值 / init初始化块(由代码顺序决定)
- 次构造函数
下图:kotlin源码(左),反编译java代码(右)
继承
不可继承 —— 默认
所有类默认使用final关键字修饰,不可被继承,方法同理。
可继承 —— open
可继承的类、方法,均用open修饰
open class Person {
open fun breath() = "活着就要呼吸"
}
实现
接口及其方法默认使用open、abstract关键字修饰。
可以有默认实现,但一般不这样使用。
object对象
∵ kotlin 没有static关键字
∴ 替代方法:使用object关键字(常写在伴生对象中)
- 对象声明
- 对象表达式
- 伴生对象
对象声明
相当于创建单例类
因为只有一个实例,故类名也是其实例的名称,调用时无需自己再实例一次。
object Student {
...
}
对象表达式
相当于匿名内部类
open class Student {
open fun study() = "学习中。。。"
}
// 1.实例一个大学生对象,继承Student。2.将这个对象赋值给s。
val s = object: Student() {
override fun study() = "学习高数中。。。"
}
伴生对象
1. 初始化时间:在类加载时才初始化,而非编译时。并且无论实例化类多少次,伴生对象都只初始化一次。
2. 作用:Kotlin中没有静态变量,因此Java中的静态变量、静态方法都可以写在伴生对象里。
// Define
class Student{
...
companion object XiaoMing {
val name = "XiaoMing"
val stuId = 001
fun sayHello() {...}
}
}
// Invoke(两种方法都可)
val id1 = Student.XiaoMing.stuId //类名.对象名.成员名
val id2 = Student.stuId //类名,成员名
3. 无名:每个类有且仅有一个伴生对象,因此可以不指定对象名
// Define
class Student{
...
companion object {
val name = "XiaoMing"
val stuId = 001
fun sayHello() {...}
}
}
// Invoke(两种方法都可)
val id1 = Student.Companion.stuId //类名.Companion.成员名, ❗companion首字母大写
val id2 = Student.stuId //类名,成员名
内部类 inner class
普通嵌套
若类Aa仅对类AA有用,则可将Aa嵌套在AA中,如此更合乎逻辑。
不足:Aa无法使用AA的数据
class AA {
...
class Aa { ... }
}
内部嵌套
自带一个对外部类的对象引用,解决了上述不足。
class AA {
...
inner class Aa { ... }
}
以下是Aa类编译之后的代码:
数据类 data class
语法:
- 可实现,kotlin1.1之后可继承
- 主构造函数必须有参数
- 不能为abstract、open、sealed、inner
- 自动创建函数:hashCode()、equals()、copy()、toString()
copy:可以在copy的同时,修改值。
data class LoginUser(val name: String, val password: String) {
constructor(name: String) : this(name, "00000")
}
fun main() {
val p1 = LoginUser("张三","12345")
val p2 = p1.copy("李四")
val p3 = LoginUser("李四")
println(p1) //LoginUser(name=张三, password=12345)
println(p2) //LoginUser(name=李四, password=12345)
println(p3) //LoginUser(name=李四, password=00000)
}
枚举类 enum class
简单使用
同:字符串常量
异:较之实现了类型安全
enum class Color{
Green, REF, BLUE, PINK
}
fun main() {
val c1 = Color.BLUE
println("c1 = $c1") //c1 = BLUE
}
进阶使用
- 在主构造函数中加入参数
- 自定义方法(记得枚举对象后加“ ; ”)
enum class Color(val rgb: String) {
Green("#7ED321"), REF("#D0021B"), BLUE("#4A90E2"), PINK("#FFD5DC");
fun getInfo() = "name = ${this.name}, ordinal = ${this.ordinal}, rgb = ${this.rgb}"
}
fun main() {
println(Color.Green.getInfo()) //name = Green, ordinal = 0, rgb = #7ED321
println(Color.BLUE.getInfo()) //name = BLUE, ordinal = 2, rgb = #4A90E2
}
❗❗注意
- 每个枚举常量都是该类的一个对象
- 在编译枚举类时,会自动添加一些属性和方法,如下图所示:
密封类 sealed class
可以封装其他类同时发挥数据类的作用。
密封类的构造函数是私有的
sealed class UserStatus {
object Unregister : UserStatus()
class Register(val userId: String) : UserStatus()
fun check(): String = when (this) {
is Unregister -> "未注册"
is Register -> "用户:${this.userId}"
}
}
fun main() {
val s0 = UserStatus.Unregister
val s1 = UserStatus.Register("13525")
println(s0.check()) // 未注册
println(s1.check()) // 用户:13525
}
泛型
多用于创建容器类
简单使用
//单泛型类
class Box<T>(item: T) {
fun getItemInfo(): T?{ ... } //单泛型方法
fun<R> changeItemInfos(change: (T) -> R): R?{ ... } //多泛型方法
}
泛型类型约束
假设有如下4个类,现在规定上述的Box中只能装入三色的小球,
此时可以限制泛型类型为Ball
Ball
BlueBall
RedBall
GreenBall
class Box<T: Ball>() { ... }
类型上界
fun <T: Comparable<T>> gt(x: T, y: T): Boolean = x > y
T: Comparable,表示 Comparable 是类型T的上界。
相当于告诉编译器,类型参数 T 代表的都是实现了 Comparable 接口的类。
类型擦除
泛型是在编译器层次上实现的,
Java和Kotlin的泛型实现,都是采用了运行时类型擦除的方式。
即,生成的class字节码文件中不包含泛型中的类型信息的。
例如,在代码中定义的 List<Object> 和 List<String> ,在编译之后都会变成 List。
JVM 看到的只是 List,而由泛型附加的类型信息对JVM来说是不可见的。
因此泛型类并没有自己独有的Class类对象。
比如,Java中并不存在List<String>.class、List<lnteger>.class,而只有List.class
对应地在Kotin中并不存在MutableList<Fruit>::class,而只有MutableList::class。
擦除的基本过程
- 替换:
把代码中的类型参数,替换成具体的类。
去掉出现的类型声明,即去掉<Xxx>的内容。
(java中默认是Object,如果指定了类型参数的上界的话,则使用这个上界。)
T get( ) Object get( )
List<String>
- 桥接:
擦除了类型之后的类可能缺少某些必须的方法,此时编泽器会动态生成桥接方法。
获取擦除的类型 —— 实化类型 reified
泛型在运行时会发生类型擦除,若想获得泛型的真实类型:
- java:反射
- kotlin:在内敛函数中,使用 reified 关键字,修饰泛型参数
inline fun <reified T> Any.isType(): Boolean =
if (this is T) true else false
fun main(arg: Array<String>) {
println("123".isType<String>()) //true
println(123.isType<String>()) //false
}
in & out
综述
对应Java中的PECS(Producer-Extends, Consumer-Super)
Java 通配符 | Kotlin 投射类型 | |
Producer | ? extends T 指定参数类型上界 | out T 只保证读安全 |
Consumer | ? super T 指定参数类型下界 | in T 只保证写安全 |
Food
Hamburger
Noodles
val f: Food = Hamburger() // ✔
val f: List<Food> = List<Hamburger>() // ✘
val f: List<Food> = List<Hamburger>() // out
val f: List<Hamburger> = List<Food>() // in
意义
对于泛型类,编译器承担了全部的类型检查工作。
编泽器禁止某些泛型的使用方式,也是为了确保类型的安全性。
若 List<Food>中可以添加所有 Food类及其子类对象,
则 List中可能既有Hamburger又有Noodles,
故 造成List中的元素类型混乱。
示例
协变 out:生产
interface Factory<out T> {
fun product(type: Int): T
} //泛型做返回值
class FoodFactory : Factory<Food> {
override fun product(type: Int): Food = when (type) {
1 -> Noodles("一份牛肉面", 12.00)
2 -> Hamburger("一个香辣鸡腿堡", 15.00)
else -> Food("食物")
}
}
open class Food(val name: String, val price: Double? = null) {
override fun toString(): String =
if (price == null) {
"生产:${this.name}"
} else {
"生产:${this.name},定价:${this.price}"
}
}
class Noodles(name: String, price: Double) : Food(name, price)
class Hamburger(name: String, price: Double) : Food(name, price)
fun main() {
val mFactory = FoodFactory()
val production0 = mFactory.product(0)
val production1 = mFactory.product(1)
val production2 = mFactory.product(2)
println(production0) //生产:食物
println(production1) //生产:一份牛肉面,定价:12.0
println(production2) //生产:一个香辣鸡腿堡,定价:15.0
}
逆变 in:消费
interface People<in T> {
fun consume(item: T): String
}
class Consumer : People<Food> {
override fun consume(f: Food) =
if (f.price == null) {
"消费:${f.name}"
} else {
"消费:${f.name}, 花费:${f.price}"
}
}
open class Food(val name: String, val price: Double? = null)
class Noodles(name: String, price: Double) : Food(name, price)
class Hamburger(name: String, price: Double) : Food(name, price)
fun main() {
val consumer = Consumer()
val consumption0 = consumer.consume(Food("食物"))
val consumption1 = consumer.consume(Noodles("一份牛肉面", 12.00))
val consumption2 = consumer.consume(Hamburger("一个香辣鸡腿堡", 15.00))
println(consumption0) //消费:食物
println(consumption1) //消费:一份牛肉面, 花费:12.0
println(consumption2) //消费:一个香辣鸡腿堡, 花费:15.0
}
不变:产 + 消
interface Consumer<T> {
fun consume(item: T): T
}
通配符
若使用时不知道泛型的具体类型,使用泛型通配符:
- java:<?>
- kotlin:<*>
星投影
A<*> 的读写都是不安全的,若要安全还是要将星号投影为out或in关键字。
T未规定类型上下界:
原泛型类 | 星投影 |
A | 读取:A<out Any?> 写入:A |
A | A<out Any?> |
A | A |