个人学习Kotlin文档

该文档为初步学习kotlin,主要是一些语法内容,记录下来,防止自己忘记,并且以后也可以看看,查缺补漏。

kotlin 基础

属性

字段和其访问器的组合。在kotlin 中,属性是头等语言。

在kotlin中,声明了一个属性后,就会自动生成默认的访问器,即getter和setter方法。

备注:当属性是使用 is 开头的,生成的 getter 方法不会添加任何前缀,在生成 setter 方法是 set 会被替换成 is 。总的来说,就是 getter 和 setter 方法都不会添加前缀了。

那么如果属性就是 is 呢?抱歉,不存在,因为 is 是关键字。

一般来说,属性都需要一个支持字段来保存属性的值,但是对于 val 修饰的属性,可以不需要支持字段,只需要重写 getter 方法,在 getter 方法中根据其他属性计算得到

自定义访问器

class User() {
    var name: String = ""
        get() = "法外狂徒 - ${name}"
        set(value) {
            field = name
        }
}

如上所示,在 User 类中的 name 属性自定义了 getter 和 setter 访问器,在访问器如果需要访问存储的字段,需要使用 field 来访问。

when 语句

when 语句是一个有返回值的语句,可以直接返回 when 表达式。

当写一个直接返回 when 表达式的表达式体函数时,when 表达式内选择分支可以直接省略 return ,分支的最后一句就是返回的语句。

// 定义一个枚举类
enum class Color {
    RED, YELLOW, BLUE
}

// 带参数的when语句
fun getColorDesc(color: Color) = when (color) {
    Color.RED -> "红色"
    Color.YELLOW -> "黄色"
    Color.BLUE -> "蓝色"
    else -> "未知"
}

// 不带参数的when语句
fun getColorDesc2(color: Color) = when {
    color == Color.RED -> "红色"
    color == Color.YELLOW -> "黄色"
    color == Color.BLUE -> "蓝色"
    else -> "未知5"
}

else 分支语句中,相当于java的 default 语句,当所有语句都没有被执行的时候,默认执行 else 分支。

在使用不带参数的 when 语句的时候,分支对象必须是布尔表达式

迭代

迭代数字

闭区间: a … b 相当于[a,b]

左闭合右开区间:a until b 相当于[a,b)

例如使用 for 循环输出1到10

    // 输出1到10,包含10
    for (i in 1..10) {
        print("${i} ,")
    }
    // 输出1到10,不包含10
    for (i in 1 until 10) {
        print("${i} ,")
    }

迭代map

for ((key, value) in map) {
    println("$key , $value")
}

使用一对值获取到 map 的 key 与 value

使用 in 检查集合和区间的成员

fun getDesc(value:Int) = when(value){
    in 0 .. 9 ->"个位数"
    in 10 .. 99 -> "两位数"
    else -> "未知"
}

上面就是在 when 分支中使用 in 检查某个值是否在区间范围内。同样,in 也可以用在检查集合。

== 与 ===

== 相当于 Java 中的 equals 。

=== 相当于 Java 中的 ==,通常用来引用比较。

函数

静态工具类

在 kotlin 中,没有 static 关键字,其中替代品就是使用顶层函数和属性。

const 相当于 java 中的 final 关键字。

创建 c31.kt 的 kotlin 文件,代码如下所示:

var name :String = "张三"

const val age = 1

fun getDesc() = "法外狂徒"

编译后,会生成一个java类,类名就是文件名加上kt,生成代码如下所示:

public final class C31Kt {
   @NotNull
   private static String name = "张三";
   public static final int age = 1;

   @NotNull
   public static final String getName() {
      return name;
   }

   public static final void setName(@NotNull String var0) {
      Intrinsics.checkParameterIsNotNull(var0, "<set-?>");
      name = var0;
   }

   @NotNull
   public static final String getDesc() {
      return "法外狂徒";
   }
}

总结一下:

kotlinjava
顶层属性private static ,有对应的 public 的 getter 和 setter 方法
constpublic static final ,没有对应的 getter 和 setter 方法
顶层函数public static final

扩展函数和属性

扩展函数

扩展函数相当于 java 中静态函数,在 java 中对应 static final 修饰的方法。

扩展函数是静态函数的一个高效的语法糖,实际上在调用的时候,它会把调用的对象作为它的第一个参数。

扩展函数不能访问类中私有和受保护的成员,参考 java 即可得出,static final 方法中也不能访问对象的私有成员和受保护成员。

如果一个类的扩展函数和成员函数拥有相同的签名(即名称与参数都相同),调用的时候优先使用成员函数。

调用扩展函数是,由该变量的静态类型决定,而不是运行时类型。如下所示:

open class A {
    open fun show() {
        println("A")
    }
}

class B : A() {
    override fun show() {
        println("B")
    }
}

fun A.printName() {
    println("this is A")
}

fun B.printName() {
    println("this is B")
}

fun main(args: Array<String>) {
    val a: A = B()
    a.show()
    a.printName()
}

// 控制台输出结果
B
this is A

变量a在运行时类型为B,所以,调用成员函数show的时候使用的是B类中的成员,但是在编译时期a的类型为A,所以,在调用扩展函数的时候,尽管A和B类都有相同名称,相同参数的扩展函数printName,但是最终调用的还是A中的扩展函数。

扩展属性

扩展属性必须自定义访问器,虽然称之为属性,但是并没有存储任何状态,因为类中没有合适的的地方存储他们,不可能给现有的类添加额外的字段。

var StringBuilder.lastChar: Char
    set(value) {
        append(value)
    }
    get() = get(length - 1)

如上图所示,对 StringBuilder 定义了扩展属性,并定义了对应的访问器,在 java 中,实际上就是定义了两个静态方法 getLastChar 和 setLastChar。

处理集合

可变参数与展开运算符

java 中使用三个点表示,而在 kotlin 中,使用 vararg 表示。

在 kotlin 中,如果要传递数组,必须要求显示的解包数组,即使用展开运算符 *

val array = Array(10){
    it*it
}
val list = listOf<Int>(1,2,3,*array)

如上所示,先定义一个数组,然后把数组里面的值填充到 List 中,如果不加 *,就会发现编译时就出错。

中缀运算符

中缀运算符多用于表示键值对,可以与只有一个参数的函数一起使用,无论是普通函数还是扩展函数。要想使用中缀运算符,必须使用 infix 修饰函数。

