绕到 Kotlin 语法糖背后

2025/01/09 Java Kotlin 共 11773 字,约 34 分钟

前言

在 Kotlin 语言设计哲学中有一个非常突出的特性:简洁。Kotlin 语言简洁,意味着 Kotlin 代码易于阅读和编写。易于阅读,不用在 RTFS 时皱着眉头。易于编写,不用在键盘上敲得手酸。

Kotlin 语言简洁的背后是 Kotlin 语法糖,语法糖在保证代码易读和易写的同时,不改变代码的功能和底层机制。

在项目开发过程中,语法糖用起来非常的爽,但其背后隐藏的实现往往容易被忽视。本文将围绕几种常见的 Kotlin 语法糖,探究其底层实现原理。

语法糖

中缀调用

定义

中缀调用(infix notation)是一种特殊的函数调用。被调用的函数使用 infix 关键字修饰。定义中缀函数的条件是:

  1. 必须是成员函数或扩展函数。
  2. 函数只能有一个参数,参数不能是可变参数,且不能有默认值。
  3. 函数使用 infix 关键字修饰。

本质

中缀函数可以是成员函数或扩展函数:

// 成员函数
class MyClass {
    infix fun myInfixFunction(param: Type): ReturnType {
        // 函数体
    }
}

// 扩展函数
infix fun Type.myInfixFunction(param: AnotherType): ReturnType {
    // 函数体
}

下面的代码:

fun main() {
    val pair = 1 to "one"
}

编译后会生成类似的代码:

public final void main() {
   Pair pair = TuplesKt.to(1, "one");
}

示例

标准库中的 to 函数,用于创建 Pair 对象:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

val pair = "key" to "value"

