Kotlin语法糖

本文介绍了Kotlin相比Java、Groovy和Scala的优势,特别是在语法简洁性和性能上的表现。作者强调了Kotlin的顶层函数、类型推断、空安全特性和智能转换等功能,以及在循环、判断、集合操作和函数使用上的便利性。同时,讨论了Kotlin的数据类、扩展函数和对象声明等特性,展示了Kotlin如何提高开发效率。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

    用Kotlin一段时间了,主要是用来做本地数据分析,需要大量的字符串和集合处理,用原生Java着实麻烦了点。Python虽然也可以用,奈何公司自研的分布式数据库只支持Java访问,只能找一个JVM语言来用了。

     Groovy和Scala都尝试过:

    Groovy对Java兼容的实在是太细致入微了(我曾经把Java代码原样贴到Groovy脚本里,直接运行无压力),以至于我根本没有动力按照Groovy的最佳实践来进行开发。而且,不论是自己的实践还是第三方测评都已经证明了Groovy的性能相较于于原生Java差一倍左右。

    Scala则是另一个极端,它提供了极其复杂的设计,上手的速度让人难以接受。此外,其编译的速度同样让人难受;再此外,无论是Eclipse还是Idea对Scala的支持都不是很美好(至少在我尝试Scala的时候是这样的)。

    Kotlin则不同,JetBrains公司自己设计的语言,Idea对其支持是全面而深入的(根本不会出现代码提示的时候卡顿的情况,Scala呵呵哒)。当Google宣布Kotlin成为Android官方开发语言的时候,另一种叫做巨头站队的确定性加入到这门语言中。Kotlin对Java语法的支持是很节制的,甚至在Idea层面上,从Java拷贝的代码到Kotlin文件中,会做好自动转化而不是原样输出。

    出于规避Oracle大流氓的角度考虑,切换其他的JVM语言是需要认真考虑的事情。虽然直接切换到其他平台,例如Go,也是可以考虑的,然而这将导致巨大的重写成本以及基础库的切换成本,我司自研数据库的访问就是一个例子。

    

    以上。

    

    在我使用Kotlin的这段时间里,从需求场景出发,最大的体会是它相当顺手的语法特征以及几乎没有门槛的切换,相对于原生Java来说,对我是相当可口的语法糖。本文不是严谨的一步步的语法教程,也不是详细的对比介绍,仅仅是使用过程中对Kotlin方便好用感触较深的点做相对全面的记录。基

    础语法可以参考 kotlincn.net


第一眼差别

顶层属性 和 顶层函数

    其实顶层函数和顶层属性的意义是差不多的,很像脚本的写法,不需要包裹在一个Class或者Object中,而单独在脚本中出现。

    静态函数是不包含状态的,也就是所谓的纯函数,它的输入仅仅来自于它的参数列表,那么调用的时候需要通过一个对象或者类中是非常多余的,因此在Kotlin中认为一个函数或方法有时候并不是属于任何一个类,它可以独立存在。

    顶层属性和顶层函数的作用于是包,即同一个包中可以不用Import而直接调用。而跨包的调用则需要Import。

    原理上,顶层文件会反编译成一个容器类。(类名一般默认就是顶层文件名+”Kt”后缀,注意容器类名可以自定义,通过@JvmName),顶层函数会反编译成一个static静态函数

 

Main函数可以作为顶层函数

    直接作为顶层函数,可以没有类定义而直接运行。偏向于脚本的形式。

 

val(常量)、var(变量)、lateinit、by lazy{}

    Kotlin中val之定义并赋值之后就无法变更的量,相当于Java中的final变量。而var则是可变量。在函数式编程中,是推荐尽量使用减少var,可变量在使用过程中很容易出现线程不安全的情况。具体参见 “理解Scala的函数式风格:从var到val的转变”。Scala和Kotlin在这一点上是一致的。

    kotlin中默认是空安全的,任何属性的声明都必须有初始化值,如果支持可空”?”,才能把属性声明为null。那么一个非空的变量如果需要在后面赋值,则需要用lateinit。lateinit var只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。lateinit var的作用也比较简单,就是让编译期在检查时不要因为属性变量未被初始化而报错。lateinit可以在任何位置初始化并且可以初始化多次。

    而 val 的延迟初始化则是要用 by lazy { 代码块 }。by lazy只能作用于val关键字标注的属性。当属性用到的时候才会初始化”lazy{}”里面的内容,而且再次调用属性的时候,只会得到结果,而不会再次执行lazy{}的运行过程。

    