infix fun Any.conn(other :Any) = Pair(this,other)
val pair = 1 conn  2

如上所示,对 Any 定义了中缀运算符 conn,对任意对象调用,就会生成一个 Pair对象。

大多数情况下并不需要自定义,一般在 Map 中都有已经定义好的中缀运算符 to。

三重引号

在 java 中,对于一些特殊的字符,我们通常需要转义,比如正则中的点号,换行符等等。

在 kotlin 中,双引号的作用同 java,但是对于需要转义的字符,我们可以通过三重引号,它可以包含任意的符号

val threeStr =
        """hello
            |world
            |/this
            |//is
            |//nkotlin
        """.trimMargin()
println(threeStr)

// 输出结果
hello
world
/this
//is
//nkotlin

可以看到,在三重引号内是什么,输出的结果就是什么。注意,在每行起那面的竖线是表示每行开始的,也可以自定义一个字符,在 trimMargin 参数中传入。

有了三重引号,最方便的还是正则表达式,再也不要那么多转义啦。

类、对象和接口

修饰符

Java 中的类和方法默认是 open 的,但是 kotlin 中默认都是 final。抽象成员始终是 open 的,不能声明为 final。

在智能转换中(即 is ),要求变量在进行类型检查后没有改变过,并且该属性没有自定义访问器,这个前提就是属性必须是 final 的,这就是 kotlin 默认是 final 的好处之一。

kotlin 默认的声明模式是 public ,而 java 默认可见性是包私有。

特殊点:

  • 类的扩展函数是不能访问他的 private 和 protected 成员
  • 外部类不能访问内部类的 private 成员

接口

kotlin 接口和 java 8 中的相似,是可以包含抽象方法的定义和非抽象方法的实现的。

如果一个类实现多个接口,且这些接口中有相同签名的方法,那么该类必须显示的实现该方法(即重写该方法)。

内部类与嵌套类

内部类,持有外部类的引用。

嵌套类,不持有外部类的引用

Javakotlin
嵌套类static class Aclass A
内部类class Ainner class A

可以看到,在 kotlin 中,没有显示修饰符的嵌套类,对应的是 java 中的静态内部类。

密封类

对一个类使用 sealed 修饰符修饰,这个类就是密封类,它的所有子类都必须定义在这个类所在的文件中,最好直接嵌套在该类中,并且子类不准是数据类(即 data 修饰的类)

sealed 修饰符隐含这个类是一个 open 类,所以不需要显示的使用 open 修饰了。

密封类的好处就是:确定了类的继承者具体有那些,除此之外别的文件不可能再有它的子类

构造方法

构造方法中的参数有的添加了 var,val ,有的没有添加,有什么区别呢?

构造方法的参数添加了var, val 意味着属性会通过该参数来初始化

如果没有添加,那么这个参数实际上是不可获取的。

如下所示:

class User(name: String)

class User2(val name:String)

其中 User 类的实例是没有 name 字段的,也就是无法通过 user.name 获取值的。

而 User2 是可以的,User2 的属性是通过构造方法的参数来初始化的,相当于下面的代码:

class User2(_name:String){
    val name:String

    init {
        name = _name
    }
}

可以接着精简为:

private class User2(_name: String){
    val name = _name
}

数据类

如果一个类是一个方便的数据容易,一般都需要重写 equals ,toString,hashCode 者三个方法。

数据类的作用就是帮助我们去自动实现者三个方法。生成方法规则如下:

equals:主构造方法中的参数自动生成属性,所有的属性值都相等,就认为是 true

hashCode:只对主构造方法中参数生成的属性进行 hash 计算,得到 hashCode

toString:按照著构造方法中参数的顺序排列生成字符串。

数据类定义方式如下:

// 定义数据类
data class User(val name :String, val age:Int)

// 创建两个对象
val user1= User("张三",33)
val user2= User("张三",33)
println("user1 == user2 : ${user1 == user2}")	// true
println("user1 === user2 : ${user1 === user2}")// false

只需要在定义类的时候前面添加 data 关键字即可。

虽然数据类不要求属性一定是 val 的 ,但是还是强烈推荐使用只读属性,让数据类实例不可变。

类委托

使用 by 关键字进行类的委托。

class  DelegateCollection<T>(inner: Collection<T> = ArrayList<T>()) : Collection<T> by inner

上面将 DelegateCollection 委托给了 inner,此时编译器会自动实现一些默认的方法,比如Collection 的 getSize,remove 等等。

object 关键字

java 中 static 关键字在 kotlin 中并没有,作为替代,kotlin 使用了以下方式:

  • 依赖包级别的函数:即顶层函数
  • 对象声明:主要用于单例
  • 伴生对象:主要用于工厂方法和静态成员

对象声明:创建单例

对象声明将类的声明与该类的单一实例声明结合到了一起,即声明就创建了唯一实例。

与普通类一样,可以包含属性,方法,初始化语句块,因为声明就是创建实例,所以不要创建构造方法,没有意义。

对象声明在java中实现:私有构造方法,构造代码块中包含一个 INSTANCE 的 static final 变量,该变量持有唯一单例。所以,在java中可以引用 INSTANCE 变量调用kotlin的对象声明。

object Single{
    fun test() {
        println("对象声明,实现单例")
    }
}

// 调用
Single.test()

伴生对象:companion object

相当于 java 的静态方法。

伴生对象成员不可以在子类中重写,它是一个声明在类中的普通对象。

伴生对象可以添加扩展函数。

伴生对象可以访问类中所有的 private 成员,private 构造方法,一般用于实现工厂方法。

伴生对象可以直接通过容器名称来访问这个对象的属性和方法,不在需要显示的指定对象的名称,即通过 Companion 直接获取该对象。如果在 companion object 后面自定义伴生对象的名称,就需要通过该名称获取该对象。

class A{
    companion object{

        private val name:String = "kotlin"

        fun test() {
            println("this is test : ${name}")
        }
    }
}

// 可以通过以下方式调用test方法
A.test()
A.Companion.test()

在 java 中,A 类中会生成一个 static final class Companion 类,并生成它的实例,类 A 中 Companion 字段持有该实例。

对象表达式:匿名内部类

对象表达式,即匿名对象,相当于 java 中的匿名内部类。

除掉对象名字外,写法与对象声明是一样的。

与对象声明不同,匿名对象不是单例的,每次对象表达式被执行的时候,都会创建一个新的对象实例。