val map = mapOf(1 to "one", 2 to "two)

自定义中缀函数:

infix fun Int.pow(exponent: Int): Int = Math.pow(this.toDouble(), exponent.toDouble()).toInt()

val result = 2 pow 3 // 计算 2 的 3 次方

解构声明

定义

解构声明允许将对象的多个属性解构为单独的变量。也就是允许展开单个复合值,并使用它来初始化多个单独的变量。

val p = Point(10, 20)
val (x, y) = p
此时:x = 10, y = 10

本质

在解构声明中初始化的每个变量,都是调用名为 componentN 的函数,其中 N 是声明中变量的位置。

componentN() 函数:它是编译器自动生成的、用于获取对象属性的函数。

  • 数据类(data class):会自动为其主构造函数参数生成 component1()component2() 等函数。
  • 普通类:可以通过实现 componentN() 函数来自定义解构支持。

定义数据类 Point:

data class Point(val x: Int, val y: Int)

编译后会生成类似的代码:

public final class Point {
   private final int x;
   private final int y;

   public final int getX() {
      return this.x;
   }

   public final int getY() {
      return this.y;
   }

   public Point(int x, int y) {
      this.x = x;
      this.y = y;
   }

   public final int component1() {
      return this.x;
   }

   public final int component2() {
      return this.y;
   }
}

使用val (x, y) = Point(10, 20) 实际执行的代码是:

val x = point.component1() // 调用 component1() 获取 x 属性 
val y = point.component2() // 调用 component2() 获取 y 属性

示例

从一个函数返回多个值:

fun getCoordinates(): Pair<Int, Int> {
    return Pair(10, 20) 
}

fun main() { 
    val (x, y) = getCoordinates() // 解构 Pair 类型 
    println("x: $x, y: $y") // 输出:x: 10, y: 20 
}

遍历集合:

fun main() {
    val map = mapOf("A" to 1, "B" to 2, "C" to 3)

    for ((key, value) in map) {  // 自动解构 Map.Entry
        println("$key -> $value")
    }
    // 输出:
    // A -> 1
    // B -> 2
    // C -> 3
}

Lambda 表达式:

val map = mapOf("A" to 1, "B" to 2)

map.forEach { (key, value) ->  // 解构 Lambda 参数
    println("$key -> $value")
}

自定义类支持解构:

class Point(val x: Int, val y: Int) {
    operator fun component1() = x
    operator fun component2() = y
}

fun main() {
    val point = Point(10, 20)
    val (x, y) = point  // 调用自定义的 component1() 和 component2()
    println("x: $x, y: $y")  // 输出:x: 10, y: 20
}

类委托

定义

类委托通过 by 关键字将接口的实现委托给另一个对象。

class DelegatedClass(delegate: SomeInterface) : SomeInterface by delegate

DelegatedClass 实现了 SomeInterface 接口,但不需要手动实现接口的方法。 通过 by 将接口的实现委托给对象 delegate

本质

编译器会为委托类生成接口方法实现,在方法实现中,通过调用委托对象的方法来完成接口逻辑。

定义类委托:

interface SomeInterface {
    fun doSomething()
}

class DelegatedClass(val delegate: SomeInterface) : SomeInterface by delegate

编译后会生成类似的代码:

public final class DelegatedClass implements SomeInterface {
   @NotNull
   private final SomeInterface delegate;

   @NotNull
   public final SomeInterface getDelegate() {
      return this.delegate;
   }

   public DelegatedClass(@NotNull SomeInterface delegate) {
      Intrinsics.checkNotNullParameter(delegate, "delegate");
      super();
      this.delegate = delegate;
   }

   public void doSomething() {
      this.delegate.doSomething();
   }
}

示例

集合使用类委托:

class CountingSet<T>(val innerSet: MutableCollection<T>) : MutableCollection<T> by innerSet {
    var objectsAdded = 0

    override fun add(element: T): Boolean {
        objectsAdded++;
        return innerSet.add(element);
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectsAdded = elements.size;
        return innerSet.addAll(elements);
    }
}

val cset = CountingSet<Int>()
cset.addAll(listOf(1,2,3))

println("objectsAdded = ${cset.objectsAdded}")
objectsAdded = 3

println("size = ${cset.size}")
size = 2

伴生对象

定义

伴生对象(Companion Object)是类中声明的一个特殊对象,使用 companionobject 关键字修饰。

Kotlin 中没有 static 关键字,不能直接定义静态方法和静态属性,但使用伴生对象可以达到相同的效果。

伴生对象是一个与类绑定的单例对象,可以访问类的私有成员,通常用来提供工厂方法、静态方法或与类相关的常量:

  • 伴生对象类似于 Java 中的 static 成员。
  • 每个类最多只能有一个伴生对象。
  • 伴生对象本质上是一个普通对象,只不过它与类绑定,并且可以通过类名直接访问。
class A {
    companion object{
        fun bar(){
            
        }
    }
}
//调用伴生对象方法
A.bar()

本质

伴生对象是一个类的静态属性。

上面类A 编译后会生成类似的代码:

public final class A {
   @NotNull
   public static final Companion Companion = new Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      private Companion() {
      }

      public final void bar() {
      }

      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

Companion 对象是一个单例对象。

示例

实现工厂模式并访问类的私有成员:

class User(private val id: Int, private val name: String) {
    companion object {
        fun create(id: Int, name: String): User {
            return User(id, name) // 伴生对象直接访问 User 的私有构造函数
        }
    }

    override fun toString() = "User(id=$id, name=$name)"
}

fun main() {
    val user = User.create(1, "Alice") // 使用伴生对象的工厂方法创建实例
    println(user)
}

lambda 表达式在作用域中访问变量

定义

lambda 表达式可以访问其所在作用域中的变量。这种访问外部作用域变量的能力被称为闭包(closure)

lambda 表达式可以访问:局部变量,外部类的成员变量,全局变量。

var sum = 0 
ints.filter { it > 0 }.forEach { sum += it } 
print(sum)

本质

lambda 表达式可以捕获其作用域中的变量,并将这些变量与 lambda 绑定在一起,然后形成一个闭包对象。

捕获不可变变量(val)

如果 lambda 表达式捕获的是不可变变量,Kotlin 编译器会直接将变量的值存储在闭包对象中。

捕获局部变量:

fun count() {
    //定义一个不可变局部变量
    val counter = 0
    val increment: () -> Unit = {
        //捕获不可变变量
        val c = counter
    }
}

上面代码编译后会生成类似的代码:

public final void count() {
   final int counter = 0;
   Function0 increment = (Function0)(new Function0() {
      public Object invoke() {
         this.invoke();
         return Unit.INSTANCE;
      }

      public final void invoke() {
         int var1 = counter;
      }
   });
}

捕获全局变量:

class Counter {
    //定义一个不可变全局变量
    private val counter = 0

    fun count(){
        val increment: () -> Unit = {
            //捕获不可变变量
            val c = counter
        }
    }
}

上面代码编译后会生成类似的代码:

public final class Counter {
   private final int counter;

   public final void count() {
      Function0 increment = (Function0)(new Function0() {
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            int var1 = Counter.this.counter;
         }
      });
   }
}

在 increment 对象中持有外部类 Counter 对象:int var1 = Counter.this.counter;

捕获可变变量(var)

如果 lambda 表达式捕获的是可变变量,Kotlin 编译器会将变量封装为一个对象(如 Ref 对象),通过间接引用来保证变量的修改对 lambda 和外部作用域都可见。在编译器生成的代码中,lambda 表达式会操作这个封装对象而非直接访问变量。

捕获局部变量:

fun count() {
    // 定义一个可变局部变量
    var counter = 0 
    val increment: () -> Unit = {
        // 捕获并修改局部变量
        counter++ 
    }
}

上面代码编译后会生成类似的代码:

public final void count() {
   final Ref.IntRef counter = new Ref.IntRef();
   counter.element = 0;
   Function0 increment = (Function0)(new Function0() {
      public Object invoke() {
         this.invoke();
         return Unit.INSTANCE;
      }

      public final void invoke() {
         int var10001 = counter.element++;
      }
   });
}

将变量封装为 Ref.IntRefvar counter = 0 final Ref.IntRef counter = new Ref.IntRef();

捕获全局变量:

class Counter {
    // 定义一个全局变量
    private var counter = 0
    
    fun count() {
        var counter = 0
        val increment: () -> Unit = {
            // 捕获并修改全局变量
            counter++
        }
    }
}

上面代码编译后会生成类似的代码:

public final class Counter {
   private int counter;
   
   public final void count() {
      Function0 increment = (Function0)(new Function0() {
         public Object invoke() {
            this.invoke();
            return Unit.INSTANCE;
         }

         public final void invoke() {
            Counter var10000 = Counter.this;
            var10000.counter = var10000.counter + 1;
         }
      });
   }
}

在 increment 对象中持有外部类 Counter 对象:Counter var10000 = Counter.this;

示例

捕获变量延长生命周期

fun count() {
    var count = 0

    val runnable = Runnable { 
        println("Count: $count") 
    }

    count++ // 修改变量
    Thread(runnable).start() // 在新线程中运行 Lambda
}

count 是局部变量,但Runnable lambda 表达式捕获了它,让 count 的生命周期延长到线程结束。

== 比较运算符

定义

在 Java 中,== 运算符,如果比较的是基本数据类型,则表示两个变量数值是否相等;如果比较的是对象,则表示两个对象内存地址是否相同。

在 Kotlin 中,== 运算符用于比较两个对象内容是否相等;===运算符用于比较两个对象内存地址是否相同。

本质

== 运算符用于比较两个对象时,如 a==b,首先会进行空检查,如果 a 对象为 null,然后判断 b 对象是否为 null,如果 b 对象也为 nulla==b 返回 true,否则返回 false;如果 a 对象不为 null,则会调用 a 对象的 equals 方法,比较内容是否相等。

a == b 等效于:

a == null ? b == null : a.equals(b);

下面的代码:

fun main() {
    val list1 = listOf(1, 2, 3)
    val list2 = listOf(1, 2, 3)
    if(list1 == list2){

    }
    if(list1 === list2){

    }
}

编译后会生成类似的代码:

public final void main() {
   Integer[] var2 = new Integer[]{1, 2, 3};
   List list1 = CollectionsKt.listOf(var2);
   Integer[] var3 = new Integer[]{1, 2, 3};
   List list2 = CollectionsKt.listOf(var3);
   if (Intrinsics.areEqual(list1, list2)) {
   }
   
   if (list1 == list2) {
   }

}

Intrinsics.areEqual 源代码为:

public static boolean areEqual(Object first, Object second) {
    return first == null ? second == null : first.equals(second);
}

示例

字符串对象内容和地址比较:

fun main() {
    val str1 = "Kotlin"
    val str2 = "Kotlin"

    println(str1 == str2)  // true,内容相等
    println(str1 === str2) // true,引用相等(字符串池的优化)
}

普通对象内容和地址比较:

fun main() {
    val list1 = listOf(1, 2, 3)
    val list2 = listOf(1, 2, 3)

    println(list1 == list2)  // true,内容相等
    println(list1 === list2) // false,引用不同
}

比较运算符本质

表达式本质
a == ba?.equals(b) ?: (b === null)
a != b!(a?.equals(b) ?: (b === null))
a > ba.compareTo(b) > 0
a < ba.compareTo(b) < 0
a >= ba.compareTo(b) >= 0
a <= ba.compareTo(b) <= 0

在 Kotlin 中,编译器会把 == 运算符会被转换为 equals 方法,把<> 运算符会被转换为 compareTo 方法。

by lazy() 属性懒加载

定义

by lazy() 用于实现属性懒加载,也就是在第一次使用该属性的时候才初始化,后续使用时都是缓存值。

val property: T by lazy(LazyThreadSafetyMode) { initialization logic }

lazy() 参数 LazyThreadSafetyMode 为线程安全模式,是一个枚举值:

  • LazyThreadSafetyMode.SYNCHRONIZED(默认):线程安全,适用于多线程环境。

  • LazyThreadSafetyMode.PUBLICATION:允许多个线程同时初始化,但只有一个结果会被使用。

  • LazyThreadSafetyMode.NONE:无锁,非线程安全,适用于单线程环境。

lazy() 参数 lambda 表达式{}:用于指定初始化逻辑,并返回属性值。

本质

lazy()函数会返回一个实现了 Lazy<T> 接口的对象:

public interface Lazy<out T> {
    //初始化并缓存lambda表达式返回的值
    public val value: T

    //是否已经初始化
    public fun isInitialized(): Boolean
}

下面的代码:

class A{
   val str by lazy(LazyThreadSafetyMode.NONE) {
       "Hello Lazy"
   }
}

编译后会生成类似的代码:

public final class A {
   @NotNull
   private final Lazy str$delegate;

   public A() {
      this.str$delegate = LazyKt.lazy(LazyThreadSafetyMode.NONE, A::str_delegate$lambda$0);
   }

   @NotNull
   public final String getStr() {
      Lazy var1 = this.str$delegate;
      return (String)var1.getValue();
   }

   private static final String str_delegate$lambda$0() {
      return "Hello Lazy";
   }
}

LazyKt.lazy 源代码为:

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }

