kotlin 进阶教程一:核心概念

789 阅读1分钟

组内培训用,适合有基础的同学。


文档:
kotlin 中文网
kotlin 官网

1 空安全

// ? 操作符,?: Elvis 操作符
val length = b?.length ?: -1
// 安全类型转换
val code = res.code as? Int
// StringsKt
val code = res.code?.toIntOrNull()
// CollectionsKt
val list1: List<Int?> = listOf(1, 2, 3, null)
val list2 = listOf(1, 2, 3)
val a = list2.getOrNull(5)
// 这里注意 null 不等于 false
if(a?.hasB == false) {}

2 内联函数


使用 inline 操作符标记的函数,函数内代码会编译到调用处。

// kotlin
val list = listOf("a", "b", "c", null)
list.getOrElse(4) { "d" }?.let {
    println(it)
}

// Decompile,getOrElse 方法会内联到调用处
List list = CollectionsKt.listOf(new String[]{"a", "b", "c", (String)null});
byte var3 = 4;
Object var10000;
if (var3 <= CollectionsKt.getLastIndex(list)) {
	var10000 = list.get(var3);
} else {
	var10000 = "d";
}

String var9 = (String)var10000;
if (var9 != null) {
	String var2 = var9;
    System.out.print(var2);
}

noline: 禁用内联,用于标记参数,被标记的参数不会参与内联。

// kotlin
inline fun sync(lock: Lock, block1: () -> Unit, noinline block2: () -> Unit) {}

// Decompile,block1 会内联到调用处,但是 block2 会生成函数对象并生成调用
Function0 block2$iv = (Function0)null.INSTANCE;
...
block2.invoke()

@kotlin.internal.InlineOnly: kotlin 内部注解,这个注解仅用于内联函数,用于防止 java 类调用(原理是编译时会把这个函数标记为 private,内联对于 java 类来说没有意义)。

如果扩展函数的方法参数包含高阶函数,需要加上内联。

非局部返回:
lambda 表达式内部是禁止使用裸 return 的,因为 lambda 表达式不能使包含它的函数返回。但如果 lambda 表达式传给的函数是内联的,那么该 return 也可以内联,所以它是允许的,这种返回称为非局部返回。
但是可以通过 crossinline 修饰符标记内联函数的表达式参数禁止非局部返回。

public inline fun <T> observable(initialValue: T, crossinline onChange: (property: KProperty<*>, oldValue: T, newValue: T) -> Unit):
        ReadWriteProperty<Any?, T> =
    object : ObservableProperty<T>(initialValue) {
        override fun afterChange(property: KProperty<*>, oldValue: T, newValue: T) = onChange(property, oldValue, newValue)
    }

3 泛型


(1) 基本用法

class A<T> {
}
fun <T> T.toString(): String {
}
// 约束上界
class Collection<T : Number, R : CharSequence> : Iterable<T> {
}
fun <T : Iterable> T.toString() {
}
// 多重约束
fun <T> T.eat() where T : Animal, T : Fly {
}


(2) 类型擦除
为了兼容 java 1.5 以前的版本,带不带泛型编译出来的字节码都是一样的,泛型的特性是通过编译器类型检查和强制类型转换等方式实现的,所以 java 的泛型是伪泛型。
虽然运行时会擦除泛型,但也是有办法拿到的。

(javaClass.genericSuperclass as? ParameterizedType)
    ?.actualTypeArguments
    ?.getOrNull(0)
    ?: Any::class.java

fastjsonTypeReferencegsonTypeToken 都是用这种方式来获取泛型的。

// fastjson 
HttpResult<PrivacyInfo> httpResult = JSON.parseObject(
    json,
    new TypeReference<HttpResult<PrivacyInfo>>() {
    }
);
// gson
Type type = new TypeToken<ArrayList<JsonObject>>() {
}.getType();
ArrayList<JsonObject> srcJsonArray = new Gson().fromJson(sourceJson, type);


Reified 关键字
在 kotlin 里,reified 关键字可以让泛型能够在运行时被获取到。reified 关键字必须结合内联函数一起用。