与 java 的匿名类一样,对象表达式可以访问创建它的函数中的变量,并且与 java 不同的时,kotlin 可以访问非 final 对象,而 java 就必须访问 final 对象。

对象表达式在需要匿名类重写多个方法的时候比较好用,如果只需要重写一个方法,推荐使用 lambda 表达式。

lambda 编程

lambda表达式和成员引用

kotlin 允许在 lambda 内部访问非 final 变量甚至去修改他们。从 lambda 内访问外部变量,我们称这些变量被 lambda 捕捉。

默认情况下,局部变量的声明周期被限制在声明这个变量的函数中,但是如果他被 labmda 捕捉了,使用这个变量的代码可以被存储并稍后再执行。原理:

  • 捕捉的是 final 变量,他得值和使用这个值的 lambda 代码一起存储。
  • 捕捉的是非 final 变量,变量值被封装在一个特殊的包装器中,这样就可以改变这个值,而对这个包装器的引用会和 lambda 代码一起存储。

集合的函数式API

集合中常用的函数式API

集合的函数式 api 有很多,比如 filter,map,all,any,count,find ,groupBy,flatMap等,并且这些函数有些是可以链式调用的。

  • filter:从集合中选取出需要的元素,添加到新的集合中输出
  • map:对集合中的每个元素进行变化,将变化后的元素添加到新的集合中输出
  • all:检查集合中所有元素是否符合给定的判断条件,所有都符合返回 true,否则返回 false。对应的,也有 !all 。
  • any:检查集合中是否有任何元素符合给定的判断条件。对应的,也有 !any。
  • count:获取集合中符合条件的元素的数量。
  • groupBy:按照条件将集合中的元素进行分组,将条件作为 key,集合中满足条件的元素作为 value,生成一个 map 输出。
  • flatMap:根据作为实参给定的函数对集合中的每个元素做变换,然后把多个列表合并成一个列表

下面对一些函数进行应用:

val persons = arrayListOf<Person>(
        Person("大牛", 11),
        Person("二狗", 22),
        Person("张三", 33),
        Person("李四", 44)
)

val isAdult = { p: Person -> p.age >= 18 }

以上定义了一些公共部分

val count:Int = persons.count(isAdult)
// 输出结果: count:3
println("count: $count")

val count2 = persons.filter(isAdult).size
// 输出结果:count2: 3
println("count2: $count2")

count 函数只关注集合中元素是否符合条件,符合条件计数器就+1,并没有生成中间集合。而filter 函数是把满足条件的元素收集到一个集合中生成了一个新的结合,然后在使用 size 获取到满足条件的元素。尽管上面两种方式都能获取,但是显然使用 count 的开销更小。

val groupBy: Map<Boolean, List<Person>> = persons.groupBy(isAdult)
println("groupBy:$groupBy")
// 输出结果:
// groupBy:{false=[Person(name=大牛, age=11)], true=[Person(name=二狗, age=22), Person(name=张三, age=33), Person(name=李四, age=44)]}

groupBy 函数返回的是一个 Map,因为传入函数的 lambda 表达式返回结果是布尔值,所以最终 Map 的 key 就是 Boolean,value 就是符合条件的元素的集合。

val names:List<String> = persons.flatMap {
    listOf(it.name)
}
println("names:$names")
// 输出结果:
// names:[大牛, 二狗, 张三, 李四]

flatMap 返回的是 List,并且传入的 lambda 表达式要求返回的是一个 Iterable。上面就是将 persons 里面的每个元素的 name 属性提取出来生成一个新的 List。

惰性集合操作:序列

惰性集合操作的入口就是 Sequence 接口,这个接口表示的就是一个可以逐个列举元素的元素序列。

对一个 List 调用扩展函数 asSequence 方法,将集合转换成序列,然后就可以了。

序列的作用:对于一个大型的集合,如果先调用 filter ,然后在调用 map 函数,每部中间都会生成一个集合,如果转换成序列,就不会有中间集合生成。即序列会对每个元素执行操作,知道所有的操作都完成了,再去执行下一个操作。

例如:

val list = Array(10) {
    it
}.toList()

val a = { it: Int -> it > 5 }
val b = { it: Int -> it * it }

list.filter(a).map (b)
list.asSequence().filter(a).map (b)

如上所示

  • 没有序列情况下:先对 list 执行 a 操作,每个元素都执行完毕生成一个新的 List,然后在对新的 List 执行 b 操作。
  • 有序列:先取 list 的一个元素执行 a 操作,然后再执行 b 操作,执行完毕后再取 list 第二个元素,直到所有元素遍历完毕。

创建序列有两种方式:

  • 对 List 使用 asSequence 扩展函数,将 List 转换成序列
  • 使用 generateSequence 函数创建序列

java 函数式接口

函数式接口:有且仅有一个抽象方法的接口。也称为 SAM 接口(Single Abstract Method)。

很多时候,我们都会将 lambda 作为函数的参数。

如果显示的声明对象作为参数(即使用 object 关键字声明匿名类),每次函数调用的时候都创建一个新的对象。但是如果是使用 lambda 表达式,只要没有访问任何来自它的函数的变量,对应的匿名类实例就只会创建一个。r如下所示:

// 定义一个函数,接受一个Runnable作为参数
fun run(r: Runnable) {
    r.run()
}

// 使用object创建匿名类,每次调用test1方法,都会创建一个Runnable实例
fun test1() {
    run(object : Runnable {
        override fun run() {
            println("匿名对象")
        }
    })
}

// 使用lambda表达式做参数,并且没有捕捉任何变量,多次调用test2也只会创建一个Runnable实例
fun test2() {
    run {
        println("lambda表达式作为参数")
    }
}

// 全局变量runnable和test3函数中调用该实例,其实就是test2的原理
val runnable = { println("lambda表达式作为参数")}
fun test3() {
    run {
        runnable
    }
}

// lambda 从包围它的作用域中捕捉了变量,每次调用就会创建一个新的实例,用来存储变量的值。
fun test4(id: Int) {
    run{
        println("捕捉该函数的变量:$id")
    }
}

以上lambda参数都创建了实例,仅仅在期望函数式接口的 java 方法中有效,对集合是使用 Kotlin 扩展方法并不合适。另外,如果把 lambda 传递给标记 inline 的 Kotlin 函数是不会创建任何匿名类的。

带接受者的 lambda