类型推断

    kotlin类型名首字母是大写的 变量名是小写的 以示区分。形式定义为:var 变量名 : 类型名 = 值。对于基本数据类型,可以直接省略类型名,而由系统进行类型推断。

 

Import

    如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义:

    import foo.Bar     // Bar 可访问

    import bar.Bar as bBar // bBar 代表“bar.Bar”

 

可读的数字

    val creditCardNumber = 1234_5678_9012_3456L

 

常见功能简化:

    println() 等同于 System.out.println()。

    不需要“new”关键字  val rectangle = Rectangle(5.0, 2.0)

    强制类型转换:”123“.toInt() 看上去更加顺畅

    ‘==‘和‘===‘的区别,双等号比较kotlin团队的处理就是通过Intrinsics.areEqual(b, c)来比较两个对象的值是否相等,三个等号就是通过java中的‘==‘比较两个对象的地址是否相等。

    类型判断:obj is String

    属性默认为final,除非申明为 open(跟val一样的原因,不推荐使用可变量)

 

没有Object类型,只有Any

    Kotlin中所有的类都有一个共同的基类Any,如果类没有申明继承其他类的话,默认继承的就是Any。它只有三个方法,equals,hashCode和toString。而去掉了Object关于同步相关的函数(wait, notify等)。

 

?和!!

    "?"加在变量名后,系统在任何情况不会报它的空指针异常,用于显式标记变量可以为空。"!!"加在变量名后,如果对象为null,那么系统一定会报异常,用于显式标记变量绝对不能为空。这两个符号能让开发者明确对变量空值的感知。任何一个变量,如果不加上?,相当于默认加上了!!。

    roomList?.size 返回的是 null 或者是 roomList的大小。

 

字符串相关

    多行字符串:

        你可以通过 trimMargin() 函数去除前导空格:

        val text = """

                        |Tell me and I forget.

                        |Teach me and I remember.

                        |Involve me and I learn.

                        |(Benjamin Franklin)

                        """.trimMargin()

         默认 | 用作边界前缀,但你可以选择其他字符并作为参数传入,比如 trimMargin(">")。

    字符串模板

        val s = "abc"

        println("$s.length is ${s.length}")

 


循环和判断

If not null 缩写

val files = File("Test").listFiles()

println(files?.size)

val b: Boolean? = ……

if (b == true) {

    ……

} else {

    // `b` 是 false 或者 null

}

If not null and else 缩写

    val files = File("Test").listFiles()

    println(files?.size ?: "empty")