// fastjson
inline fun <reified T : Any> parseObject(json: String) {
    JSON.parseObject(json, T::class.java)
}
// gson
inline fun <reified T : Any> fromJson(json: String) {
    Gson().fromJson(json, T::class.java)
}
// 获取 bundle 中的 Serializable
inline fun <reified T> Bundle?.getSerializableOrNull(key: String): T? {
    return this?.getSerializable(key) as? T
}
// start activity
inline fun <reified T : Context> Context.startActivity() {
    startActivity(Intent(this, T::class.java))
}


(3) 协变(out)和逆变(in)

javaList 是不变的,下面的操作不被允许。

List<String> strList = new ArrayList<>();
List<Object> objList = strList;


但是 kotlinList 是协变的,可以做这个操作。

public interface List<out E> : Collection<E> { ... }
val strList = arrayListOf<String>()
val anyList: List<Any> = strList

注意这里赋值之后 anyList 的类型还是 List<Any> , 如果往里添加数据,那个获取的时候就没法用 String 接收了,这是类型不安全的,所以协变是不允许写入的,是只读的。在 kotlin 中用 out 表示协变,用 out 声明的参数类型不能作为方法的参数类型,只能作为返回类型,可以理解成“生产者”。相反的,kotlin 中用 in 表示逆变,只能写入,不能读取,用 in 声明的参数类型不能作为返回类型,只能用于方法参数类型,可以理解成 “消费者”。

注意 kotlin 中的泛型通配符 * 也是协变的。

4 高阶函数


高阶函数: 将函数用作参数或返回值的函数。

写了个 test 方法,涵盖了常见的高阶函数用法。

val block4 = binding?.title?.test(
    block1 = { numer ->
        setText(R.string.app_name)
        println(numer)
    },
    block2 = { numer, checked ->
        "$numer : $checked"
    },
    block3 = {
        toIntOrNull() ?: 0
    }
)
block4?.invoke(2)

fun <T: View, R> T.test(
    block1: T.(Int) -> Unit,
    block2: ((Int, Boolean) -> String)? = null,
    block3: String.() -> R
): (Int) -> Unit {
    block1(1)
    block2?.invoke(2, false)
    "5".block3()
    return { number ->
        println(number)
    }
}

5 作用域函数

// with,用于共用的场景
with(View.OnClickListener {
    it.setBackgroundColor(Color.WHITE)
}) {
    tvTitle.setOnClickListener(this)
    tvExpireDate.setOnClickListener(this)
}

// apply,得到值后会修改这个值的属性
return CodeLoginFragment().apply {
    arguments = Bundle().apply {
        putString(AppConstants.INFO_EYES_EVENT_ID_FROM, eventFrom)
    }
}

// also,得到值后还会继续用这个值
tvTitle = view.findViewById<TextView?>(R.id.tvTitle).also { 
    displayTag(it)
}

// run,用于需要拿内部的属性的场景
tvTitle?.run { 
    text = "test"
    visibility = View.VISIBLE
}

// let,用于使用它自己的场景
tvTitle?.let {
    handleTitle(it)
}

fun <T> setListener(listenr: T.() -> Unit) {
}

6 集合

list.reversed().filterNotNull()
    .filter {
        it % 2 != 0
    }
    .map {
        listOf(it, it * 2)
    }
    .flatMap {
        it.asSequence()
    }.onEach {
        println(it)
    }.sortedByDescending {
        it
    }
    .forEach {
        println(it)
    }

7 操作符重载

重载(overload)操作符的函数都需要使用 operator 标记,如果重载的操作符被重写(override),可以省略 operator 修饰符。
这里列几个比较常用的。

索引访问操作符:

a[i, j] => a.get(i, j)
a[i] = b => a.set(i, b)

注意 i、j 不一定是数字,也可以是 String 等任意类型。

public interface List<out E> : Collection<E> {
    public operator fun get(index: Int): E
}
public interface MutableList<E> : List<E>, MutableCollection<E> {
    public operator fun set(index: Int, element: E): E
}

