Kotlin学习记录(二)标准函数、静态方法、延迟初始化和密封类、扩展函数和运算符重载、高阶函数、高阶函数的应用、泛型、类委托和委托属性
七、标准函数和静态方法
Activity::class.java
相当于Activity.class
所有定义在companion object
中的方法都可以使用类似于Java静态方法的形式调用。
7.1 标准函数with、run和apply
Kotlin中标准函数指的是Standard.kt文件
中定义的函数,任何Kotlin代码都可以自由调用所有标准函数,例如let函数
。
with函数
with函数
接收两个参数,第一个是任意类型的对象,第二个是Lambda
表达式。with
函数会在Lambda
表达式中提供第一个参数对象的上下文,并用Lambda表达式中最后一行代码作为返回值返回。
val result = with(object) {
//obj的上下文
"value" //with函数的返回值
}
当我们连续调用多次builder对象的方法时,可以考虑用with函数让代码变得精简。如果用StringBuilder**作为with第一个参数,**那么接下来Lambda表达式的上下文就会是这个StringBuilder对象,于是我们在Lambda表达式中就可以直接调用append()和toString()
run函数
run函数
通常要在某个对象基础上调用,只接收一个Lambda参数,并在Lambda表达式中提供调用对象的上下文,使用Lambda表达式中最后一行代码作为返回值。
val result = obj.run {
//obj的上下文
"value" //run函数的返回值
}
apply函数
apply函数
通常要在某个对象基础上调用,只接收一个Lambda参数,并在Lambda表达式中提供调用对象的上下文,但无法指定返回值
,而自动返回调用对象本身
。
val result = obj.apply{
//obj的上下文
}
传的参数越多,以上三种方法优势越明显。
7.2 定义静态方法
静态方法在某些编程语言中叫做类方法,指**不需要创建实例就能调用的方法,**适用于编写一些工具类,因为工具类通常没有创建实例的必要,基本是全局通用的。
Kotlin中极度弱化了静态方法概念,因为提供了比静态方法更好用的单例类。直接通过Util.doAction()
调用。
object Util {
fun doAction(){
println("do action")
}
}
- 单例类中直接定义的方法一定要先创建Util类才能调用,而compaion object中的方法可以直接使用Util.doAction2()的方式调用
companion object
class Util {
fun doAction1(){ //先创建Util类的实例才能调用
println("do action1")
}
companion object{
fun doAction2(){
println("do action2")
}
}
}
companion object
会在Util类的内部创建一个伴生类,doAction2()是定义在这个伴生类里面的实例方法,Kotlin会保证Util类始终只会存在一个伴生类对象,调用Util.doAction2()方法实际调用了Util类中伴生对象的doAction2
真正的静态方法:注解和顶层方法
@JvmStatic
- 如果给单例类或companion object中的方法加上
@JvmStatic
注解,Kotlin编译器才会将这些方法编译成真正的静态方法。
companion object{
@JvmStatic
fun doAction2(){
println("do action2")
}
}
顶层方法
指没有定义在任何类中的方法,Kotlin编译器会将所有顶层方法编译成静态方法。Kotlin中顶层方法可以在**任何位置被直接调用,不用管包名路径,**也不需要创建实例。
Java中所有方法必须定义在类中,没有顶层方法的概念。
文件名叫做Helper.kt,于是Kotlin编译器会自动创建一个叫做**HelprKt的Java类,顶层方法以静态方法的形式定义在HelperKt类中,**于是在Java中使用HelpKt.doSomething()
来调用就可以了。
八、延迟初始化和密封类
Kotlin中强制转换使用的关键字是as。
repeat也是一个非常常用的标准函数传入一个,然后把Lambda表达式中内容执行n遍。
8.1 对变量延迟初始化
当代码中有越来越多全局变量实例,用延迟初始化可以解决必须编写大量额外判空处理代码问题。
- 延迟初始化使用
lateinit关键字
,告诉Kotlin编译器,会在晚些时候对这个变量进行初始化,这样就**不用在一开始将它赋值为null,**声明类型后面也不用加?了。lateinit
关键字也有一定风险,如果在还未完成初始化时调用程序就会抛出UninitializedPropertyAccessException
异常。 ::adapter.isInitialized
用于判断adapter是否已经初始化。if(!::adapter.isInitialized)
8.2 使用密封类优化代码
在when语句中传入一个密封类变量作为条件时,Kotlin会强制要求将每一个子类所对应的条件全部处理,从而保证没有编写else条件,也不可能出现漏写条件分支的情况。。有时会为了满足编译器要求而编写无用条件分支。使用Kotlin的密封类可以很好解决这个问题。密封类关键字是sealed class
。新建Result.kt
密封类及其所有子类只能定义在同一个文件的顶层位置,不能嵌套在其他类中
sealed class Result
class Success(val msg : String) : Result()
class Failure(val error : Exception) : Result()
//密封类是一个可继承的类,继承时需要在后面**加上一对括号**。
//使用:
fun getResultMsg(result:Result) = when(result){
is Success ->result.msg
is Failure ->"Error is ${result.error.message}"
//没有else条件,Kotlin编译器会自动检查该密封类有哪些子类
//并强制要求每个子类所对应的条件全部处理
}
九、扩展函数和运算符重载
9.1 大有用途的扩展函数
扩展函数表示即使不修改某个类源码的情况下,依然能打开这个类添加新的函数。可以帮助我们用更加面向对象的思维解决这个功能。
- 定义扩展函数只需要在函数名前面加一个
ClassName.
。 - 最好将扩展函数定义成顶层方法,使扩展函数拥有全局的访问域。
- 利用好扩展函数将大幅提升代码质量和开发效率。
9.2 有趣的运算符重载
将lettersCount()定义成String类的扩展函数
fun String.lettersCount():Int{
var count = 0
for(char in this){
if(chard.isLetter()){
count++
}
}
return count
}
operator关键字
- 指定
函数
前加上opeartor关键字
就可以实现运算符重载的功能。 - 运算重载可以让对象和对象相加,对象和数字相加等等。
class Money(val value : Int) {
**operator** fun plus(money: Money):Money{
val sum = value + money.value
return Money(sum)
}
}
//使用:
val money1 = Money(5)
val money2 = Money(10)
val money3 = money1+money2
println(money3.value)
Kotlin允许我们对同一个运算符进行多重重载。
class Money(val value : Int) {
operator fun plus(money: Money):Money{
val sum = value + money.value
return Money(sum)
}
operator fun plus(newValue:Int):Money{//使money和数字直接相加
val sum = value + newValue;
return Money(sum)
}
}
Kotlin允许我们重载的运算符和关键字多达十几个。
a in b
表示判断a是否在b当中,也就是b.contains(a);
例:要让字符串可以乘以一个数字,肯定要在String类中重载乘号运算符才行,根据语法糖表达式和实际调用函数对照表,重载乘号运算符函数名必须是times
operator fun String.times(n:Int):String{
val builder = StringBuilder()
repeat(n){
builder.append(this)//this为调用times的字符串
}
return builder.toString()
}
//现在字符串就有了和一个数字相乘的能力
val str = "abc" * 3
println(str)
times()
函数还可以进一步精简为
operator fun String.times(n:Int) = repeat(n)
就可以在getRandomLengthString()函数中使用
fun getRandomLengthString(str:String) = str*(1..20).random()
十、高阶函数
高阶函数——一个函数接收另一个函数作为参数,或者返回值的类型是另一个函数,这个函数是高阶函数。
作用:允许让函数类型的参数来决定函数的执行逻辑
正如字段类型有有整型、布尔型等,Kotlin增加了函数类型的概念。
例:
(String,Int) -> Unit
fun example(func:(String,Int) -> Unit){
func("hello",123)
}
左边是参数类型,如果不接收任何参数写一对空括号就好,右边声明该函数返回值是什么,没有返回值就使用Unit,相当于Java中的void。
像Kotlin集合相关函数式API如map、filter函数,Kotlin标准函数中的run、apply函数等,这种接收Lambda参数的函数就可以称为具有函数式编程风格的API。如果希望定义自己的函数式API,就必须借助高阶函数实现。
10.1 定义高阶函数
fun num1AndNum2(num1:Int,num2:Int,operation:(Int,Int)->Int):Int{
val result = operation(num1,num2)
return result;
}
fun plus(num1: Int,num2: Int):Int {
return num1+num2;
}
fun minus(num1: Int,num2: Int):Int {
return num1-num2;
}
fun main(){
val num1 = 100
val num2 = 80
//::plus是一种函数引用方式的写法,表示plus()函数作为参数传递给num1AndNum2()函数
val result1 = num1AndNum2(num1,num2,::plus)
val result2 = num1AndNum2(num1,num2,::minus)
Lambda表达式写法,Lambda参数是函数的最后一个参数时,将Lambda表达式移到函数括号的外面
//val result1 = num1AndNum2(num1,num2){n1,n2 -> n1+n2}
//val result2 = num1AndNum2(num1,num2){n1,n2 -> n1-n2}
println("result1 is $result1")
println("result2 is $result2")
}
- 在函数类型前面加上
ClassName.
表示函数定义在哪个类中。
StringBuilder.build表示给StringBuilder类定义了一个build扩展函数
StringBuilder.()表示这个函数类型定义在StringBuilder类中。
- 将函数类型定义在
StringBuilder类
中好处:build函数传入的Lambda表达式会自动拥有StringBuilder
的上下文。
fun StringBuilder.build(block : StringBuilder.() → Unit) : StringBuilder{
block()
return this
}
- block是函数名,方便在build函数内部调用
高阶函数内部的实现原理:Lambda表达式在底层被转换成了匿名类的实现方式,每调用一次Lambda表达式,都会创建一个新的匿名类实例,会造成额外的内存和性能开销。
10.2 内联函数的作用
Kotlin代码最终是要编译成Java字节码的,Java中并没有高阶函数的概念。
fun num1AndNumber2(num1: Int, num2: Int, operator: (Int, Int) -> Int): Int {
return operator(num1, num2)
}
fun main() {
val num1 = 100
val num2 = 80
val result = num1AndNumber2(num1, num2) { n1, n2 ->
n1 + n2
}
println(result)
}
上述的Kotlin代码大致会转成下面的Java代码:
public static int num1AndNum2(int num1, int num2, Function operation) {
int result = (int)operation.invoke(num1, num2);
return result;
}
public static void main() {
int num1 = 100;
int num2 = 80;
int result = num1AndNum2(num1, num2, new Function() {
@Override
public Integer invoke(Integer n1, Integer n2) {
return n1 + n2;
}
});
}
上面的代码是进行了调整的,并不是严格对应了Kotlin转换成的Java代码。这里可以看到,num1AndNum2()
函数的第三个参数变成了Function
接口,这个是Kotlin内置的接口,里面有一个待实现的invoke()
函数。而num1AndNum2()
就是调用了Function
接口的invoke()
函数,并将num1
和num2
参数传了进去。
在调用num1AndNum2()
函数的时候,之前的Lambda表达式在这里变成了Function
接口的匿名类实现,然后在invoke()
函数中实现了n1 + n2
的逻辑,并将结果返回。
- 每调用一次Lambda都会创建一个新的匿名类实例,会造成额外的内存和性能开销。内联函数可以将Lambda表达式带来的运行时开销完全消除。
- 用法:定义高阶函数时加上
inline关键字
声明。
内联函数原理:Kotlin编译器会将内联函数中代码在编译时替换到函数类型参数调用的地方。
例如从num1AndNum2
的函数体中替换到main
中。
10.3 noinline与crossline
- Kotlin编译器会将加了
inline关键字
的高阶函数被引用的所有Lambda表达式全部进行内联。
但如果只想内联其中一个,就要使用noinline关键字
。
inline fun inlineTest(block1: ()->Unit, noinline block2: ()->Unit){
TODO("Implementation this")
}
这里使用了inline
关键字声明了inlineTest()
函数,原本block1()
和block2()
这两个函数类型参数所引用的Lambda表达式都会被内联。但是我们在block2
参数的前面加上了noinline
关键字,那么现在只会对block1
参数所引用的Lambda表达式进行内联了,这就是noinline
关键字的作用。
为什么还要提供
noinline
关键字来排除内连功能。
内联的函数类型参数在编译时会被进行代码替换,因此内联的函数类型参数没有真正的参数属性
。只能允许被传递给另一个内联函数。
而非内联的函数类型参数是一个真实的参数可以传递给其他任何函数。
- 内联函数引用的Lambda表达式可以用
return关键字
进行函数返回。return代表返回外层调用函数。
因为它替换了外层调用函数中的代码。
非内联函数只能进行局部返回。例如使用return@printString
,并且不再执行Lambda表达式剩余部分代码。
fun printString(str: String, block: (String) -> Unit) {
println("printString begin")
block(str)
println("printString end")
}
fun main() {
println("main start")
val str = ""
printString(str) { s ->
println("Lambda start")
if (s.isEmpty()) return@printString // 局部返回
//Lambda表达式中是不允许直接使用return关键字的,
//上面使用了return@printString进行局部返回,并且不再执行Lambda表达式的剩余部分代码。
println(s)
println("Lambda finish")
}
println("main end")
}
//输出
// main start
// printString begin
// Lambda start
// printString end
// main end
如果我们我们将printSring()
函数声明成一个内连函数,就能在Lambda表达式中使用return关键字了
inline fun printString(str:String,block:(String) -> Unit){
println("printString begin")
block(str)
println("printString end")
}
fun main(){
println("main start")
val str = ""
printString(str){
s->
println("lambda start")
if(s.isEmpty()) return
println(s)
println("lambda end")
}
println("main end")
}
// 输出
// main start
// printString begin
// Lambda start
此时return返回外层调用函数,也就是main()。
crossinline
将高阶函数声明成内联函数是一种良好的编程习惯,绝大部分高阶函数可以直接声明成内联函数,以下是例外。
inline fun runRunnable(block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}
加上inline
关键字之后,上面的代码在block部分会有错误提示Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'
这样写会报错:
在runRunnable()函数中,创建了一个Runnable对象,在Runnable的Lambda表达式中调用了传入的**函数类型参数。**Lambda表达式在编译时会被转换成匿名类的实现方式——在匿名类中调用了传入的函数类型参数;而内联函数的Lambda表达式允许使用return关键字进行函数返回,但是在匿名类中调用了的函数类型参数不可能进行外层调用函数返回,最多只能对匿名类中的函数调用进行返回,这里出错。
所以:在高阶函数中创建了另外的Lambda或匿名类的实现,在这些实现中调用函数类型参数,再将高阶函数声明成内联函数,一定会提示错误
借助crossinline关键字解决
inline fun runRunnable(**crossinline** block:() -> Unit){
val runnable = Runnable{
block()
}
runnable.run()
}
crossinline,用于保证在内联函数的Lambda表达式中一定不会使用return关键字。上面的代码之所以在没有添加crossinline
时会报错,就是因为内联函数的Lambda表达式中允许使用return
关键字,和高阶函数的匿名类实现中不允许使用return
关键字之间造成了冲突。
声明了crossinline后,无法在调用runRunnable函数时的Lambda表达式中使用return关键字进行函数返回了,但是仍然可以使用return@runRunable进行局部返回。
十一、高阶函数的应用
11.1 简化SharedPreferences的用法
原来的用法,Java思维:
val editor = getSharedPreferences("data", Context.MODE_PRIVATE).edit()
editor.putString("name","Tom")
editor.putInt("age",28)
editor.putBoolean("married",false)
editor.apply()
简化之后:
fun SharedPreferences.open(block: SharedPreferences.Editor.() ->Unit){
val editor = edit()
editor.block()
editor.apply()
}
1.用**扩展函数**的方式向SharedPreferences类中添加了一个open函数,
并且还接收一个函数类型的参数,所以open是一个高阶函数
2.open内拥有SharedPreferences上下文,因此可以直接调用edit()来获取Editor对象
val editor 接收的是一个 SharedPreferences.Editor.()的函数类型参数,
这里需要调用editor.block()**对函数类型参数进行调用**
这样就可以在函数类型参数的具体实现中添加数据
3.最后调用editor.apply()方法提交数据
使用:
getSharedPreferences("data",Context.MODE_PRIVATE).open {
putString("name","Tom")
putInt("age",28)
putBoolean("married",false)
}
//注意:现在Lambda表达式拥有的是SharedPreferences.Editor的上下文环境,
//因此可以直接调用put添加数据,open会自动调用apply()会自动提交
//其实Google提供的KTX扩展库中已经包含了上述SharedPreferences的简化用法。
//我们可以在项目中直接使用edit函数
getSharedPreferences("data", Context.MODE_PRIVATE).edit {
putString("name", "Tom")
putString("age", 10)
}
11.2 简化ContentValues的用法
ContentValues主要用于结合SQLiteDatabase的API存储和修改数据库中的数据:
val values= ContentValues()
values.put("name","Game of Thrones")
values.put("author","George Martin")
values.put("pages","720")
values.put("Book",null,values)
除了用apply函数简化,还可以做到更好。
Kotlin中 A to B这样的语法结构会创建一个Pair
对象。
- 定义cv方法
fun cvOf(vararg pairs:Pair<String,Any?>):ContentValues{
}
接收一个Pair参数,A to B创造出的参数类型。
vararg关键字对应Java中可变参数列表,允许向这个方法传入0个、1个、任意多个Pair类型参数。
//方法作用:构建一个ContentValues对象
Pair是一种键值对的数据结构,因此需要通过泛型指定键和值分别对应什么类型数据。ContentValues所有键都是字符串类型,其值却可以有多种类型,所以需要将Pair值泛型指定成Any?。因为Any是Kotlin所有类的共同基类,相当于Java中Object,?表示允许传入空值。
fun cvOf(vararg pairs:Pair<String,Any?>):ContentValues{
val cv = ContentValues()
for(pair in pairs){//遍历pairs参数
**val key = pair.first
val value = pair.second**
when(value){//逐个添加到ContentValues中
is Int -> cv.put(key,value)
is Long -> cv.put(key,value)
is Short -> cv.put(key,value)
is Float -> cv.put(key,value)
is Double -> cv.put(key,value)
is Boolean -> cv.put(key,value)
is String -> cv.put(key,value)
is Byte -> cv.put(key,value)
is ByteArray -> cv.put(key,value)
null -> cv.putNull(key)
}
}
return cv
}
Kotlin使用了Smart Cast功能,比如when语句进入Int条件分支后,这个条件下面value会被自动转换成Int类型,而不再是Any?类型
val values = cvOf("name" to "Game of Thrones","author" to "George Martin",
"pages" to 720,"price" to 20.85)
db.insert("Book",null,values)
//KTX库contentValuesOf
类似于mapOf()函数语法结构构建ContentValues对象。
fun cvOf(vararg pairs:Pair<String,Any?>) = ContentValues().apply{
//apply函数返回它的调用对象本身
for(pair in pairs){
val key = pair.first
val value = pair.second
when(value){
is Int -> put(key,value)
is Long -> put(key,value)
is Short -> put(key,value)
is Float -> put(key,value)
is Double -> put(key,value)
is Boolean -> put(key,value)
is String -> put(key,value)
is Byte -> put(key,value)
is ByteArray -> put(key,value)
null -> putNull(key)
//Lambda表达式中会自动拥有ContentValues的上下文,这里直接调用各种put方法
}
}
}
11.3 infix函数
我们已多次使用A to B构建键值对。但to并不是Kotlin语言中的一个关键字,我们能使用A to B的语法结构是因为Kotlin提供了一种高级语法糖特性:infix
函数,A to B这种写法实际上等价于·
例子1:String类的startsWith函数(用于判断一个字符串是否以指定参数开头)
if("Hello Kotlin".startsWith("Hello")){
//处理具体逻辑
}
//新建一个infix.kt
//用infix函数更具可读性的语法表达startsWith(),beginsWith()内部还是调用startsWith
infix fun String.beginsWith(prefix : String) = startsWith(prefix)
infix函数允许我们将函数调用时的小数点、括号等计算机相关的语法去掉。
//调用
if("Hello Kotlin" beginsWith "Hello"){
//处理具体逻辑
}
infix函数由于语法糖格式特殊性,有两个比较严格的限制,特点:
- infix函数不能定义成顶层函数
- infix函数必须是某个类的成员函数
- 可以使用扩展函数的方式将infix函数定义到某个类中
- infix函数必须接收且只能接收一个参数
例子2:集合
val list = listOf("Apple","Banana","Orange","Pear","Grape")
if(list.contains("Banana")){
//处理具体逻辑
}
//借助infix函数让这段代码变得更加具有可读性
infix fun <T> Collection<T>.has(element:T) = contains(element)
//我们给Collection接口添加了一个扩展函数,Collection是Java和Kotlin所有集合的总接口
//使用
if(list has "Banana"){
//处理具体逻辑
}
mapOf()函数中允许我们使用A to B这样的语法来构建键值对,to()还是使用了infix函数,to()函数的源码:
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
- 将to()函数定义在了A类型下,并接收一个B类型的参数,A和B可以是两种不同类型的泛型,使得我们可以构建出字符串to整型的键值对
- Pair对象,A to B 创建并返回了一个包含A,B数据的Pair对象。也就是说A to B实际得到了包含A、B数据的Pair对象。
infix fun <A,B> A.with(that: B):Pair<A,B> = Pair(this,that)
//mapOf()函数接收的是Pair类型的可变参数列表。
val map = mapOf("Apple" with 1,"Banana" with 2,"Orange" with 3,"Pear"with 4,"Grape" with 5)
十二、泛型
12.1 泛型的基本用法
泛型,我们需要给任何一个变量指定一个具体的类型,泛型允许我们在不指定具体类型的情况下进行编程
两种定义方式:定义泛型类和定义泛型方法
- 定义泛型类
class MyClass<T>{
fun method(param : T):T{
return param
}
}
//MyClass是一个泛型类,允许使用T类型参数和返回值
//调用时
val myClass = MyClass<Int>()
val result = myClass.method(123)
- 可以将method()的泛型指定成任意类型定义泛型方法
class MyClass {
fun <T>method(param : T):T{
return param
}
}
//调用
val myClass = MyClass()
val result = myClass.method<Int>(123)
//类型推导机制简化
val myClass = MyClass()
val result = myClass.method(123)
- Kotlin允许我们对泛型类型进行限制,这里将method()泛型上界设置为Number类型。
class MyClass {
fun <T:Number>method(param : T):T{
return param
}
}
//只能将method()方法的泛型指定成数字类型,Int、Float、Double
//不指定泛型时,泛型的上界默认是Any?
上界默认是Any?,不想让泛型为空需要手动指定为Any
- build函数作用与apply函数基本一样,但build函数只能作用在StringBuilder类上,通过泛型让它作用在所有类上。
fun StringBuilder.build(block:StringBuilder.() -> Unit):StringBuilder{
block()
return this
}
//修改
fun <T> T.build(block: T.() -> Unit): T {
block()
return this
}
XXX? .build{
}
十三、泛型的高级特性
13.1 对泛型进行实化
13.1.1 Java的泛型擦除机制
JDK 5中,Java引入了泛型功能。关于泛型详细内容可以看这篇文章https://blog.csdn.net/qq_53749266/article/details/117486714?spm=1001.2014.3001.5502
实际上Java的泛型功能通过类型擦除机制来实现,意思是,泛型对于类型的约束只在编译时期存在,运行时仍会按照JDK5之前的机制来运行,JVM识别不出来在代码中指定的泛型类型。
Kotlin也是一样,所以不能使用a is T
或 T::class.java
这样的语法,因为T的实际类型在运行时已经被擦除了。
非Java之处是,Kotlin有内联函数,内联函数的代码在编译时会自动被替换到调用它的地方,编译后会直接使用实际的类型替代内联函数中的泛型声明,不存在泛型擦除的问题。
变成
fun foo(){
//do something with String type
}
这意味着Kotlin可以将内联函数中的泛型进行实化
13.2 怎样才能将泛型实例化?
- 该函数必须是内联函数,用
inline
关键字修饰 - 在泛型声明的地方必须加上
reified
关键字表示该泛型要进行实例化
inline fun <reified T> getGenericType(){
}
实现获取泛型实际类型的功能,getGenericType()返回当前指定泛型的实际类型。
inline fun <reified T> getGenericType() = T::class.java
//调用
fun main(){
val result1 = getGenericType<String>()
val result2 = getGenericType<Int>()
println("result1 is $result1")
println("result2 is $result2")
}
结果:
13.3 泛型实化的应用
泛型实化功能允许我们在泛型函数中获得泛型的实际类型,使得类似于a is T、T::class.java
这样的语法成为了可能
- 启动一个Activity
val intent = Intent(context,TestActivity::class.java)
context.startActivity(intent)
- 新建一个reified.kt文件,使用实化泛型的方法启动activity
/**
* Intent 的第二个参数本该是一个具体的 Activity 的 Class 类型,
* 但由于 T 已经是一个被实化的泛型了,因此这里可直接传入 **T::class.java。**
*/
inline fun <reified T> startActivity(context:Context){
val intent = Intent(context,T::class.java)
context.startActivity(intent)
}
- 启动TestActivity
startActivity<TestActivity>(context)
- 如果我们希望使用Intent带一些参数
/**
* startActivity函数重载,
* 增加了一个函数类型参数,并且它的函数类型是定义在 Intent 类当中的
*/
inline fun <reified T> startActivity(context: Context,block:Intent.() -> Unit){
//在创建完Intent的实例之后,随即调用该函数类型参数,并把Intent的实例传入
val intent = Intent(context,T::class.java)
intent.block()
context.startActivity(intent)
}
- 这样调用startActivity()函数时,就可以在Lambda表达式中为Intent传递参数了
startActivity<TestActivity>(context){
putExtra("param1","data")
putExtra("param2",123)
}
13.4 泛型的协变
Kotlin 的内置 API 中使用了很多协变和逆变的特性,如果想对Kotlin有更深的了解就需要好好学习这部分。
- 首先了解一个约定。一个泛型类或者泛型接口中的方法,它的参数列表是接收数据的地方,因此可以称它为 in 位置,而它的返回值是输出数据的地方,因此可以称它为 out 位置。
假设某个方法接收一个 Persion 类型参数,这时传入 Student 的实例是合法的。
但如果某个方法接收一个 List 类型参数,这时传入一个 List 的实例,在 Java 中是不允许的,因为 List 不能成为 List的子类,否则将可能存在类型转换的安全隐患。为什么存在类型转换安全隐患?看代码:
open class Person(val name:String,val age:Int)
class Student (name:String,age:Int):Person(name,age)
class Teacher (name:String,age:Int):Person(name,age)
class SimpleData <T>{
private var data: T?=null
fun set(t:T){
data = t;
}
fun get():T?{
return data
}
}
class Simple {
fun main(){
val student = Student("Tom",19)
val data = SimpleData<Student>()
data.set(student)//将studnet封装入SimpleData<Student>
handleSimpleData(data)// 实际上这行代码会报错,这里假设它能编译通过
val studentData = data.get()//类型转换异常
}
// 报错原因:
// 传入的是 SimpleData<Student>,
// 但创建的是 Teacher 的实例用来替换 SimpleData<Person> 参数中的原有数据,合法。
// 但上述 data.get() 获取内部封装的 Student 数据,
// 可现在 SimpleData<Student> 实际包含的却是一个 Teacher 实例,这里必然会产生**类型转换异常**。
// 所以,Java 是不允许使用这种方式传递参数的。
// 也就是说,即使 Student 是 子类,**但SimpleData<Student> 并不是 SimpleData<Person> 的子类**。
fun handleSimpleData(data:SimpleData<Person>){
val teacher = Teacher("Jack",35)
data.set(teacher)
}
}
回顾代码,问题出在我们在handleSimpleData()中向SimpleData设置了Teacher实例,如果泛型类SimpleData<T>
在其泛型类型的数据T上是只读的,就没有类型转换安全隐患的。
泛型协变的定义:假如定义了一个MyClass< T > 的泛型类,A是B的子类型,同时MyClass< A >是MyClass< B >的子类型,就可以称MyClass在T这个泛型上是协变的。
如何才能让MyClass成为MyClass的子类型?
如果一个泛型类在其泛型类型的数据上是只读的,那么它是没有类型转换安全隐患的。而要实现这一点,则需要让MyClass<T>
类中的所有方法都不能接收 T 类型的参数。换句话说,T 只能出现在 out 位置上,而不能出现在 in 位置上。
修改SimpleData类的代码:
/**
* **out 关键字意味着 T 只能出现在 out 位置上**,**同时也意味着 SimpleData 是在泛型 T 上是协变的**。
* 由于泛型 T 不能出现在 in 位置上,所以不能使用 set() 为 data 参数赋值了,
* 所以这里采用**构造函数**的方式来赋值。
* 虽然构造函数中的泛型 T 在 in 位置上,但由于使用了 **val 关键字**,所以 T 仍然是**只读**的,
* 即使使用 var 关键字,只要给它加上 private 修饰符,保证这个泛型 T 对于外部而言是不可修改的,
* 那么就都是合法的写法。
*/
class SimpleData <**out** T>(val **data: T**){ //val类型无法修改
fun get():T?{
return data
}
}
fun main(){
val student = Student("Tom",19)
val data = SimpleData<Student>(**student**)
handleSimpleData(data)
val studentData = data.get()
}
经过协变声明
fun handleSimpleData(data:SimpleData<Person>){
//由于经过了协变声明,SimpleData<Student>就是SimpleData<Person>的子类了
//虽然泛型声明是 Person 类型,实际会得到Student的实例,
**val personData = data.get()//**Person是Student的父类,**向上转型不会出错**
}
Kotlin默认给许多内置的API加上了协变声明,包含了各种集合的类与接口
/**
* List 简化版源码:
* **out 关键字说明 List 在泛型 E 上是协变的**。
* 原则上声明了协变后,泛型 E 只能出现在 out 位置,但 contaion() 中,仍然出现在 in 位置上,
* 这是因为 contains() 的目的非常明确,它**只是为了判断当前集合中是否包含参数中传入的这个元素,
* 而不会修改当前集合中的内容,因此这种操作实际上又是安全的。**
* 为了让编译器能够理解这种操作是安全的,使用了 **@UnsafeVariance** 注解,编译器就会允许泛型 E 出现在 in 位置上了。
*/
//List简化版的源码
public interface List<out E> : Collection<E> {
//List在泛型E前面加上了out关键字,说明List在泛型E上是协变的
override val size:Int
override fun isEmpty():Boolean
override fun contains(element: @UnsafeVariance E) : Boolean
//contains()方法只是判断,不会修改集合中的内容
//加上@UnsafeVariance注解,编译器就会允许泛型E出现在in位置上了
override fun iterator(): Iterator
public operator fun get(index:Int):E
}
13.5 泛型的逆变
逆变的定义:定义一个MyClass<T>
的泛型类,其中 A 是 B 的子类型,同时MyClass<B>又是MyClass<A>的子类型
,那么就可以称 MyClass 在 T 这个泛型上是逆变的。
例子:
/**
* 用来执行一些转换操作
*/
interface Transformer<T> {
// 参数 t:T 经过转换后,将会变成一个字符串,至于具体的转换逻辑则由子类去实现。
fun transformer(t:T):String
}
//Transformer<Person>的匿名类实现
fun main(){
val trans = object : Transformer<Person>{
// 通过 transform() 将传入的 Person 对象转换成了一个字符串
override fun transformer(t: Person): String {
return "${t.name} ${t.age}"
}
}
handleTransformer(trans)//报错因为Transformer<Person>不是Transformer<Student>的子类型
}
//接收一个Transformer<Student>类型的参数
fun handleTransformer(trans:Transformer<Student>){
val student = Student("Tom",19)
//创建一个Student对象,并调用参数的transform()方法将Student对象转换成一个字符串
val result = trans.transformer(student)
// 打印结果:
// Tom 19
}
open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)
使用Transformer的匿名类将 Student转化成字符串 没有问题(Student是Person的子类),但实际上会报错,因为handleTransformer(trans:Transformer<Student>)
中Transformer不是Transformer子类型。这时需要逆变。
修改:
/**
* in 关键字表示 T 只能出现在 in 位置,Transformer 在泛型 T 上是逆变的
*/
interface Transformer<**in** T>{
fun transformer(t:T):String
}
这样就可以运行了,Transforemer已经变成了Transformer的子类型。
接下来看一看为什么逆变时泛型T不能出现在out位置上?
interface Transformer<**in** T>{
//逆变不允许泛型T出现在out位置上,为了让编译器正常编译通过,@UnsafeVariance,与List源码一致。
fun transformer(name: String,age:Int):@UnsafeVariance T
}
fun main(){
// 如果让泛型 T 出现在 out 位置的隐患
val trans1 = object :Transformer1<Person>{
// 构建了一个 Teacher 对象,并直接返回。
override fun transform(name: String,age: Int): Person {
// transform() 的返回值要求是一个 Person 对象,
//而 Teacher 是 Person 的子类,这种写法是合法的。
return Teacher(name,age)
}
}
handleTransformer1(trans1)
}
fun handleTransformer1(trans:Transformer1<Student>){
// **期望得到的是一个 Student 对象,但实际上得到的是一个 Teacher 对象,**
// Teacher无法转换成Student类型,因此造成类型转换异常。
val result = **trans.transform("Tom",19)**
println(result)
// 打印结果
// Exception in thread "main" java.lang.ClassCastException:
// com.example.myapplication.test.Teacher cannot be cast to com.example.myapplication.test.Student
}
open class Person(val name: String,val age:Int)
class Student(name:String,age: Int):Person(name,age)
class Teacher(name:String,age: Int):Person(name,age)
关于逆变,假设 Student
是 Person
的子类,然后让 Comparable<Person>
成为 Comparable<Student>
的子类,这叫逆变。
逆变功能在Kotlin内置API中的应用——Comparable
/**
* Comparable 的源码:
* Comparable 接口为逆变的意义在于,
如果使用 Comparable<Person> 实现了让两个 Person 对象比较大小的逻辑,
那么用这段逻辑去比较两个 Student 对象的大小也一定是成立的,
因此让 Comparable<Person> 成为 Comparable<Student> 的子类合情合理,这也是逆变非常典型的应用。
*/
public interface Comparable<in T> {
// 实现具体的比较逻辑
public operator fun compareTo(other: T): Int
}
Comparable在T这个泛型上是逆变的
只要我们严格按照语法规则,让泛型在协变时只出现在out位置上,逆变时只出现在in位置上,就不会存在类型转换异常的情况。 类委托和委托属性
十四、类委托和委托属性
委托:一种设计模式,理念是操作对象自己不会去处理某段逻辑,把工作委托给另外一个辅助对象去处理。
Kotlin的委托分为类委托和委托属性。
14.1 类委托
核心思想是将一个类的实现委托给另一个类去完成。
例如:Set是一个存储无序且不能重复的数据的接口,使用时需要使用他的具体的实现类,如HashSet。借助委托模式我们可以轻松实现一个自己的实现类。比如这里定义MySet实现Set接口
class MySet<T>(val helperSet: HashSet<T>) : Set<T>{
override val size: Int
get() = helperSet.size
override fun contains(element: T) = helperSet.contains(element)
override fun containsAll(elements: Collection<T>) = helperSet.containsAll(elements)
override fun isEmpty() = helperSet.isEmpty()
override fun iterator() = helperSet.iterator()
}
构造函数中的HashSet相当于一个辅助对象。
- Set接口所有的方法实现,都是调用了辅助对象中相应的方法实现,这其实就是一种委托模式
- 其中少部分方法实现由自己重写,甚至加入一些自己的方法,MySet会成为一个新的数据结构类,这就是委托模式的意义所在
- 如果待实现方法特别多,使用委托关键字——by,免去之前一大堆模板式代码
class MySet<T>(val helperSet: HashSet<T>) : Set<T> by helperSet{
fun helloWorld() = println("Hello World")
override fun isEmpty() = false
}
14.2 委托属性
核心思想:将一个属性的具体实现委托给另一个类去完成,如下:
class MyClass{
var p by Delegate()//by关键字连接了左边的p属性和右边的Delegate实例
}
这里的by关键字:
- 将p的属性具体实现委托给了Delegate类去完成:
- 调用p属性时会自动调用Delegate类的getValue()方法
- **给p属性赋值时会自动调用Delegate类的setValue()**方法
因此对Delegate类进行具体的实现,必须实现getValue、setValue,并且用operator关键字声明。
class Delegate{
var propValue:Any? = null
//当用val声明时,不用在Delegate类中实现setValue()方法
//参数:第一个参数声明委托功能在什么类中使用,
KProperty<*>是Kotlin中的一个属性操作类,可用于获取各种属性相关的值。
operator fun getValue(myClass: MyClass,prop:KProperty<*>):Any?{
return propValue
}
operator fun setValue(myClass: MyClass,prop:KProperty<*>,value:Any?){
propValue = value
}
}
//<*>表示不知道或不关心泛型的具体类型
//value:Any? 表示要赋值给委托属性的值
如过用val关键字声明p属性就不用实现setValue()方法
14.3 实现一个自己的lazy函数
by lazy 的基本语法结构 :val p by lazy { … }
。by lazy代码块中的代码一开始不会执行,当变量首次被调用时才会执行,是一种懒加载技术。
by才是Kotlin中的关键字,lazy
只是一个高阶函数。
在lazy函数中会创建并返回一个Delegate
对象,调用p属性时,实际调用Delegate
对象的getValue()
方法,然后getValue()
方法中又会调用lazy函数传入的Lambda表达式,这样表达式中的代码块就得到执行了,并且调用p属性后得到的值就是Lambda表达式中最后一行代码的返回值。
实现一个自己的lazy函数
class Later {
class Later<T>(val block:() -> T){
var value:Any? = null
operator fun getValue(any: Any?,prop:KProperty<*>):T{
//Any?表示我们希望Later的委托功能能在所有类中都可以使用
if(value == null){//用一个value变量进行缓存
value = block()
}
return value as T
}
//懒加载技术不会对属性赋值,这里不用实现setValue()方法
}
}
实现完委托属性的功能后,为了让它的用法更类似于lazy函数,再定义一个顶层函数(不定义在任何类当中的函数),作用:创建Later类的实例,并将接收的函数类型参数传给Later类的构造函数,我们将顶层函数定义成泛型函数,并且它接收一个函数类型参数
fun <T> later(block:() -> T) = Later(block)
使用
val xxx by later{//将代码块中的内容通过later顶层函数传递给Later类的getValue方法
}