Kotlin

kotlin data class

data class会自动生成以下方法:

  • equals()

  • hashCode()

    1
    2
    3
    4
    5
    public int hashCode() {
    int var10000 = Integer.hashCode(this.age) * 31;
    String var10001 = this.name;
    return var10000 + (var10001 != null ? var10001.hashCode() : 0);
    }
  • toString()

  • copy()

  • componentN()

    编译器会为数据类生成 组件函数(Component function), 有了这些组件函数, 就可以在 解构声明(destructuring declaration) 中使用数据类:

    1
    2
    3
    4
    val jane = User("Jane", 35)
    val (name, age) = jane
    println("$name, $age years of age")
    // 输出结果为 Jane, 35 years of age
  • 属性的get()/set()

    val的属性不会有setter

  • constructor()

    只有有参构造函数,没有无参构造函数。fastJson解析会抛该异常,需升级到高版本并引入kotlin-reflect依赖

Kotlin Object类

其字节码实现是:提供一个类,会在static方法块中实例化的该类的static final 对象。

是饿汉模式的,通过JVM类加载机制确保线程安全的

Kotlin空安全

Kotlin空安全原理:

String、String?

首先通过注解 @Lorg/jetbrains/annotations/NotNull;和@Lorg/jetbrains/annotations/Nullable;来向编译器标示参数是否为空,如果不为空则通过 INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull (Ljava/lang/Object;Ljava/lang/String;)来进行检查,此时如果我们给个空值则编译器出现报错提示。

?.(elvis猫王表达式)

对用使用 ?. 操作符号kotlin会判断是否为null 如果不为null执行对应的逻辑,如果为null则什么也不执行(此时的默认结果也是null)

!!.

对用使用 !! 操作符号 Kotlin 同样会执行null判断如果不为null 则执行对应的逻辑,如果为null 则抛出异常,即执行 INVOKESTATIC kotlin/jvm/internal/Intrinsics.throwNpe ()

kotlin 泛型

Kotlin 泛型系统继承于 Java泛型,依然是一种语法糖的伪泛型,会在编译时发生类型擦除。但如果是内联函数+reified时,是字节码中保留类型的真泛型

Kotlin高阶函数

Kotlin中的高阶函数通过位于kotlin.jvm.functions下的函数接口(SAM)来表示,在编译时将函数类型转换为这些接口的实现。编译器生成相应的字节码来处理高阶函数调用。(由于都是预先写好的函数接口,所以其实高阶函数最大支持的参数数量是有限的,现为22个)

image-20240604140550931
1
2
3
4
5
/** A function that takes 22 arguments. */
public interface Function22<in P1, in P2, in P3, in P4, in P5, in P6, in P7, in P8, in P9, in P10, in P11, in P12, in P13, in P14, in P15, in P16, in P17, in P18, in P19, in P20, in P21, in P22, out R> : Function<R> {
/** Invokes the function with the specified arguments. */
public operator fun invoke(p1: P1, p2: P2, p3: P3, p4: P4, p5: P5, p6: P6, p7: P7, p8: P8, p9: P9, p10: P10, p11: P11, p12: P12, p13: P13, p14: P14, p15: P15, p16: P16, p17: P17, p18: P18, p19: P19, p20: P20, p21: P21, p22: P22): R
}

例如,以下高阶函数:

1
2
3
fun doSomeThing(x: Int, predicate: (Int) -> String): String {
return predicate(x)
}

当编译这个函数时,编译器会生成类似如下的字节码(伪代码,用于说明):

1
2
3
4
5
6
7
public final class ExperimentalFiled3 {
@NotNull
public final String doSomeThing(int x, @NotNull kotlin.jvm.functions.Function1 predicate) {
Intrinsics.checkNotNullParameter(predicate, "predicate");
return (String)predicate.invoke(x);
}
}

只有一个抽象方法的接口称为函数式接口单一抽象方法(SAM)接口

Kotlin闭包

Lambda表达式和匿名函数在Kotlin中都是闭包,意味着它们可以捕获并持有其定义作用域内的变量。

函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起构成闭包。

其中包括两个要点:

  • 函数
  • 周围环境(&状态&上下文)比如在a函数里定义了b匿名函数和变量x,b能引用到a的变量x,就叫闭包

这两者的实现原理类似的,通过编译器生成对应的

继承于kotlin.jvm.internal.Lambda并且实现kotlin.jvm.functions下函数接口的类,将表达式或匿名函数类方法体代码塞入该类的invoke方法中,并在被调用处生成该类的实例

比如

1
val sum = { a: Int, b: Int -> a + b }

编译器会生成类似如下的字节码(伪代码,用于说明):

1
2
3
4
5
6
7
8
9
10
11
12
public final class SumLambda extends Lambda implements Function2<Integer, Integer, Integer> {
public static final SumLambda INSTANCE = new SumLambda();

private SumLambda() {
super(2);
}

@Override
public Integer invoke(Integer a, Integer b) {
return a + b;
}
}

使用时:

1
val sum = SumLambda.INSTANCE

Lambda表达式和匿名函数的区别主要还是在于语法的不同、返回类型是否需要显示声明、非局部返回上。

Kotlin 类、方法为什么默认final

为了让程序编写者慎用继承,仅当需要被继承时才手动使用open关键字修饰需要被集成的类或方法。这样可以增加对“降低耦合性、提高灵活性”的考虑

kotlin 嵌套类&内部类

java: 只分为 静态内部类 和 非静态内部类, 只要是非静态内部类都会固定持有外部类的对象,只要是静态内部类都是不持有外部类引用的

