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
fastjson
的 TypeReference
和 gson
的 TypeToken
都是用这种方式来获取泛型的。
// 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)java
中 List
是不变的,下面的操作不被允许。
List<String> strList = new ArrayList<>();
List<Object> objList = strList;
但是 kotlin
中 List
是协变的,可以做这个操作。
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
}
getValue
、setValue
、provideDelegate
操作符:
用于委托属性,变量的 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
类相当于同时继承了 BaseAImpl
、BaseBImpl
类,并且重写了 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
方法的参数跟上卖弄完全一致即可,返回值类型必须是属性类型。ReadOnlyProperty
、ReadWriteProperty
、PropertyDelegateProvider
都是 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
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。