Kotlin语法基础

Kotlin 概述

官网:https://kotlinlang.org/

下面是一些 Kotlin 的学习资源:

  1. 官方文档英文版:https://kotlinlang.org/docs/home.html

  2. Kotlin的在线练习站点:https://play.kotlinlang.org/?_gl=1g2jnrc_gcl_auNzY2NTg3NTAwLjE3NTUyNjU4ODM._gaODgyNjQzMjkzLjE3NTUyNjU4ODg._ga_9J976DJZ68czE3NTUyNjU4ODQkbzEkZzEkdDE3NTUyNjYxMDUkajkkbDAkaDA.

  3. Kotlin官方文档中文翻译版:https://book.kotlincn.net/

  4. Jetbrains关于Kotlin的在线课程:https://hyperskill.org/courses?category=4&utm_source=jbkotlin_hs&utm_medium=referral&utm_campaign=kotlinlang-docs&utm_content=button_1&utm_term=22.03.23

基础语法

kotlin 1.8.20

包定义与导入

包的声明应处于源文件顶部,目录与包的结构无需匹配:源代码可以在文件系统的任意位置。真这么做的话,Idea里面会有提示让你修改成文件目录对应的包名:Package directive does not match the file location 。说明目录结构与包名匹配是推荐的方式,代码组织更清晰

import 关键字和 Java 里差不多。Kotlin 里支持如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义:

import org.example.Message // Message 可访问
import org.test.Message as TestMessage // TestMessage 代表“org.test.Message”

关键字 import 并不仅限于导入类;也可用它来导入其他声明:

访问修饰符

Kotlin 中有四个可见性修饰符: private 、 protected 、 internal 和 public 。 默认可见性是 public

变量命名冲突