if null 执行一个语句

    val values = ……

    val email = values["email"] ?: throw IllegalStateException("Email is missing!)

    val email = values[“key”]!!

    在可能会空的集合中取第一元素

        val emails = …… // 可能会是空集合

        val mainEmail = emails.firstOrNull() ?: ""

    映射可空值(如果非空的话)

        val value = ……

        val mapped = value?.let { transformValue(it) } ?: defaultValueIfValueIsNull

 

When(升级版swit)

    // 直接返回表达式结果

    gameCount[thisUserGameCount] = when (gameCount.containsKey(thisUserGameCount)) {

            true -> gameCount[thisUserGameCount]!! + 1

            false -> 1

    } 

    // 我们可以用任意表达式(而不只是常量)作为分支条件

    when (obj) {

        1          -> "One"

        "Hello"    -> "Greeting"

        is Long    -> "Long"

        !is String -> "Not a string"

        else       ->  throw IllegalArgumentException("Invalid color param value")

    }

    

For 和 区间

    for (i in 1..100) { …… }  // 闭区间:包含 100

    for (i in 1 until 100) { …… } // 半开区间:不包含 100

    for (x in 2..10 step 2) { …… }

    for (x in 10 downTo 1) { …… }

    if (x in 1..10) { …… }

    for (index in items.indices) {

        println("item at $index is ${items[index]}")

    }

    for ((k, v) in map) {

        println("$k -> $v")

    }

    x !in a..b

 

直接返回代码块结果

返回 when 表达式

fun transform(color: String): Int {

    return when (color) {

        "Red" -> 0

        "Green" -> 1

        "Blue" -> 2

        else -> throw IllegalArgumentException("Invalid")

    }

}

返回 “try/catch”表达式

fun test() {

    val result = try {

        count()

    } catch (e: ArithmeticException) {

        throw IllegalStateException(e)

    }

    // 使用 result

}

“if”表达式

val result = if (param == 1) {

        "one"

    } else if (param == 2) {

        "two"

    }

 


Collection相关

创建

    arrayOf(1, 2, 3) 创建了 array [1, 2, 3]。 

    库函数 arrayOfNulls() 可以用于创建一个指定大小的、所有元素都为空的数组。

    接受数组大小以及一个函数参数的 Array 构造函数,用作参数的函数能够返回给定索引的每个元素初始值:

        val asc = Array(5, { i -> (i * i).toString() })    // 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"]

 

可变和不可变集合

    listOf 创建不可变LIst,跟Java不同,除了不能改变List的引用之外,也不能变更List的内容;

    mutableListOf,可以变更内容的List。

    setOf, mutableSetOf,mapOf, mutableMapOf:与上述内容类型

    

不型变(invariant)

    这意味着 Kotlin 不让我们把 Array<String>赋值给 Array<Any>,以防止可能的运行时失败。(但是你可以使用 Array<out Any>, 参见类型投影)

 

常用函数(实用)

    list.joinToString(

         separator = " | ",

         prefix = "(",

         postfix = ")"

    )

    val sortedMapByValue = map.toSortedMap()

    val sortedMap = map.toList().sortedBy { (key, value) -> key }.toMap()val sortedMapByValue = map.toSortedMap()

    val sortedMap = map.toList().sortedBy { (key, value) -> key }.toMap()

 

取值

    方括号取值,如 arr[0]。

 

流式处理(用法跟Java差别不大)

    filter, map, reduce, all, any, count, find

 


函数相关

函数定义

    单行定义:fun sum(a: Int, b: Int) = a + b

    指定是否可能返回空:fun parseInt(str: String): Int? {……}

    

默认参数

    可以省去不需要的参数传递,无需像Java那样为了传递默认参数而定义一堆多态的函数。

 

fun someFunction( param1: String, 

    param1: Boolean = true,   

    param2: Boolean = true, 

    param3: String = “3"

) {

    // function body

}

 

函数有默认值的时候,参数有默认值的话,我们可以不传这个参数

 

someFunction("aa”,false)

//这样相当于someFunction("aa”,false, true,”3")

如果要在多个默认参数之中,设置个别参数的值,就可以使用命名参数了。

 

someFunction("aa”, param2 = true)

//这样相当于

someFunction("aa",true, false,”3")

中缀函数

    infix fun String.中缀(param: String): String {

        return "This:" + this + “, Param” +param

    }

    val resultInfix = "北京" 中缀 "上海"

 

let、with、run、apply、also函数

    // if not null 执行代码

    val value = ……

    value?.let {

        …… // 代码会执行到此处, 假如data不为null

    }

 

方法可以嵌套

    即函数内部还可以定义函数。而且局部函数可以访问外部函数(即闭包)的局部变量。

 

标签处返回

由于推荐使用Lambda表达式,而默认的Lambda表达式中的return是针对函数的(因为Lambda表达式的作用跟 for()循环 类似)。因此标签处返回最重要的一个用途就是从 lambda 表达式中返回。

例如,直接返回的例子:

fun foo() {

    listOf(1, 2, 3, 4, 5).forEach {

        if (it == 3) return // 非局部直接返回到 foo() 的调用者

        print(it)

    }

    println("this point is unreachable")

}

这个 return 表达式从最直接包围它的函数即 foo 中返回。 

如果我们需要从 lambda 表达式中返回,我们必须给它加标签并用以限制 return。

fun foo() {

    listOf(1, 2, 3, 4, 5).forEach lit@{

        if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者,即 forEach 循环

        print(it)

    }

    print(" done with explicit label")

}

现在,它只会从 lambda 表达式中返回。

通常情况下使用隐式标签更方便。 该标签与接受该 lambda 的函数同名。

fun foo() {

    listOf(1, 2, 3, 4, 5).forEach {

        if (it == 3) return@forEach // 局部返回到该 lambda 表达式的调用者,即 forEach 循环

        print(it)

    }

    print(" done with implicit label")

}

前文三个示例中使用的局部返回类似于在常规循环中使用 continue。并没有 break 的直接等价形式,不过可以通过增加另一层嵌套 lambda 表达式并从其中非局部返回来模拟:

fun foo() {

    run loop@{

        listOf(1, 2, 3, 4, 5).forEach {

            if (it == 3) return@loop // 从传入 run 的 lambda 表达式非局部返回

            print(it)

        }

    }

    print(" done with nested loop")

}

 

函数作为第一公民

fun main(args: Array<String>) {

    // 函数作为参数

    fun testFunParam(param1:Int, funcParam: (i:Int, j:String) -> Int) {

        val default = "200"

        println( param1 + funcParam(param1, default) )

    }

    // 定义Lambda表达式作为变量

    val lambda1: (i:Int, j:String) -> Int =  { first, second ->

        first + second.toInt()

    }

    testFunParam(1, lambda1)

    // 等价于

    testFunParam(1) { first, second ->

        first * second.toInt()

    }

}

 

函数作为第一公民的好处,需要对比Java来看。以下列出Java使用Lambda表达式的方式。

Java中实际上是通过定义 FunctionalInterface 来实现Lambda表达式的

Java里面实际上是借用的对象实例化。

// 1.1 使用匿名内部类根据 name 排序 players

Arrays.sort(players, new Comparator<String>() {

    @Override

    public int compare(String s1, String s2) {

        return (s1.compareTo(s2));

    }

});

// 1.2 使用 lambda expression型变量

Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2));

Arrays.sort(players, sortByName);

// 1.3 直接使用Lambda表达式

Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2)));