在 lambda 函数体内可以调用一个不同对象的方法,而且无需借助任何的额外限定符,这样的 lambda 叫做带接收者的 lambda。

下面是一些常用的函数

函数结构函数体内使用的对象返回值常用场景
withfun <T, R> with(receiver: T, block: T.() -> R): Rthis指向第一个参数lambda的返回值一个类调用多个方法,值需要传入一个调用对象即可。常用在 RecycleView 的 OnBinderViewHolder 中
applyfun T.apply(block: T.() -> Unit): Tthis指向调用对象调用对象类似build模式
letfun <T, R> T.let(block: (T) -> R): Rit 指向调用对象lambda的返回值处理不为 null 的操作场景
alsofun T.also(block: (T) -> Unit): Tit 指向调用对象调用对象与 let 相似,let可用的 also 都可以使用。一般用于多个扩展函数的链式调用。
runfun <T, R> T.run(block: T.() -> R): Rthis指向调用对象lambda的返回值适用于 let,this 函数的任何场景
usefun <T : Closeable?, R> T.use(block: (T) -> R): Rthis 指向调用对象lambda的返回值用来操作可关闭的资源。例如BufferedReader,如果对文件操作,可以使用 useLines 函数

kotlin 类型系统

智能转换

is 运算符

类似java中的 instanceof 关键字用法,用来检查对象是否与特定的类型兼容(即此对象是否是该类型,或者是派生类)

对象经过 is 检查后,不再需要进行显示的转换,kotlin 会自动转换成检查的类型。

如果是对一个类的属性使用 is 进行只能转换,必须要求改属性是 val 修饰的,并且没有自定义的访问器

as 运算符与 as? 运算符

用于执行引用类型的显示类型转换。

如果要转换的类型与指定的类型兼容,就转换成功,否则失败。

在使用 as 的时候,转换失败就直接抛出异常了,使用 as? 的时候,转换事变就会返回 null 值。

延迟初始化的属性

使用 lateinit 关键字延迟属性的初始化工作。

延迟初始化的属性都是 var,因为需要在构造方法外修改它的值,而 val 的属性会被编译成必须在构造方法中初始化的 final 字段。

可空性和 Java

当 kotlin 调用 java 代码的时候,可能存在不知道属性的类型是否是可空的,这时候就会当作平台类型处理,即可以当作可空类型处理,也可以当作非空类型处理。

当然,如果 java 代码中有一些注解信息表示了是可空的或者是非空的,kotlin 是可以知道其类型的。

数据类型

Java 中把基本数据类型和引用类型做了区分。

基本数据类型:直接存储了它的值。如int类型的变量,直接存储值。它的好处就是更高效的存储和传递值,但是无法对这些值调用方法,而且放入到集合中必须用它的包装类型。

引用类型:存储的是指向包含该对象的内存地址的引用。比如 String 类型,存储的就是地址。

但是在 kotlin 中,数字类型尽可能的选择最高效的方式来表示,即可能是基本数据类型,也可能是引用类型,看场景选择最优的类型。但是在用作泛型类型参数的基本数据类型会被编译成对应的 java 包装类型。

任何时候,只要使用了基本数据类型的可空版本,说明它就被编译成了对应的包装类型。

Unit与Nothing

unit

kotlin 的 Unit 类型,相当于Java中的 void。但是还是有一定的区别的。

Unit 是一个完备的类型,可以作为类型参数,而 void 则不可以。

一般用在泛型中,如下所示:

interface Processor<T> {
    fun process():T
}

class NoResultProcessor : Processor<Unit> {
    override fun process() {
    }
}

process 方法会返回 T,但是在 NoResultProcessor 中的是不需要返回任何值的,但是实现Processor 接口时需要传入一个类型的,这里就可以使用 Unit 代替,在 NoResultProcessor#process 不需要显示的 return Unit() ,编译器会自动添加上。

Nothing

Noting 表示计算结果永远不会返回,一般用作抛出异常的函数中返回值。

Noting 类型没有任何值,只有被当作函数返回值,或者被当作泛型函数返回值的类型参数使用才有意义。

fun exception(): Nothing {
    throw NullPointerException()
}

data class Person(val name: String?, val age: Int)

fun test() {
    val person = Person(null, 23)
    // 如果exception没有返回值,编译不通过。
    // 编译器会自动识别name属性为非空类型
    val name:String = person.name ?: exception()
    println("name:$name")
}

如上所示,当 person.name 为 null 时,Elvis 运算符会使用右边的函数获取到返回结果,但是 exception 内部会报错。所以,编译器就可以自动判断出name 属性一定时非空类型的,因为一旦 person.name 为 null 程序就会异常终止。

运算符重载以及其他约定

重载算术运算符

kotlin 的运算符是不支持自动交换性的。即不支持运算符左右两边的对象互换。

重载运算符的方法:用预先定义的一个名称来声明函数(可以是扩展函数),然后用 operator 来修饰这个函数。

重载二元算术运算

data class Point(val x: Int, val y: Int){
    operator fun plus(p: Point): Point = Point(this.x + p.x, this.y + p.y)
}

fun main(args: Array<String>) {
    val p1 = Point(1, 2)
    val p2 = Point(3, 4)
    val p3 = p1 + p2
    println("p3: $p3")
}
// 输出结果:
// p3: Point(x=4, y=6)

如上所示,自定义一个类 Point ,然后使用 operator 重写 plus 方法,就可以对 Point 的示例使用 + 号进行算术运算。同样,只要实现其他几个方法,也可以使用对应的符号进行运算。

当然,也可以在定义为扩展函数,但是如果在类中已经有了该算术符号的实现,在使用扩展函数就无效了,因为扩展函数的调用顺序是低于类中的函数的。

一般来说,对于二元算术运算符,都是对 val 属性进行运算的,支持的二元运算符有以下:

表达式函数名称
a+bplus
a-bminus
a*btimes
a/bdiv
a%bmod

重载复合赋值运算符

一般是对 var 属性进行运算。

表达式函数名称
a+=bplusAssign
a-=bminusAssignn
a*=btimesAssign
a/=bdivAssign

重载一元运算符

表达式函数名称
+aunaryPlus
-aunaryMinus
!anot
++a, a++inc
–a, a–dec

重载比较运算符

equals:恒等运算符

重载恒等运算符不需要使用 operator 关键字。