如果变量命名和保留字冲突,可使用`(键盘上数字1左边的字符,需在英文模式下输入)进行转义例如

val `object` = Object()
fun toString(`object`: Object): String

面向对象

Kotlin 中使用关键字 class 声明类。类声明由类名、类头(指定其类型参数、主构造函数等)以及由花括号包围的类体构成。类头与类体都是可选的; 如果一个类没有类体,可以省略花括号。

class Empty

构造函数

在 Kotlin 中的一个类有一个主构造函数并可能有一个或多个次构造函数。主构造函数在类头中声明,它跟在类名与可选的类型参数后。

class Person constructor(firstName: String) { /*……*/ }
// 如果主构造函数没有任何注解或者可见性修饰符,可以省略这个 constructor 关键字。
class Person(firstName: String) { /*……*/ }
// 如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面:
class Customer public @Inject constructor(name: String) { /*……*/ }
// 构造函数参数支持默认值
class Person(firstName: String = "Foo") { /*……*/ }

如果一个非抽象类没有声明任何(主或次)构造函数,它会有一个生成的不带参数的主构造函数。构造函数的可见性是 public。如果你不希望你的类有一个公有构造函数,那么声明一个带有非默认可见性的空的主构造函数:单例就要这么写构造函数

class DontCreateMe private constructor() { /*……*/ }

在 JVM 上,如果主构造函数的所有的参数都有默认值,编译器会生成一个额外的无参构造函数,它将使用默认值。这使得 Kotlin 更易于使用像 Jackson 或者 JPA 这样的通过无参构造函数创建类的实例的库。

属性

声明类属性时,可以使用尾部逗号

class Person(
    val firstName: String,
    val lastName: String,
    var age: Int, // 尾部逗号
) { /*……*/ }

创建类的实例

创建对象没有 Java 中的 new 关键字,直接调用类名对应的构造函数即可

val invoice = Invoice()
val customer = Customer("Joe Smith")

伴生对象

如果你需要写一个可以无需用一个类的实例来调用、但需要访问类内部的函数(例如,工厂方法),你可以把它写成该类内对象声明中的一员。

更具体地讲,如果在你的类内声明了一个伴生对象, 你就可以访问其成员,只是以类名作为限定符。

class User(val name: String) {
    // 定义伴生对象
    companion object Factory {
        fun create(name: String): User = User(name)
    }
}

fun main(){
    // 直接通过类名调用伴生对象的方法,相当于 static 方法调用了
    val userInstance = User.create("John Doe")
    println(userInstance.name)  // John Doe
}

初始化代码块

初始化代码块可以有多个,按顺序执行

class A {
    
    init {
        println("初始化代码块")
    }
}

open关键字

open 关键字和 java 中的 final 用在类上是相反的,final 是禁止有子类,而 open 是允许有子类。默认情况下 Kotlin 中所有类都是 final 的

open class Base {
    // 可以被重写
    open fun v() {}
    
    // 不可被重写
    fun nv() {}
}

internal 关键字 - 内部类

internal 修饰类的方法,表示这个类方法只适合当前module使用,如果其他module使用的话,会找不到这个internal方法或者报错。

数据类 data class

https://kotlinlang.org/docs/data-classes.html

基本等效于DTO、POJO等类

data class Customer(val name: String, val email: String)

自动具有下面的方法

  • getters (and setters in case of vars) for all properties
  • equals()
  • hashCode()
  • toString()
  • copy()
  • component1(), component2()......componentN()

copy 方法

函数

Kotlin 函数定义使用 fun 关键字

函数定义参数列表支持尾部逗号

fun powerOf(
    number: Int,
    exponent: Int, // trailing comma
) { /*...*/ }

函数参数默认值

Kotlin 支持函数参数默认值,这个特性也叫做可选参数

fun read(
    b: ByteArray,
    off: Int = 0,
    len: Int = b.size,
) { /*...*/ }

有下面几点注意事项

  1. 重载父类的函数时,子类函数会使用父类的函数的默认值,所以在子类函数中重新声明默认值是不允许的
open class A {
    open fun foo(i: Int = 10) { /*...*/ }
}

class B : A() {
    override fun foo(i: Int) { /*...*/ }  // No default value is allowed.
}

class C : A() {
    // error: An overriding function is not allowed to specify default values for its parameters
    override fun foo(i: Int = 20) { /*...*/ }  
}
  1. 如果一个具有默认值的参数位于一个没有默认值的变量之前,则只能通过调用具有命名参数的函数来使用默认值
fun foo(
    bar: Int = 0,
    baz: Int,
) { /*...*/ }

foo(baz = 1) // The default value bar = 0 is used
  1. 如果所有具有默认值的参数之后的最后一个参数是函数类型,则可以将相应的lambda参数作为命名参数传递,也可以在括号外传递
fun foo(
    bar: Int = 0,
    baz: Int = 1,
    qux: () -> Unit,
) { /*...*/ }

foo(1) { println("hello") }     // Uses the default value baz = 1
foo(qux = { println("hello") }) // Uses both default values bar = 0 and baz = 1
foo { println("hello") }        // Uses both default values bar = 0 and baz = 1

命名参数

调用函数时可以命名一个或多个函数的参数。当函数有很多参数并且很难将值与参数相关联时,这会很有用,特别是当它是布尔值或空值时。
在函数调用中使用命名参数时,你可以自由地更改它们的列出顺序。如果想使用它们的默认值,你可以完全省略这些参数。

fun reformat(
    str: String,
    normalizeCase: Boolean = true,
    upperCaseFirstLetter: Boolean = true,
    divideByCamelHumps: Boolean = false,
    wordSeparator: Char = ' ',
) { /*...*/ }

调用函数时,无需为所有参数指定名称

reformat(
    "String!",
    false,
    upperCaseFirstLetter = false,
    divideByCamelHumps = true,
    '_'
)

void 函数

如果函数没有返回有用的值,则其返回类型为Unit。Unit是一种只有一个值的类型-Unit。此值不必显式返回:

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello $name")
    else
        println("Hi there!")
    // `return Unit` or `return` is optional
}

Unit 可以不用写,例如:

 // `return Unit` or `return` is optional
fun printHello(name: String?) {}

表达式函数

当函数体由单个表达式组成时,可以省略花括号,并在=符号后指定函数体

fun double(x: Int): Int = x * 2
// 返回类型可省略,编译器可以类型推断出来
fun double(x: Int) = x * 2

非表达式函数必须始终明确指定返回类型,除非返回Unit。有些语言类型推断很强,可以不用写返回类型。但是 Kotlin 不会推断具有块体的函数的返回类型,这个是设计上如此,因为这些函数在函数体体中可能有复杂的控制流,并且返回类型对读者来说并不明显,有时甚至对编译器来说也这样。

可变参数

使用 vararg 修饰符标记函数的最后一个参数为可变参数,就是不知道会有多少个参数

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}

函数类型

https://kotlinlang.org/docs/lambdas.html#function-types

Kotlin 中具有函数类型,支持函数类型的变量定义

fun main() {
    // 定义匿名函数
    val func: () -> Unit = fun() {
        println("匿名函数")
    }
    
    // func 变量的类型是: 
    func() // 调用函数
}

函数类型命名

为函数设置类型别名

typealias FuncType1 = (Int, Int) -> Unit

在定义函数类型时,支持对参数进行命名,这些名称可用于记录参数的含义,起到添加注释文档的作用

typealias FuncType2 = (a: Int, b: Int) -> Unit

创建函数类型的对象

  1. 通过函数字面量进行创建
// 通过lambda 创建
val func1 = { a: Int, b: Int -> a + b }

// 通过匿名函数创建
val func2 = fun(s: String): Int { return s.toIntOrNull() ?: 0 }
  1. 使用函数引用进行创建:支持各种各样的函数定义
class N
fun double(x: Int): Int = x * 2
fun Int.isOdd(x: Int): Boolean = x == 2

fun main() {
    fun localFun() {}
    // 本地函数引用
    val func2 = ::localFun
    // 顶层函数引用
    val func3 = ::double
    // 成员函数引用,不支持函数重载
    val func4 = List<Int>::size
    val func5 = String::intern
    val func6 = String::replace  // 编译错误:Overload resolution ambiguity. All these functions match.
    // 构造函数引用
    val func7 = ::N
    // 扩展属性函数引用
    val func8 = Int::isOdd;
}
  1. 通过类实现某个函数类型,创建类对象从而创建函数对象
class IntTransformer: (Int) -> Int {
    override operator fun invoke(x: Int): Int = TODO()
}

val intFunction: (Int) -> Int = IntTransformer()

调用函数对象

和 js 一样,Kotlin 调用函数对象的方式有以下方式:

  1. 通过函数名称调用

  2. 函数对象有 invoke 方法,和 js 函数的 call 方法一样

fun main() {
    val stringPlus: (String, String) -> String = String::plus
    val intPlus: Int.(Int) -> Int = Int::plus

    println(stringPlus.invoke("<-", "->"))
    println(stringPlus("Hello, ", "world!"))

    println(intPlus.invoke(1, 1))
    println(intPlus(1, 2))
    println(2.intPlus(3)) // extension-like call
}

扩展函数

https://www.kotlinprimer.com/extension-functions/basics/functions-with-receiver/

这个特性简单来说就是:向别人定义好的类添加方法。比如为 JDK 中的 String 类添加一个方法:

fun String.lastChar(): Char = this.get(this.length - 1)
// 或者
fun String.lastChar(): Char = this[this.length - 1]

然后就可以在任意String类型上调用此方法

fun main() {
    println("hello world".lastChar()) // d
}

其中 String 类型被称为 receiver type,而调用扩展函数的对象实例被称为 receiver object:

image-20251024003035437

此外 this 可省略:

fun String.lastChar(): Char = get(length - 1)

局部函数

简单来说:函数内定义函数。局部函数的作用这里不再赘述

class User(val id: Int, val name: String, val address: String)

fun saveUser(user: User) {
    // 定义局部函数
    fun validate(
        user: User,
        value: String,
        fieldName: String
    ) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(
                "Cannot save user ${user.id}: $fieldName is empty"
            )
        }
    }
    validate(user, user.name, "Name")
    validate(user, user.address, "Address")
    // Save user to the database
}

Java中需要借助局部类来实现类似的功能

public class Main {
  public static void main(String[] args) {
    // 局部类
    class A {
      // 类似局部函数功能
      public void method() {
      }
    }
  }
}

Lambda 函数

https://kotlinlang.org/docs/lambdas.html

lambda 定义和 Java 一样,使用单箭头 ->

val sum: (Int, Int) -> Int = { x: Int, y: Int -> x + y }

比如这个函数可以具体为下面的函数

fun <T, R> Collection<T>.fold(
    initial: R,
    combine: (acc: R, nextElement: T) -> R
): R {
    var accumulator: R = initial
    for (element: T in this) {
        accumulator = combine(accumulator, element)
    }
    return accumulator
}
ints.filter { it > 0 } // this literal is of type '(it: Int) -> Boolean'

匿名函数

匿名函数就是没有函数名称的函数

max(strings, { a, b -> a.length < b.length })

这个函数定义可以长下面这样:

fun compare(a: String, b: String): Boolean = a.length < b.length
fun compareString(a: String, b: String): Boolean = a.length < b.length

闭包 closure

闭包是可以在函数体中访问的变量的作用域。

lambda 表达式、匿名函数、局部函数和对象表达式可以访问其闭包,其中包括在外部作用域中声明的变量。闭包中捕获的变量可以在lambda中修改:

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

内联函数

https://kotlinlang.org/docs/inline-functions.html

Function Receiver

https://discuss.kotlinlang.org/t/what-is-a-receiver/18588

函数类型可以定义一个 receiver 类型,例如函数定义:A.(B) -> C,其中 A 就是 receiver 类型,这么写表示这个函数可以在 A 这个对象上调用,调用时需要传递一个 B 类型的参数,并且会返回 C 类型。

https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver

Function literals with receiver

https://kotlinlang.org/docs/lambdas.html#function-literals-with-receiver

函数式接口/SAM

https://book.kotlincn.net/text/fun-interfaces.html

摘自:https://blog.csdn.net/vitaviva/article/details/104055623

只有一个抽象成员函数的接口称为函数式接口单一抽象方法(SAM,Single Abstract Method)接口。函数式接口可以有多个非抽象成员函数,但只能有一个抽象成员函数。

interface ToString {
    String toString(Object object);
}

class A {
    public String delegateToString(ToString strategy) {
		return strategy.toString(this);
    }
}

需要实现一个ToString的匿名内部类对象,即使只有一个方法,也要对方法进行声明。在Java8之后,我们可以使用Lambda对调用进行简化:

public static void main(String[] args) {
  A a = new A();
  // java8之前
  a.delegateToString(new ToString() {
    @Override
    public String toString(Object object) {
      return object.toString();
    }
  });
  // java8 之后可以这么写
  a.delegateToString(object -> object.toString());
  a.delegateToString(Object::toString);
}

这种借助Lambda对Parser的调用称为SAM转换(Single Abstract Method Conversions),在Kotlin中使用在Java中定义的delegateToString方法时,SAM转换同样有效:

fun main() {
    val a = A()

    a.delegateToString { it.toString() }
}

Kotlin将Java的接口翻译成了lambda,因此在kotlin的同名方法实际变成了一个高阶函数

String delegateToString(ToString strategy)  => fun delegateToString(f: (obj) -> String)

当在Kotlin中定义相同的delegateToString方法时(仍使用Java定义的ToString接口)

class B {
    fun delegateToString(strategy: ToString): String? {
        return strategy.toString(this)
    }
}

fun main() {
    // Argument type mismatch: actual type is 'Function0<String>', but 'ToString' was expected.
    // 注:1.8.20似乎已经没有此限制了,下面这行不会报错
    B().delegateToString { it.toString() }
}

此时delegateToString在Kotlin中有明确定义,并不是一个高阶函数,所以此时只能用以下两种方式调用:

// 匿名类对象
B().delegateToString(object : ToString {
    override fun toString(obj: Any?): String {
        return obj.toString()
    }
})
// 使用SAM转换方式
B().delegateToString(ToString {
    it.toString()
})

如果将ToString接口改成Kotlin版本的,如下所示:

interface ToStringKt {
    fun toString(obj: Any?): String?
}

class B {
    fun delegateToString(strategy: ToStringKt): String? {
        return strategy.toString(this)
    }
}

fun main() {
    // Error: Argument type mismatch: actual type is 'Function0<String>', but 'ToStringKt' was expected.
    B().delegateToString { it.toString() }

    // No Error
    B().delegateToString(object : ToStringKt {
        override fun toString(obj: Any?): String {
            return obj.toString()
        }
    })

    // Error: Interface 'interface ToStringKt : Any' does not have constructors.
    B().delegateToString(ToStringKt {
        it.toString()
    })
}

此时解决办法是将ToStringKt改为函数式接口

fun interface ToStringKt {
    fun toString(obj: Any?): String?
}

class B {
    fun delegateToString(strategy: ToStringKt): String? {
        return strategy.toString(this)
    }
}

fun main() {
    // Error: Argument type mismatch: actual type is 'Function0<String>', but 'ToStringKt' was expected.
    B().delegateToString { it.toString() }

    // Hint: Convert to lambda
    B().delegateToString(object : ToStringKt {		// 写法1
        override fun toString(obj: Any?): String {
            return obj.toString()
        }
    })
	// 写法1可简化为写法2
    B().delegateToString({ it.toString() })   // 写法2
	// 写法2还可简化为
    B().delegateToString { it.toString() }  // 写法3
}

运算符重载

协程

参见:https://www.cnblogs.com/vonlinee/p/19223443

JVM平台注解

@JvmStatic

对于标注的元素,如果它是函数,则需要从此元素生成额外的静态方法。如果此元素是属性,则应生成额外的静态 getter / setter 方法。

class A {
    companion object {
        @JvmStatic
        val VAL = 1

        val nonStaticValue = 2

        @JvmStatic
        fun callStatic() {
            println("callStatic")
        }

        fun callNonStatic() {
            println("callNonStatic")
        }
    }
}

fun main() {
    A.callStatic()
    A.Companion.callNonStatic()
    A.VAL
    A.Companion.nonStaticValue
}

@JvmField

使用@JvmField,在 Java 中调用的时候,可以直接使用属性名,而不是对应的getter方法。

class A(@JvmField val a: String, val b: String)
posted @ 2025-08-15 20:32  vonlinee  阅读(12)  评论(0)    收藏  举报