注册

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 这种方法调用的时候看起来统一一点,如果是别的方法,完全可以写在类所在文件的顶层。


作者:流沙三七
链接:https://juejin.cn/post/7034110955571609607
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册