SynchronizedLazyImplSafePublicationLazyImplUnsafeLazyImpl都实现了 Lazy<T> 接口。

UnsafeLazyImpl为例,每次调用它的 value 时:

internal class UnsafeLazyImpl<out T>(initializer: () -> T) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    private var _value: Any? = UNINITIALIZED_VALUE

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

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

如果 _value === UNINITIALIZED_VALUE,先初始化_value = initializer!!()_value的值为lambda表达式返回的结果值),然后返回 _value值。

示例

多线程环境的属性懒加载:

import kotlin.concurrent.thread
import kotlin.LazyThreadSafetyMode

val lazyValue: String by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
    println("Computing lazyValue in thread ${Thread.currentThread().name}")
    "Thread-Safe Lazy Value"
}

fun main() {
    repeat(3) {
        thread {
            println(lazyValue)
        }
    }
}

输出结果:
Computing lazyValue in thread Thread-0
Thread-Safe Lazy Value
Thread-Safe Lazy Value
Thread-Safe Lazy Value

泛型参数类型<T>判断:is T

定义

不管是在 Java 中,还是在 Kotlin 中,泛型在运行时会被擦除,擦除意味着在运行时无法判断泛型类型。

使用 reified 关键字,与 inline 函数结合,可以在运行时判断泛型类型。