调用操作符:
invoke 是调用操作符函数名,调用操作符函数可以写成函数调用表达式。

val a = {}
a() => a.invoke()
a(i, j) => a.invoke(i, j)

变量 block: (Int) -> Unit 调用的时候可以写成 block.invoke(2),也可以写成 block(2),原因是重载了 invoke 函数:

public interface Function1<in P1, out R> : Function<R> {
    /** Invokes the function with the specified argument. */
    public operator fun invoke(p1: P1): R
}


getValuesetValueprovideDelegate 操作符:
用于委托属性,变量的 get() 方法会委托给委托对象的 getValue 操作符函数,相对应的变量的 set() 方法会委托给委托对象的 setValue 操作符函数。

class A(var name: String? = null) {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String? = name
    operator fun setValue(thisRef: Any?, property: KProperty<*>, name: String?) {
        this.name = name
    }
}
// 翻译
var b by A() => 
    val a = A()
    var b:String?
        get() = a.getValue(this, ::b)
        set(value) = a.setValue(this, ::b, value)

表达式 ::b 求值为 KProperty 类型的属性对象。

跟前面的操作符函数有所区别的是,这两个操作符函数的参数格式都是严格要求的,一个类中的函数格式符合特定要求才可以被当做委托对象。

provideDelegate 主要用于对委托对象通用处理,比如多个变量用了同一个委托对象时需要验证变量名的场景。

var b by ALoader()

class A(var name: String? = null) : ReadWriteProperty<Any?, String?>{
    override fun getValue(thisRef: Any?, property: KProperty<*>): String? {
        return name
    }

    override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) {
        this.name = value
    }
}

class ALoader : PropertyDelegateProvider<Any?, A> {
    override operator fun provideDelegate(thisRef: Any?, property: KProperty<*>) : A {
        property.run {
            when {
                isConst -> {}
                isLateinit -> {}
                isFinal -> {}
                isSuspend -> {}
                !property.name.startsWith("m") -> {}
            }
        }
        return A()
    }
}

// 翻译
var b by ALoader() =>
    val a = ALoader().provideDelegate(this, this::b)
    var b: String?
        get() = a.getValue(this, ::b)
        set(value) = a.setValue(this, ::b, value)

8 委托

8.1 委托模式


// 单例
companion object {
    @JvmStatic
    val instance by lazy { FeedManager() }
}

// 委托实现多继承
interface BaseA {
    fun printA()
}

interface BaseB {
    fun printB()
}

class BaseAImpl(val x: Int) : BaseA {
    override fun printA() {
        print(x)
    }
}

class BaseBImpl() : BaseB {
    override fun printB() {
        print("printB")
    }
}

class Derived(a: BaseA, b: BaseB) : BaseA by a, BaseB by b {
    override fun printB() {
        print("world")
    }
}

fun main() {
    val a = BaseAImpl(10)
    val b = BaseBImpl()
    Derived(a, b).printB()
}

// 输出:world


这里 Derived 类相当于同时继承了 BaseAImplBaseBImpl 类,并且重写了 printB() 方法。
在实际开发中,一个接口有多个实现,如果想复用某个类的实现,可以使用委托的形式。
还有一种场景是,一个接口有多个实现,需要动态选择某个类的实现:

interface IWebView {
    fun load()
}

// SDK 内部 SystemWebView
class SystemWebView : IWebView {
    override fun load() {
        ...
    }

    fun stopLoading() {
        ...
    }
}

// SDK 内部 X5WebView
class X5WebView : IWebView {
    override fun load() {
        ...
    }

    fun stopLoading() {
        ...
    }
}

abstract class IWebViewAdapter(webview: IWebView) : IWebView by webview{
    abstract fun stopLoading()
}

class SystemWebViewAdapter(private val webview: SystemWebView) :  IWebViewAdapter(webview){
    override fun stopLoading() {
        webview.stopLoading()
    }
}