重载该运算符是不能使用扩展函数的,因为所有的类都继承于 Any,Any 中有 equlas 的默认实现,而扩展函数优先级是小于类中的函数的,所以,即使重写了也没有任何效果。

该函数对应的运算符是恒等运算符,即 ===。

compareTo:排序运算符

同上很等运算符,不需要使用 operator 关键字,需要实现 Comparable 接口,并重写 compareTo 方法。

实现完成后,就可以使用 > ,>=,<,<= 号来比较两个对象。

在 compareTo 方法中,可以使用 compareValuesBy 方法来简洁的实现 compareTo 方法。

fun main(args: Array<String>) {
    val p1 = Person("abc", 11)
    val p2 = Person("abc", 22)
    val p3 = Person("bcd", 22)

    println("p1>p2:${p1>p2}")
    println("p1>p3:${p1>p3}")
    // 输出结果:
    // p1>p2:true
    // p1>p3:false
}

data class Person(val name: String, val age: Int)
    : Comparable<Person> {
    override fun compareTo(other: Person): Int =
            compareValuesBy(this, other, Person::name, Person::age)
}

如上所示,对 Person 类实现了 Compareble 接口,之后就可以使用符号来比较两个 Person 实例。其中 compareTo 方法中使用到了 compareValuesBy 方法,前两个参数为比较的对象,后面可以传入任意个参数,从第三个参数一次开始比较,如果相等,比较第四个,直到所有的参数比较完成。

集合与区间的约定

  • 下标运算符:通过下标获取和设置元素,使用 a[b] ,所有的集合和区间都默认支持。只要实现了 get 和 set 方法即可,同样,该方法也需要使用 operator 修饰。
  • in 约定:用来检查某个对象是否属于集合,对应的函数时 contains。
  • rangeTo 约定:返回一个区间,对应的符号时 a … b。可以自定义该运算符,但是如果该类试下了 Comparable 接口,就不需要了。
  • iterator 约定:iterator 方法可以被定义为扩展函数,也可以让类实现 Iterator 接口,并重写 next 和 hasNext 方法。

解构声明与组件函数

解构声明:允许展开单个复合值,并使用他们来初始化多个单独的变量

fun main(args: Array<String>) {
    val (x, y) =  Point(1, 2)
    println("x:$x,y:$y")// 输出结果 x:1,y:2
}

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

如上所示,自定义了 Point 类,并重写了 componentN 方法,N时声明中变量的位置,最大值值允许5个。重写后就可以使用解构声明,将类中的变量赋值给单独的变量。

如果使用的数据类,可以不需要重写 componentN 方法,因为数据类已经自动重写了。

委托属性

委托属性:依赖于约定。操作对象不用自己执行,而是把工作委托给另一个辅助的对象,我们把辅助对象成为委托。

惰性初始化和 by lazy()

惰性初始化时一种常见的模式,直到第一次访问该属性的时候,才会根据需要创建一个对象。

对于惰性初始化,我们一般支持属性技术来完成。即对外提供一个属性供访问,内部使用另一个属性来存储。如下所示:

fun loadEmails(): List<String> {
    return listOf("111@qq.com", "222.@gmail.com")
}

class User() {
    private var _email: List<String>? = null

    val email: List<String>
        get() {
            if (_email == null) {
                _email = loadEmails()
            }
            return _email!!
        }
}

如上所示,支持属性为 _email,所有的访问都是通过 email 属性完成的。get 的时候会判断 _email 是否为 null,如果为 null,会先进行加载。这就是所谓的惰性初始化。

支持属性技术实现惰性初始化有一个重要的问题,就是线程不一定时安全的。

而在 kotlin 中推荐使用 by lazy 解决该问题,lazy 函数时线程安全的。如果许哟啊,可以设置其他选项来告诉它使用哪个锁。如果该类不会再多线程环境中使用,可以避开同步。

class User2() {
    val email: List<String> by lazy {
        loadEmails()
    }
}

如上所示,一句话完成了前面的支持属性技术,并且线程时安全的。

lazy 函数可以传入 mode 参数控制线程同步,有三种模式:

  • SYNCHRONIZED:默认就是这个。该值只在一个线程中计算,所有的线程都可以看到相同的值,如果同时有多个线程访问,但是初始化过程没完成,只能等待着。
  • PUBLICATION:可以多个线程都进行初始化工作,最终所有线程使用的值都是第一个完成初始化线程的值,其他的线程即使初始化完成也不用了。(让我想到了受精过程,大家都在跑,最后就一个赢家,哈哈哈)。
  • NONE:没有任何的线程安全措施。就与上面的支持属性技术一样的。

实现委托属性

先创建一个接口和这个接口的包装类

interface Listener {
    fun propertyChange(proName: String, oldValue: Any?, newValue: Any?)
}

open class ListenerChangeWrap {

    private val listeners: ArrayList<Listener> = arrayListOf()

    fun addListener(l: Listener) {
        listeners.add(l)
    }

    fun removeListener(l: Listener) {
        listeners.remove(l)
    }

    fun propChange(proName: String, oldValue: Any?, newValue: Any?) {
        for (l in listeners) {
            l.propertyChange(proName, oldValue, newValue)
        }
    }
}

创建一个类,继承 ListenerChangeWrap ,在属性发生改变的时候调用 propChange 告诉监听器发生了属性发生了变化。

class Person1(val name: String, age: Int, address: String) : ListenerChangeWrap() {

    var age: Int = age
        set(value) {
            propChange("age", field, value)
            field = value
        }

    var address: String = address
        set(value) {
            propChange("address", field, value)
            field = value
        }
}

在 main 方法中调用,创建 Person1 实例,并创建监听器:

fun main(args: Array<String>) {
    val p = Person1("张三", 20, "天安门")
    p.addListener(object : Listener {
        override fun propertyChange(proName: String, oldValue: Any?, newValue: Any?) {
            println("$proName 改变了,由 $oldValue 变为了 $newValue")
        }
    })
    p.age = 33
    p.address = "故宫"
}
// 输出结果:
// age 改变了,由 20 变为了 33
// address 改变了,由 天安门 变为了 故宫

以上方法实现了属性变化的通知。但是有个思考,当前只有两个属性,我们可以这么做,如果有很多个属性,每个属性中的 set 方法都是大同小异的,我们可以提供一个辅助类来实现属性的变化通知:

class ObservableProperty(val proName: String,
                         var proValue: Any?,
                         val lw: ListenerChangeWrap) {

    fun getValue(): Any? = proValue

    fun setValue(newValue: Any?) {
        lw.propChange(proName, proValue, newValue)
        proValue = newValue
    }
}

在 Person1 中,我们可以使用支持属性技术,精简以下,以下新建一个类Person2

class Person2(val name: String, age: Int, address: String) : ListenerChangeWrap() {

    val observableAge = ObservableProperty("age", age, this)
    var age: Int
        get() = observableAge.getValue() as Int
        set(value) = observableAge.setValue(value)

    val observableAddress = ObservableProperty("address", address, this)
    var address: String
        get() = observableAddress.getValue() as String
        set(value) = observableAddress.setValue(value)
}

委托属性

其实上面的已经精简了许多了,但是有的时候我们还不想写属性的 get 和 set 方法怎么办,还是通过辅助类来实现,但是辅助类中的中的方法要使用约定的方法,签名保证正确,就可以使用 kotlin 内置的了:

class ObProperty<T>(var proValue: T,
                    val lw: ListenerChangeWrap) {
    operator fun setValue(p: Person3, prop: KProperty<*>, newValue: T) {
        lw.propChange(prop.name, proValue, newValue)
        proValue = newValue
    }

    operator fun getValue(person3: Person3, property: KProperty<*>) = proValue
}

如上,要使用 operator 修饰,另外创建 Person3 使用 by 关键字,将属性委托给 ObProperty ,之前的那些支持属性的逻辑就全部由 kotlin 编译器自动完成。

class Person3(val name: String, age: Int, address: String) : ListenerChangeWrap() {
    var age: Int by ObProperty(age, this)
    var address: String by ObProperty(address, this)
}

还可以在简单点么?

可以,你甚至不需要手动去实现可观察的属性逻辑,可以使用 kotlin 标准库,它已经实现了类时上面 ObProperty 类。

class Person4(val name: String, age: Int, address: String) : ListenerChangeWrap() {

    var age: Int by Delegates.observable(age) { prop: KProperty<*>, oldValue: Int, newValue: Int ->
        propChange(prop.name, oldValue, newValue)
    }

    var address: String by Delegates.observable(address) { prop: KProperty<*>, oldValue: String, newValue: String ->
        propChange(prop.name, oldValue, newValue)
    }
}

如上所示,直接使用 Delegates 类,就不需要自己去实现 getValue 和 setValue 方法啦。

看源码得知,Delegates #observable 方法在内部会创建 ObservableProperty 的匿名实例,而这个 ObservableProperty 原理就是上面的 ObProperty 。

委托属性的变换规则

其实上面就解释了委托属性的变化规则。

将属性委托给一个代理类 Delegate,类中实际上会生成该代理类的实例,属性的 get 和 set 方法都会委托给代理类实例的 getValue 和 setValue 方法,其实就是类似支持属性技术。

在map中保存属性值

自定对象:具有动态定义的属性集的对象。

class Person{
    private val _attrs = hashMapOf<String, String>()

    fun setAttrs(attrName: String, value: String) {
        _attrs.put(attrName, value)
    }

    val name : String by _attrs
}

fun main(args:Array<String>) {
    val p = Person()
    p.setAttrs("name" , "张三")
    p.setAttrs("address" , "北京")
    println(p.name) // 输出: 张三
    println(p.address) // 输出:北京
}

以上就是将属性委托到 map 中的做法,非常简单,只需要将 map 放在 by 关键字后面就可以。因为标注库已经在 Map 接口上定义了 getValue 和 setValue 方法,属性的名称将自动作用在 map 中的键,属性值作为 map 中的值。

高阶函数:lambda 做为参数和返回值

高阶函数:以一个函数为参数或者返回值的函数。在 kotlin 中,函数可以用 lambda 或者函数引用来表示。

在 java 中使用函数类:FunctionN 接口

背后原理:函数类型被声明为普通的接口,一个函数类型的变量是 FunctionN 接口的实现。

kotlin 标准库定义了一系列的接口,这些接口对应不同参数数量的接口。FunctionN ,N代表有多少个参数,0表示没有任何参数。

如下所示,在 kotlin 定义了一个有3个参数的方法,最后一个参数为 lambda 参数

fun twoProcessor(x:Int, y:Int, k: (x: Int, y: Int) -> Int) {
    println(k(x,y))
}

fun main(args: Array<String>) {
    val kotlinAddFun: (x: Int, y: Int) -> Int = { x, y -> x + y }
    twoProcessor(1,2, kotlinAddFun)
}

如果需要在 java 中调用 twoProcessor 方法,传入的最后一个参数必须是 Function2 的实现类,如下所示:

public class Test {

    public static void main(String[] args) {
        twoProcessor(1, 2,
                new Function2<Integer, Integer, Integer>() {
                    @Override
                    public Integer invoke(Integer x, Integer y) {
                        return x + y;
                    }
                });
        // java 8 可以使用lambda表达式
        twoProcessor(3, 4, (x, y) -> x + y);
    }
}

内联函数 inline

lambda 表达式会被编译成匿名类,这表示,每调用一次 lambda 表达式就会有一个额外的类被创建,如果 lambda 捕捉了某个变量,那么每次调用的时候都会创建一个新的对象。

如何解决上面的开销?可以将函数声明为 inline ,它的函数体会被直接替换到函数被调用的地方,而不是正常的被调用。

inline fun <T> sysHello(name:String,action: ()->T) {
    println("hello ,$name")
    action()
}

fun main(args: Array<String>) {
    sysHello("张三") {
        println("绰号法外狂徒")
    }
}

sysHello 函数在编译的时候会被内联,由 lambda 生成的字节码成为了函数调用者的一部分,而不是被包含在一个匿名接口的匿名类中,相当于如下:

fun main(args: Array<String>) {
    val name = "张三"
    println("hello ,$name")
    println("绰号法外狂徒")
}

内联函数是不可以传递函数类型的变量作为参数,这样的话,参数的内容是不会被内联的。

fun test2() {
    val action ={
        println("绰号法外狂徒")
    }
    sysHello("张三",action)
}

fun test3() {
    val action ={
        println("绰号法外狂徒")
    }
    val name = "张三"
    println("hello ,$name")
    action()
}

如上所示,内联函数传入的参数是一个函数类型的变量,内联的结果如 test3 所示, action变量的执行语句不会被内联,必须传入一个 lambda 表达式才行。