inline fun <reified T> isType(value: Any): Boolean {
    return value is T
}

inline 关键字修饰的函数,编译器会把每一次函数调用都换成函数实际的代码实现。

本质

reified 泛型通过内联优化,在函数调用时将泛型替换为具体类型,保留类型信息。

下面的代码:

inline fun <reified T> isType(value: Any): Boolean {
    return value is T
}

fun main() {
    val value = "hello"
    val isString = isType<String>(value)
    val isNumber = isType<Number>(value)
}

编译后会生成类似的代码:

public final boolean isType(Object value) {
   Intrinsics.checkNotNullParameter(value, "value");
   int $i$f$isType = false;
   Intrinsics.reifiedOperationMarker(3, "T");
   return value instanceof Object;
}

public final void main() {
   String value = "hello";
   int $i$f$isType = false;
   boolean isString = true;
   int $i$f$isType = false;
   boolean isNumber = value instanceof Number;
}

value 被编译器推断为 String 类型,所以 isString 值直接为 true,而 Number 类型信息被保留:boolean isNumber = value instanceof Number;

示例

获取 Class 对象:

inline fun <reified T> getClassName(): String {
    return T::class.java.simpleName
}

fun main() {
    println(getClassName<String>()) // 输出:String
    println(getClassName<List<Int>>())// 输出:List
}

泛型类型转换:

inline fun <reified T> safeCast(value: Any): T? {
    return value as? T
}

fun main() {
    val result: String? = safeCast<String>("Hello")
    println(result)  // 输出: Hello

    val failed: Int? = safeCast<Int>("Hello")
    println(failed)  // 输出: null
}

创建实例:

inline fun <reified T> createInstance(): T? {
    return T::class.java.getDeclaredConstructor().newInstance()
}

class MyClass {
}

fun main() {
    val instance: MyClass? = createInstance<MyClass>()
}

泛型与类型擦除的对比:

fun <T> checkGenericType(value: T) {
    // 编译错误:无法直接使用 is 检查泛型类型
    // if (value is T) { ... }
}

inline fun <reified T> checkReifiedType(value: Any) {
    if (value is T) {
        println("Value is of type ${T::class.simpleName}")
    }
}

fun main() {
    checkReifiedType<String>("Hello")  // 输出: Value is of type String
}

总结

Kotlin 语法糖背后是 Kotlin 编译器默默努力的结果,语法糖并不改变代码的功能和底层机制。

文档信息

Search

    Table of Contents