kotlin: 分为

  1. 内部类 inner 修饰的内部类(类成员), 相当于 java非静态内部类,固定持有外部类引用的(本质上是生成外部类引用的构造参数)

  2. 嵌套类 非inner修饰的成员类, 相当于 java静态内部类,不持有外部类引用的

  3. 3.1 匿名内部类(常见) 使用对象表达式创建的类(object: Interface{ }),可以访问到外部类成员,但默认不持有外部类引用,只有主动持有外部变量是才会将该外部类对象(如Activity对象)作为构造参数(见字节码)引入

    3.2 方法内嵌套类 在方法内定义的非inner修饰的类,可以访问到外部类成员,但默认不持有外部类引用,只有主动持有外部变量是才会将该外部类对象(如Activity对象)作为构造参数(见字节码)引入

Kotlin注解

Kotlin代码可以经过编译器转换成VM虚拟机能识别的字节码,所以JavaKotlin可以互相进行调用。而由于JavaKotlin语言特性的差异,当Java调用Kotlin代码时,可以在Kotlin代码中适当增加一些注解,从而更方便的调用Kotlin代码。

@JvmOverloads

在Kotlin的方法里有多个默认参数时,如果在Java中直接调用,只能调用一个包含完整参数的方法,如果想暴露更多的重载函数给Java,可以使用@JvmOverloads 用于生成重载。对于每一个有默认值的参数,生成的重载会把当前有默认值的参数及其右边的参数都去掉,所以如果方法中所有的参数都有默认值,生成的重载函数中还会有一个无参的重载函数。

@JvmOverloads 主要用于构造函数、方法中,同时不能用于抽象方法、接口中的方法等。

常见应用在自定义View构造函数中

1
2
3
4
5
VpLoadMoreView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : LinearLayout(context, attrs, defStyle) {}

@JvmStatic

@JvmStatic用于声明静态方法。在具名对象及伴生对象中使用时,既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法,如:

1
2
3
4
5
6
class KtA {
companion object {
@JvmStatic
fun invokeStatic() {}
fun invokeNoStatic() {}
}

在Java中调用:

1
2
3
4
5
6
public void invokeKt() {
KtA.invokeStatic(); //正确,可以直接调用
//KtA.invokeNoStatic(); //错误,这里调用不到
KtA.Companion.invokeStatic(); //正确
KtA.Companion.invokeNoStatic(); //正确
}

@JvmField

@JvmField使得编译器不再对该字段生成getter/setter并将其作为公开字段

@JvmSynthetic

@JvmSynthetic可以修饰于方法上,控制只能在Kotlin中调用,如:

1
2
3
4
class KtA {
@JvmSynthetic
fun visit() {}
}

Java中调用:

1
2
3
4
public void invokeKt() {
KtA clz = new KtA();
clz.visit(); //错误,这里在Java中调用不到。
}

如果想在Java中调用到Kotlin类中的方法,将@JvmSynthetic去掉即可。

@JvmInline

1
2
3
4
5
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@SinceKotlin("1.5")
public actual annotation class JvmInline

@JvmInline在1.5.0版本引入,可以指定一个类为内联类,需结合value一起使用;在1.5.0之前使用inline关键字。

1
2
3
4
5
6
7
//1.5.0之前,inline标记内联类
inline class Person(private val name: String = "")
//1.5.0之后,@JvmInline + value 标记内联类
@JvmInline
value class Person(private val name: String = "")

内联类构造参数中有且只能有一个成员变量,最终被内联到字节码中的value。,上述代码经过内联优化会在字节码中将Person对象转换为String值,从而由堆分配优化为栈分配。

@JvmName 、@JvmMultifileClass

@JvmName 注解可以生成类名;如果类名已存在,可以修改已生成的 Java 类的类名。
包名相同并且类名相同或者有相同的 @JvmName 注解有会错误,可以通过@JvmMultifileClass把他们合并到一起

Kotlin lateinit 和 by lazy

简述:

lateinit是用于var的不可空类型属性,是声明延迟初始化,需开发者保证赋值的时序正确

by lazy是用于val的属性,by是用于委托的关键字,lazy是提供线程安全的懒加载的实现

lateinit var适用于你在声明变量时不知道它的初始值是多少的场景,需要保证代码访问时序的正确性

lazy更适用于,「一个对象的创建需要消耗大量的资源,而我不知道它到底会不会被用到」的场景,lazy只有在第一次被调用到的时候才会去赋值

lazy本质是生成了一个SynchronizedLazyImpl对象,这个对象初始化的时候会持有一个函数的引用,当调用它的value的时候,会去检查是不是初始化过了,如果初始化过了直接返回,没有的话调用传入的函数,获取到返回值之后再返回,从而实现了一个懒加载的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
private var initializer: (() -> T)? = initializer
@Volatile private var _value: Any? = UNINITIALIZED_VALUE
// final field is required to enable safe publication of constructed instance
private val lock = lock ?: this

override val value: T
get() {
val _v1 = _value
if (_v1 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST")
return _v1 as T
}

return synchronized(lock) {
val _v2 = _value
if (_v2 !== UNINITIALIZED_VALUE) {
@Suppress("UNCHECKED_CAST") (_v2 as T)
} else {
val typedValue = initializer!!()
_value = typedValue
initializer = null
typedValue
}
}
}
}

但是上面的代码中,我们可以看到,这个实现它在第一次获取值的时候是有加锁来实现线程安全,但是很多时候我们的代码都是单线程调用的,不需要考虑线程安全问题,这个时候就会有额外的性能开销

Author

white crow

Posted on

2021-07-30

Updated on

2024-07-15

Licensed under