使用 inline 关键字只能提升带有 lambda 参数的函数的性能,其他情况需要自行判断。比如对于集合操作的序列(Sequence),在大量数据的时候推荐使用,在少量数据的时候不推荐?为何,因为他不是内联函数。

总结:

  • 如果 lambda 表达式 在某个地方保存起来,lambda 表达式的代码是无法被内联的,因为在某个地方 lambda 表达式被保存起来,并且赋值给一个变量(该变量持有这个 lambda 表达式的引用)。
  • 传入的参数是函数类型的参数,而不是 lambda 表达式,是无法内联的。
  • lambda 参数如果直接调用,或者作为参数传递给另一个 inline 函数,它是可以被内联的。

高阶函数控制流:@标签与匿名函数返回

return 语句的返回总结就一句话:从最近的 fun 关键字声明的函数返回。

非局部返回:如果在 lambda 中使用了 return 语句,他会从调用 lambda 的函数中返回,而不是从 lambda 代码块中返回。

val persons = listOf<String>("大牛", "二狗", "张三", "李四")

fun test() {
    print("start  ")
    persons.forEach() {
        if (it.equals("张三")) {
            return
        }
        print("$it , ")
    }
    print("  end")
}
// 输出:  start  大牛 , 二狗 , 

如上说是,调用 test 函数,return 语句会直接返回,不会再执行接下俩的动作。如果只是想返回到 forEach 函数呢?

fun test() {
    print("start  ")
    persons.forEach() lable@{
        if (it.equals("张三")) {
            return@lable
        }
        print("$it , ")
    }
    print("  end")
}
// 输出:start  大牛 , 二狗 , 李四 ,   end

在 forEach 使用 @ 定义一个标签(标签名称任意),然后可以在符合条件的直接返回到标签处,就可以不用直接返回到调用的函数了。上面代码中也可以不使用自定义的标签,可以直接使用函数名称作为抱歉,即 retrun@forEach。在使用函数名称作为标签的时候,不能再自定义标签,因为一个函数的标签只能由一个。

标签太多了危害:返回语句变得更加笨重,不推荐使用。要返回,推荐直接使用匿名函数。如下所示:

fun test() {
    print("start  ")
    persons.forEach(fun(s: String): Unit {
        if (s.equals("张三")) {
            return
        }
        print("$s , ")
    })
    print("  end")
}

使用匿名函数,默认是从局部返回的,不用到处加上标签啦。

泛型

泛型类型参数

泛型允许定义带有类型形参的类型,当这种类型的实例被创建出来的时候,类型形参就会被替换成类型实参的具体类型。

下面以泛型类为例子:

val generic = Generic<String>()

class Generic<T>

如上所示,类的定义中,T 就是类型形参,在具体的实例中,String 就是类型实参。

可以给类,接口,方法,顶层函数,扩展函数,扩展属性声明类型参数。

不可以给普通的属性声明类型参数,因为不能再一个类的属性中存储多个不同类型的值,例如下面是不可以的。

class Generic<T>{
    // 该语句会报错,不可以给属性声明类型形参
    val <E> test:E
        get() {
            TODO()
        }

}

如上所示,给 test 属性声明了类型参数,实际中会报错。

类型参数约束

类型参数约束可以限制类型实参的类型。

fun <T : Number> List<T>.sum(): T {
    // ....
}

如上所示,给 T 添加了上界限约束,所有的类型实参必须是上界的子类型。

默认情况下,类型参数的上界都是 Any?,所以,类型实参是可以传入 null 的,如果不想传入 null,可以限制类型参数的上界是 Any 类。

再某些情况下,可能存在多个类型上界约束,需要这样写:

fun <T> List<T>.change(): T where T : CharSequence, T : Runnable {
    // ...
}

再方法后面,使用 where 语句限定多个上界即可。

运行时泛型:擦除和实化类型参数

类型擦除:参考java的类型擦除。jvm 再运行时,泛型类的类型实参在运行时是不保留的。kotlin 的泛型在运行时也是被擦除的。但是,在 kotlin 中,可以通过 inline 函数,是其类型参数不被擦除,这一操作就叫做实化。

类型擦除对类型检查,类型转换影响

fun main() {
    val intList = listOf<Int>(1, 2, 3)
    val floatList = listOf<Float>(1.3f, 2.6f, 3.9f)
    val strList = listOf<String>("hello , kotlin")

    listToast(intList)
    listToast(floatList)
    listToast(strList)
}

// 星号投影,用于表示未知类型实参的泛型类型。类似java中的?
fun listToast(c: Collection<*>) {
    val intList = c as? List<Int>
            ?: throw IllegalArgumentException("类型转换错误")
    intList.sum()
}

fun <T : Number> List<T>.sum() {
    var result = 0
    for (ele in this) {
        result += ele.toInt()
    }
    println("list值总和:$result")
}

如上所示,我们在调用了三个不同类型实参的 List。

  • 类型实参为 Int,结果输出6,符合预期
  • 类型实参为 Float,结果输出6,符合预期
  • 类型实参为 String,报错:java.lang.String cannot be cast to java.lang.Number

对于第三个,我们的预期有点差距,在 listToast 函数中,类型转换成功了,居然真的把 List 转换成了 List。但是在调用 sum 函数的时候报错了。

为何会这样?因为类型擦除,List后面的类型实参在运行时候被擦除了,一律认为是List,所以 c as? List 转换成功。

者就告诉了我们,因为类型擦除,导致了类型转换与类型检查都是失效的,需要自己去判断,编译器是无法判断出对错的。

实化类型参数

实化,就是不让泛型进行类型擦除。

内联函数的类型参数能够被实化,即意味着在运行时引用实际的类型实参。

kotlin 中的方法就是通过内联函数并且在泛型声明中使用 reified 关键字。做法如下所示:

// 错误写法
 fun <T> isT(value: Any) {
    if (value is T) {
        println("$value is ${T::class.java}")
    } else {
        println("$value not ${T::class.java}")
    }
}

// 正确写法
inline fun <reified T> isT(value: Any) {
    if (value is T) {
        println("$value is ${T::class.java}")
    } else {
        println("$value not ${T::class.java}")
    }
}

为啥 inline 函数可以让泛型的类型实参实化呢?因为用 inline 标记一个函数,编译器会把每一次函数调用都换成函数实际的代码实现。

但是,内联函数还是仅仅不够的,还需要将类型形参使用 reified 关键字进行标记,在运行的时候替换代码就会用具体的类型实参代替原来的 T。

