Kotlin语法初探

本文参考资料

引言

最近接手一个项目是用 IDE 把之前的 Java 代码转成了 Kotlin 的,索性现在就多花些时间来窥探一下 Kotlin,计划在接下来的开发中使用 Kotlin 来完成。

Kotlin应该怎么读

Kotlin 在 2016 年发布了其第一个稳定版本 v1.0,紧跟着在 2017 年的 Google I/O 大会就正式宣布被 Android 平台支持。

Kotlin 具有很多下一代编程语言,静态语言特性:如类型推断、多范式支持、可空性表达、扩展函数、模式匹配等。

其主要设计目标:

  • 创建一种兼容 Java 的语言
  • 让它比 Java 更安全,能够静态检测常见的陷阱。如:引用空指针
  • 让它比 Java 更简洁,通过支持 variable type inference,higher-order functions (closures),extension functions,mixins and first-class delegation 等实现。
  • 让它比最成熟的竞争对手 Scala 语言更加简单。

函数编程核心概念

头等函数

把函数当做值来使用,可以用变量保存它,把它当做参数传递,或者当做其他函数的返回值。

不可变性

使用不可变对象,创建后状态不能再变化。

表达式函数体

fun max(a:Int, b:Int): Int{
    return if(a > b) a else b
}

在kotlin中除了(for、do 和 do\while)以外大多数控制结构都是表达式。

fun max(a:Int, b:Int): Int = if(a > b) a else b

可变变量和不可变变量

val : 不可变变量

var :可变变量

vararg : 可变参数(类似 Java 中的 String…,用于参数声明)

可以使用 abstract 修饰属性,必须在子类初始化。

abstract class Person{
    abstract val name : String
}

class XiaoQiang : Person(){
    override val name : String = "xiaoqiang"
}

延迟初始化

一般地,属性声明为非空类型必须在构造函数中初始化。 然而,这经常不方便。例如:属性可以通过依赖注入来初始化, 或者在单元测试的 setup 方法中初始化。 这种情况下,你不能在构造函数内提供一个非空初始器。 但你仍然想在类体中引用该属性时避免空检查。

为处理这种情况,你可以用 lateinit 修饰符标记该属性:

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // 直接解引用
    }
}

字符串模板

var a = 1
// 模板中的简单名称:
val s1 = "a is $a" 

a = 2
// 模板中的任意表达式:
val s2 = "${s1.replace("is", "was")}, but now is $a"

类和属性

类的定义

在 Kotlin 中的一个类可以有一个主构造函数以及一个或多个次构造函数。主构造函数是类头的一部分:它跟在类名(与可选的类型参数)后。

class Person(val name:String, var age:Int? = null)

val 声明了只读属性(只有 getter) var 声明了读写属性(有 getter 和 setter)

主构造函数不能包含任何的代码。初始化的代码可以放到以 init 关键字作为前缀的初始化块(initializer blocks)中。

class InitOrderDemo(name: String) {

    val firstProperty = "First property: $name".also(::println)
    
    init {
        println("First initializer block that prints ${name}")
    }
    
    val secondProperty = "Second property: ${name.length}".also(::println)
    
    init {
        println("Second initializer block that prints ${name.length}")
    }
}

初始化顺序自上而下。

如果构造函数有注解或可见性修饰符,这个 constructor 关键字是必需的,并且这些修饰符在它前面:

class Customer public @Inject constructor(name: String) { …… }

自定义访问器

class Person(val name:String, var age:Int? = null){
    val isMarried: Boolean
    	get() {
            return age > 30
        }
}

枚举和when

在 kotlin 中 enum 需要出现在 class 前面才有意义。

enum class Color {RED, YELLOW, BLUE}

和 Java 一样,枚举并不是值的列表,可以给枚举声明属性和方法。

enum class Color(var r: Int, var g: Int, var b: Int){
    RED(255, 0, 0), YELLOW(255, 255, 0), BLUE(0, 0, 255)
}

和if相似,when 是一个有返回值的表达式

fun getColor(color: Color){
    when(color){
        Color.RED -> "红色"
        Color.YELLOW -> "黄色"
        Color.BLUE -> "蓝色"
    }
}

可以写成这样

fun getColor(color: Color) = when(color){
    Color.RED -> "A"
    Color.YELLO, Color.BLUE -> "B"
}

类型智能转换

fun getStringLength(obj: Any): Int? {
    if (obj is String) {
        // ‘obj’ 在该条件分支内自动转换成 ‘String’
        return obj.length
    }

    // 在离开类型检测分支后,‘obj’ 仍然是 ‘Any’ 类型
    return null
}

在 kotlin 中 is 和 Java 中的 instanceOf 类似,但是不需要强制转换,在用 is 检查过某种类型后,编译器会自动完成转换。

和 when 一起使用,when 不仅仅检查是否相等,而且可以检查类型。

fun  eval(e: Expr): Int =
    when(e){
        is Num -> e.value
        is Sum ->
            eval(e.right) + eval(e.left)
        else ->
            println("else")
    }

代码块可以作为 if 和 when 的分支,最后一个表达式就是结果

fun  eval(e: Expr): Int =
    when(e){
        is Num -> {
            println("test")
            println("hhhhh")
            e.value   //最后一个表达式
        }
        is Sum -> {
            println("hei")
            eval(e.right) + eval(e.left)
        } 
        else ->
            println("else")
    }

函数

创建集合

Kotlin 没有自己的集合类,而是采用 Java 标准的集合类

val list = arrayListOf(1, 2, 3)
val map = hashMapOf(1 to "one", 2 to "two")

函数参数

joinToString(collection, separator = " ", prefix = " ", postfix = ".")