假设有100个参数的Lambda,没有现成的对象可用,那么就只能先新建类,再新建这个实体作为参数传入

@FunctionalInterface

public interface LambdaWithManyParams {

    default void doDefaultWork() {

        // Method body

    }

    static void doStaticWork(){

        System.out.println("Hello");

    }

    // 在这个接口里面只能有一个抽象方法。

    void run(int a, int b, int c, int d);

}

相对于这一点,Kotlin则完全无需任何预定义就可以进行Lambda表达式的声明。

 


类和对象

数据类

我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data:

    data class User(val name: String, val age: Int)

编译器自动从主构造函数中声明的所有属性导出以下成员:

    equals()/hashCode() 对;

    toString() 格式是 "User(name=John, age=42)";

    componentN() 函数 按声明顺序对应于所有属性;

    copy() 函数(见下文)。

为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:

    主构造函数需要至少有一个参数;

    主构造函数的所有参数需要标记为 val 或 var;

    数据类不能是抽象、开放、密封或者内部的;

此外,成员生成遵循关于成员继承的这些规则:

如果在数据类体中有显式实现 equals()、 hashCode() 或者 toString(),或者这些函数在父类中有final 实现,那么不会生成这些函数,而会使用现有函数;

 

复制

在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 在数据库更新操作中特别多。copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:

    fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

这让我们可以写:

    val jack = User(name = "Jack", age = 1)

    val olderJack = jack.copy(age = 2)

 

属性扩展和函数扩展

属性扩展

    var StringBuilder.lastChar: Char get() = get(length -1)

        set(value: Char) { this.setCharAt(length -1, value)

    }

    fun main() {

        val sb = StringBuilder("kotlin") println(sb.lastChar) sb.lastChar = '!' println(sb.lastChar)

    }

函数扩展:

        fun String.selfDefinedFun() { …… }

        “String”.selfDefinedFun()

放手去用扩展函数。每当你有一个主要用于某个对象的函数时,可以考虑使其成为一个以该对象为接收者的扩展函数。

但是为了尽量减少 API 污染,尽可能地限制扩展函数的可见性。

根据需要,使用局部扩展函数、成员扩展函数或者具有私有可视性的顶层扩展函数。

 

object

将类的声明和定义该类的单例对象结合在一起(即通过object就实现了单例模式)。