注意,带有 reified 类型形参的内联函数时不可以在 java 中调用的。

推荐使用实化类型参数场景:

  • 用于类型检查与类型转换:is,as 等
  • 用于 kotlin 反射 API,即引用 ::class
  • 用于获取响应的 java.lang.Class,即 ::class.java
  • 作为类型实参给其他函数调用

实话类型参数只能用在内联函数上,意味着函数和所有传给他的 lambda 都会被内联。如果不想 lambda 被内联,可以在声明函数的时候在参数上使用 noinline 标记。

变型

子类型与子类

任何时候,如果需要类型A的值,你都能是使用类型B的值当作A的值,那么类型B就是A的子类型,反之A就是B的超类型。

子类一般就是子类型,但是子类型不是子类。

非空类型A是可空类型A?的子类型。

对于两个任意类型的A,B,如果G即不是G的子类型也不是超类型,就称 G 这个泛型类在该类型参数上是不变型。所有的Java类都是不变型的。

变型的意义

fun addInt(list: MutableList<Any>) {
    list.add(10)
}

fun main() {
    val list = mutableListOf<String>("hello world")

    // 如果下面这行成立,很显然会出错。
    //    addInt(list)
}

如上所示,如果简单的认为 MutableList 是 MutableList 的子类型,显然就会出错。

变型分为两种,协变与逆变。

声明点变型:协变与逆变

变型修饰符:in,out

声明点变型:在类成员中声明中对类型参数 T 时候使用 in ,out 关键字修饰 。

如果 A 是 B 的子类型,那么 Product 就是 Product 的子类型,我们就说子类型化被保留了,即协变。相反,如果 Consumer 是 Consumer的子类型,我们就说子类型化关系被反转了,即逆变。

要说明协变,只需要在类型形参上加上 out 关键字即可。将一个类的类型参数标记为协变,在该类型实参没有精确匹配到函数中定义的类型实参时,可以让该类的值作为这个函数的实参传递,也可以作为这个函数的返回值。即在类型参数T上加上 out 关键字有两层含义:

  • 子类型化被保留
  • T 只能用在 out 位置(即函数返回值位置)

要说明逆变,只需要在类型形参上加上 in 关键字即可。在类型参数 T 加上 in 关键字两层含义:

  • 子类型化被反转了
  • T 只能用在 in 位置(即函数的类型参数位置)

如果把类的实例当作一个更泛化的类型的实例使用,变型的作用就是防止该实例被误用,即防止调用存在潜在危险的方法。但是在某些方法上是不可能存在这些危险的,所以,即使标记了类的类型参数是 out ,in,对他们都是没有影响的:

  • 构造函数(显然,类被创建后构造函数就不会在用到,有啥危险?)
  • private 方法(类的实例根本没法调用,不可能有危险)

即对于构造函数,private 方法,即使类型参数使用了 out 或者 in 修饰,也是可以随便使用,反正也不会有危险,管他在哪个位置呢。

除了协变,逆变,还有不变型,即再参数上没有加 out 也没有加 in,他是没有任何安全措施的。

使用点变型

使用点变型:每一次使用带类型参数的类型的时候,指定这个类型参数是否可以用它的子类型或者超类型替换。其实就是类似与 java 中的通配符。

使用点变型的作用:有助于放宽函数可接受的类型的范围。

声明点变型与使用点变型的定义就可以看到两者在使用位置上的差别:

  • 声明点变型:用在类的定义中
  • 使用点变型:用在方法的定义中

如下所示,我们定义一个函数:

fun <T> copyData(src: MutableList<T>, dest: MutableList<T>) {
    for (item in src) {
        dest.add(item)
    }
}

fun main() {
    val src = mutableListOf<String>("hello", "kotlin")
    val dest = mutableListOf<String>()
    copyData(src, dest)
    println("dest : $dest")
}

如上所示,copyData就是将 src 所有子元素复制到 dest 中。

在 main 方法中,如果 src 和 dest 的元素都是 String 类型,完全没问题。但是如果 src 的元素类型是 String ,而 dest 的元素类型是 Any,按照我们正常的理解,是完全可以使用 copyData 复制的,但是调用该方法的时候会报错,咋办?

首先我们使用类似 Java 通配符的方式来解决

fun <T : R, R> copyData2(src: MutableList<T>, dest: MutableList<R>) {
    for (item in src) {
        dest.add(item)
    }
}

fun main() {
    val src = mutableListOf<String>("hello", "kotlin")
    val dest = mutableListOf<Any>()
    copyData2(src, dest)
    println("dest : $dest")
}

ok,顺利解决。但是 kotlin 提供了一种更简单的方法。src 中,T 显然只是在 out 位置,而 dest 中,T 只在 in 位置,所以,所以,协变逆变正好可以用上。

fun <T> copyData3(src: MutableList<out T>, dest: MutableList<T>) {
    for (item in src) {
        dest.add(item)
    }
}

在src 的位置声明了类型参数是 out 位置,那么其他地方的 T 不需要任何修饰符,就默认为在 in 位置。当然,也可以按照在 dest 的类型参数上使用 in 修饰符,可以达到同样效果。

fun <T> copyData4(src: MutableList<T>, dest: MutableList<in T>) {
    for (item in src) {
        dest.add(item)
    }
}

其中 copyData3 和 copyData4 方法中,使用的就是使用点变型。

上面发生的其实就是一个投影。copyData3 中发生的是 out 投影,即 src 的类型参数是 T 的 out 投影,它的功能是受到限制的,即只能在 out 位置使用 T。同样,copyData4 中发生的是 in 投影。

上面的方法只是一个演示,src 显然可以直接使用 List 代替使用 MutableList, 而 List 是只读的,天生自带 out 投影。

星号投影 *

上面的使用点变型说到了投影,都是在方法的参数中加上 in 或者 out 限制类型参数 T 的位置。

星号投影:表明你不知道关于泛型实参的任何信息。对应 java 中的 ?

注意信号投影与 Any? 的区别,比如 MutableList<*> 表示是包含某种特定元素的列表,但是你不知道这种元素的类型是什么,而 MutableList<Any?> 表示这个列表可以包含任何元素。

当类型实参的信息并不重要的时候,可以使用星号投影的语法:不需要使用任何在签名中引用类型参数的方法,或者只是读取数据而不关心这个数据的具体类型。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值