class X5WebViewAdapter(private val webview: X5WebView) :  IWebViewAdapter(webview){
    override fun stopLoading() {
        webview.stopLoading()
    }
}

8.2 委托属性


格式:

import kotlin.reflect.KProperty

public interface ReadOnlyProperty<in R, out T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T
}

public interface ReadWriteProperty<in R, T> {
    public operator fun getValue(thisRef: R, property: KProperty<*>): T

    public operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

public fun interface PropertyDelegateProvider<in T, out D> {
    public operator fun provideDelegate(thisRef: T, property: KProperty<*>): D
}

自定义委托对象, getValue 方法的参数跟上面完全一致即可,返回值类型必须是属性类型;setValue 方法的前两个参数跟上面完全一致即可,第三个参数类型必须是属性类型;provideDelegate 方法的参数跟上卖弄完全一致即可,返回值类型必须是属性类型。
ReadOnlyPropertyReadWritePropertyPropertyDelegateProvider 都是 kotlin 标准库里的类,需要自定义委托对象时直接继承他们会更方便。

9 怎么写单例?

不用 object 的写法可能是:

// 不带参单例
class A {
    companion object {
        @JvmStatic
        val instance by lazy { A() }
    }
}

// 带参的单例,不推荐
class Helper private constructor(val context: Context) {

    companion object {
        @Volatile
        private var instance: Helper? = null

        @JvmStatic
        fun getInstance(context: Context?): Helper? {
            if (instance == null && context != null) {
                synchronized(Helper::class.java) {
                    if (instance == null) {
                        instance = Helper(context.applicationContext)
                    }
                }
            }
            return instance
        }
    }
}

先说带参的单例,首先不推荐写带参数的单例,因为单例就是全局共用,初始化一次之后保持不变,需要的参数应该在第一次使用前设置好(比如通过 by lazy{ A().apply { ... } }),或者单例内部拿应用内全局的参数,然后上例中 context 作为静态变量,Android Studio 会直接报黄色警告,这是个内存泄漏。context 可以设置一个全局的 applicationContext 变量获取。

然后上面不带参的单例可以
直接用 object 代替或者直接不用 object 封装,写在文件顶层,可以对比下编译后的代码:

// kotlin
object A{
    fun testA(){}
}

// 编译后:
public final class A {
   @NotNull
   public static final A INSTANCE;

   public final void testA() {
   }

   private A() {
   }

   static {
      A var0 = new A();
      INSTANCE = var0;
   }
}


// kotlin
var a = "s"
fun testB(a: String){
    print(a)
}

// 编译后:
public final class TKt {
   @NotNull
   private static String a = "s";

   @NotNull
   public static final String getA() {
      return a;
   }

   public static final void setA(@NotNull String var0) {
      Intrinsics.checkNotNullParameter(var0, "<set-?>");
      a = var0;
   }

   public static final void testB(@NotNull String a) {
      Intrinsics.checkNotNullParameter(a, "a");
      boolean var1 = false;
      System.out.print(a);
   }
}

可以发现,直接文件顶层写,不会创建对象,都是静态方法,如果方法少且评估不需要封装(主要看调用的时候是否需要方便识别哪个对象的方法)可以直接写在文件顶层。

同理,伴生对象也尽量非必要不创建。

// kotlin
class A {
    companion object {
        const val TAG = "A"

        @JvmStatic
        fun newInstance() = A()
    }
}

// 编译后
public final class A {
   @NotNull
   public static final String TAG = "A";
   @NotNull
   public static final A.Companion Companion = new A.Companion((DefaultConstructorMarker)null);

   @JvmStatic
   @NotNull
   public static final A newInstance() {
      return Companion.newInstance();
   }

   public static final class Companion {
      @JvmStatic
      @NotNull
      public final A newInstance() {
         return new A();
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

可以发现,伴生对象会创建一个对象(废话...),知道这个很重要,因为如果伴生对象里没有函数,只有常量,那还有必要创建这个对象吗?函数也只是为了 newInstance 这种方法调用的时候看起来统一一点,如果是别的方法,完全可以写在类所在文件的顶层。