object declaration的类最终被编译成:一个类拥有一个静态成员来持有对自己的引用,并且这个静态成员的名称为INSTANCE,当然这个INSTANCE是单例的,故这里可以这么去使用。

和普通类的声明一样,可以包含属性、方法、初始化代码块以及可以继承其他类或者实现某个接口,但是object它不能包含构造器。

它也可以定义在一个类的内部。

 

伴生对象: 

在Kotlin中是没有static关键字的,也就是意味着没有了静态方法和静态成员,在 Kotlin 中类没有静态方法。在大多数情况下,它建议简单地使用包级函数。如果确实需要,可以用伴生对象。

    class A { 

        companion object coName(可以省略){ 

            //methods and fields 

        } 

    }

实际上底层实现是把它当做静态内部类来看待的,并且目标类会持有该内部类的一个引用,那么最终调用的方法实际上是定义在这个静态内部类中的实例方法。

那么在伴生对象中定义的方法和包级别函数(顶层函数)有什么区别呢?而顶层函数则是作为一个包装类的静态方法出现的。如Test.kt里面有一个main方法,则会生成一个TestKt.class,而main方法则是这个类的静态方法。

类中的静态方法和内部类中的实例方法的区别,因为成员内部类中的方法是可以访问外部中定义的方法和成员的,哪怕是private的,而静态方法是做不到这一点的。

 

继承

要声明一个显式的超类型,我们把类型放到类头的冒号之后:

    open class Base(p: Int)

    class Derived(p: Int) : Base(p)

类上的 open 标注与 Java 中 final 相反,它允许其他类 从这个类继承。默认情况下,在 Kotlin 中所有的类都是 final。

不写open关键字的:类、方法、属性是不能被继承和重写的。

    要在父类被重写的方法或属性前加open关键字,并且子类中重写的方法或属性前都要加override关键字

    可以用一个 var 属性覆盖一个 val 属性,但反之则不行。这是允许的,因为一个 val 属性本质上声明了一个 getter 方法,而将其覆盖为 var 只是在子类中额外声明一个 setter 方法

    覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须把子类重写的中省略默认参数值

 

接口

    既包含抽象方法的声明,也包含实现。这个跟Java8中提供的特性一样,但是Kotlin可是兼容到Java6的。相当于在老版本实现了高版本的功能。

    在接口中声明的属性要么是抽象的,要么提供 访问器的实现。

 

this

为了表示当前的 接收者 我们使用 this 表达式:在类的成员中,this 指的是该类的当前对象。

在扩展函数或者带有接收者的函数字面值中, this 表示在点左侧传递的 接收者 参数。

如果 this 没有限定符,它指的是最内层的包含它的作用域。

要引用其他作用域中的 this,请使用 标签限定符:限定的 this

要访问来自外部作用域的this(一个类 或者扩展函数, 或者带标签的带有接收者的函数字面值)我们使用this@label,其中 @label 是一个代指 this 来源的标签:

class A { // 隐式标签 @A

    inner class B { // 隐式标签 @B

        fun Int.foo() { // 隐式标签 @foo

            val a = this@A // A 的 this

            val b = this@B // B 的 this

            val c = this // foo() 的接收者,一个 Int

            val c1 = this@foo // foo() 的接收者,一个 Int

            val funLit = lambda@ fun String.() {

                val d = this // funLit 的接收者

            }

            val funLit2 = { s: String ->

                // foo() 的接收者,因为它包含的 lambda 表达式

                // 没有任何接收者

                val d1 = this

            }

        }

    }

}

 

 


参考

Kotlin 和 Scala的对比:https://superkotlin.com/kotlin-vs-scala/

Kotlin中文网: https://www.kotlincn.net/

Kotlin系列之顶层函数、中缀调用、解构声明 https://blog.youkuaiyun.com/u013064109/article/details/79887528 

理解Scala的函数式风格:从var到val的转变  http://developer.51cto.com/art/200907/134956.htm

kotlin中val和var不为人知的故事 https://www.jianshu.com/p/4c7ca01fe99f

Kotlin系列之let、with、run、apply、also函数的使用 https://blog.youkuaiyun.com/u013064109/article/details/78786646

Kotlin学习系列之:object关键字的使用场景 https://blog.youkuaiyun.com/xlh1191860939/article/details/79460601

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值