kotlin 中可以显式的标明一些参数的名称,也可以设置参数的默认值

fun <T> joinToString(
    collection: Collection<T>, 
    separator: String = ", ",
    prefix: String = "",
    postfix: String = ""
): String

顶层函数和属性

在 Java 中函数都属于某个类,而在 Kotlin 中函数可以脱离类存在,这种函数叫做顶层函数。

在顶层函数中我们可以编写常用的工具方法,在 Kotlit 中不用使用工具类来包含对应的工具方法。

package com.test.util;

fun dp2px(val dp: Int): Int {

}

假设上面的文件名字叫 ConvertUtil.kt, Kotlin 会编译生成对应的 Java 类如下:

package com.test.util;

public class ConvertUtil{
    public static int dp2px(int dp){

    }
}

如果我们要改变生成的类的名字,需要添加 @JvmName 注释

@file:JvmName("TestUtil")

package com.test.util;

fun dp2px(val dp: Int): Int {

}

和函数一样,属性也可以放到文件的顶层

var redpointCount = 0

如果要定义常量可以使用 const 来修饰

const val TYPE_NEW = "happy new year";

扩展函数

扩展函数其实是定义在类外面的成员函数。

package strings

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

在扩展函数中可以直接访问被扩展类的其他方法和属性,但是不能访问私有的或者是受保护的成员。

循环

for循环

for 循环可以对任何提供迭代器(iterator)的对象进行遍历,这相当于像 C# 这样的语言中的 foreach 循环。

val items = listOf("apple", "banana", "kiwifruit")
for (item in items) {
    println(item)
}

如需在数字区间上迭代,请使用区间表达式:

for (i in 1..3) {
    println(i)
}
for (i in 6 downTo 0 step 2) {
    println(i)
}

如果你想要通过索引遍历一个数组或者一个 list,你可以这么做:

for (i in array.indices) {
    println(array[i])
}

或者你可以用库函数 withIndex:

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

遍历 map

for ((k, v) in map) {   //k、v 可以改成任意名字
    println("$k -> $v")
}

while循环

while 与 do..while 和 Java 使用类似

while (x > 0) {
    x--
}

do {
  val y = retrieveData()
} while (y != null) // y 在此处可见

循环中 kotlin 支持 break 和 continue, return 操作。

在 Kotlin 中任何表达式都可以用标签 label 来标记。 标签的格式为标识符后跟 @ 符号,例如:abc@fooBar@都是有效的标签。

loop@ for (i in 1..100) {
    for (j in 1..100) {
        if (……) break@loop
    }
}

对象和接口

接口

interface Clickable{
    fun click()
}

实现接口

class Button : Clickable{
    override fun click() = println("i was clicked !")
}

在 kotlin 中使用 overried 修饰符是强制要求的。

和 Java 一样,一个类可以实现多个接口,但只能继承自一个类。

抽象类

abstract class Clickable{
    abstract fun click()
}

继承类

class Button : Clickable() {
    override fun click() = println(" i was clicked ! ")
}

上面的继承为什么 Clickable() 有个小括号,这是因为抽象类有构造方法。

在kotlin中构造方法分为主构造方法(类体外部声明)和从构造方法(类体内部声明)。

class User(val nickname: String)

上面的 User 类有一个主构造方法

class User constructor(_nickname: String){
    val nickname: String

    init{
        nickname = _nickname
    }
}

class User constructor(_nickname: String){
    val nickname = _nickname
}

constructor 关键字用来表示一个从构造或者主构造方法的声明。 init 关键字引入一个初始化语句块。

open和final

Java 的类和方法默认是 open 的,而在 kotlin 中类和方法默认是 final 的,也就是说默认不具有被继承性。

open class RichButton : Clickable{
    open fun click()
    fun longclick()  //不可被重写
}

但是如果一个类被声明为 abstract 的(也就是上面说的抽象类),这个类相当于默认是 open 的(包括所有方法)

public protected 和 private

在 kotlin 中和 Java 类似,默认是 public 的。

修饰符类成员顶层声明
public所有地方可见所有地方可见
internal模块中可见模块中可见
protected子类中可见——
private类中可见文件中可见

内部类和嵌套类

分类在Java中在kotlin中
嵌套类(不持有外部类的引用)static class Aclass A
内部类(持有外部类的引用)class Ainner class A

在 kotlin 中要引用外部类的实例使用 this@Outer

class Outer{
    inner class Inner{
        fun getOuterReference(): Outer = this@Outer
    }
}

data修饰符

data class  Client(val name: String, val postalCode: Int)

如果需要一个数据容器(需要重写 toString、equals、hashCode),添加 data 修饰符会自动生成。

object关键字

object SingleInstance{

}

object 关键字类不具有构造方法,而且只有一个实例就是它本身。

伴生对象

在 kotlin 中类不能拥有静态成员,Java 的 static 关键字并不是 kotlin 的一部分。

class A{
    companion object{
        fun bar(){
            println(" ")
        }
    }
}

直接调用:A.bar()

使用 companion 关键字修饰 object 的嵌套类类似于 Java 中的静态方法和静态成员。

其他

获取javaClass

kotlin 中的 Class 与 Java 不同,kotlin 中有一个自己的 Class 叫做 KClass

person::classPerson::class 都是获取 kotlin 的 KClass,所以 println(person::class == Person::class) 为true。

我们可以从 kotlin 的 KClass 获取到 java 的 Class, person::class.java 就是如此,先获取到 kotlin 的 KClass 然后再获取 javaClass。

companion object {
    fun toMain(context: Context): Intent{
        return Intent(context, MainActivity::class.java)
    }
}