在2017年的Google IO开发者大会上,Google正式宣布了Kotlin成为Android官方编程语言。

准备工作

在IntelliJ IDEA 15和Android Studio 3.0之后的版本都已经内置了Kotlin插件,我们都知道 Android Sutdio是基于IntelliJ IDEA开发的,而IntelliJ IDEA又是由JetBrains所开发,而Kotlin同样是由JetBrains创造的,可以说Kotlin有着雄厚的背景。

  1. 但如果你是旧版本的IDEA或AS,就需要下载Kotlin插件,在设置中找到Plugins,搜索Kotlin并安装即可。

  2. 接着新建项目选择Kotliin/JVM

  3. 也可以直接在已有项目中新建Kotlin类


Hello World

一切都准备就绪,就可以开始我们的第一行Kotlin代码了。

一个简单的Hello World。

fun main(args: Array<String>) {
    println("Hello Kotlin")
}

OK,简单分析下。

Kotlin的语法和Java相比,非常简洁。特别要注意的是,在Kotlin中大括号最好不要换行,因为有时候在换行后某些语法糖的作用会导致语义不同。

同Java一样,Kotlin的程序入口也是main函数,但不必像Java那样声明一个类。Kotlin的函数是可以在顶层声明的。

Kotlin中每行语句后不必加分号,当然你要加也是可以的,非强制性。但如果在一行中有多条语句,那就得在每条语句后加分号。

函数的关键字是fun,变量定义是var <变量名> : <变量类型> : <初始化值>

Kotlin编译后和Java一样也会生成一个.class文件,但文件名是该.kt文件名+Kt,比如Hello.kt编译后生成的是HelloKt.class

接下来简单讲讲Kotlin的各种语法,和Java相似的我会直接跳过,主要讲不太一样的。


基本语法

变量

常量定义:val <变量名> : <变量类型> = <初始值>

变量定义:var <变量名> : <变量类型> = <初始值>

例:

fun main(args: Array<String>) {
    val name: String = "小明"
    var age = 18         //自动类型推断 可以省略变量类型
    var weight = 70.0
    var height: Double   //没有赋初始值就不能省略变量类型
    height = 1.75 as Double  //as是强制类型转换 这里只是为演示下才这么写 实际上根本没必要
    
    print("名字:$name\n")
    println("年龄:${age}岁\n" +
            "身高:${height}米\n" +
            "体重:${weight}千克")
    println("BMI指数:${weight / (height * height)}")
}

在字符串格式化时,可以使用传统的"xxx" + <变量名>的方式。但Kotlin显然有更好的方式,可以像上面一样直接用xxx$<变量名>来内插。
如果后面还紧接其他字符时用xxx${<变量名>}xxx给变量名加上大括号就可以正确识别了,如果紧接的是\n等转义字符也可以不用加,但为了编码规范和可读性建议还是加上。

上面这段代码的输出为:

名字:小明
年龄:18岁
身高:1.75米
体重:70.0千克
BMI指数:22.857142857142858

数组与集合

Kotlin中的集合分为可变和不可变,可变的名字前面多一个Mutable,直接上代码:

//数组 长度不可变 可写 不指定类型时元素类型可变
var array: Array<String> = arrayOf("hello", "world")
array[0] = "h"
print("Array -> ${array[0]}")

//List 有序线性数据结构 类型固定 相当于Java的ArrayList
//只读
var list: List<String> = listOf("hello", "world")
println("List -> ${list[0]}")

//可变List
var mlist: MutableList<String> = mutableListOf("hello")
mlist.add("world")
print("MutableList -> ")
for(i in mlist) {
    print(i)
}

//Map 无序可重复 键值对数据结构 即Hash表
//同样只读
var map: Map<String, Int> = mapOf("hello" to 1, "world" to 2)
println("-----Map-----")
//下面这两句作用一样
println( map.getValue("hello") )
println( map["hello"] )
//遍历
for((k,v) in map) {
    println("$k -> $v")
}

//同样还有MutableMap

//还有个Set是集合,无序不可重复,可以拿来去重。其他都一样,不多说了

输出(手动换了行以便区分):

Array -> h

List -> hello

MutableList -> helloworld

-----Map-----
1
1
hello -> 1
world -> 2

循环和条件

in

使用in关键字进行区间操作:

//x在区间[1,10]内则执行
if(x in 1..10) {
}

//x不在区间[1,10]内则执行
if(x !in 1..10) {
}

//迭代1到5 类似Java的foreach
for(i in 1..5) {
}

//downTo为倒序迭代 迭代4到1
for(i in 4 downTo 1) {
}

//使用setp设置步长 这里访问了1 3
for (i in 1..4 step 2) {
}

//使用until设置开区间 [1,10)
for(i in 1 until 10) {
}

//支持多个参数来遍历map
for((k, v) in map) {
}

如果想像Java的for一样通过索引遍历一个数组或者一个 List,可以使用indices

//使用array[i]访问
for(i in array.indices) {
}

if的其他形式

if的分支可以在代码块最后加上表达式,则代码块的值就为这个最后的表达式。也可以直接写成表达式形式:

//表达式形式
val max = if (a > b) a else b

//代码块最后的表达式将为代码块的值
val max = if(a > b) {
    print("Choose a")
    a
} else {
    print("Choose b")
    b
}

when

when就类似Java中的switch,不过显然更加强大,会自动进行类型推断:

when (x) {
    "hello"  -> print("x == hello")
    0,1      -> print("x == 0 || x == 1") //可以把多个分支放到一起 用逗号分隔
    in 1..10 -> print("x in [1,10]")      //同样可以用in判断区间
    is Long  -> print("x is Long")        //is是显式类型推断 这里意思为x是Long类型返回true 否则返回false
    else     -> print("")                 //else类似于Java中的default
}

when也可以用来取代 if-else if链。 如果不提供参数,所有的分支条件都是简单的布尔表达式,而当一个分支的条件为真时则执行该分支:

when {
    x > 10 -> print("x > 10")
    x < 5  -> print("x < 5")
    else   -> print("5 <= x <= 10")
}

标签

可以用给循环加上标签,在使用breakcontinue能方便地控制,在标签名后面加上@就可以定义标签:

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

标签限制的break跳转到刚好位于该标签指定的循环后面的执行点。continue继续标签指定的循环的下一次迭代。

标签也可用在子类调用指定父类方法时:super@<父类名>.<方法>()

函数

函数的声明方式:

fun <函数名>(<参数>): <返回类型> {
    return <返回值>
}

但函数不返回值时,返回类型为Unit。当然,更多时候是直接定义成fun <函数名>(<参数>) {}

可以将表达式作为函数体,进行简写,例:

fun add(a: Int, b: Int) = a + b

参数默认值

设定默认值后如果不传参,使用默认值。无默认值的参数如果在有默认值的参数之后,则必须使用命名参数的方式调用,后面会讲。

例:

fun say(str: String = "hello") = str

可变参数

在Java中我们用<参数类型>... <参数名>来定义可变参数,在Kotlin中,使用vararg来定义。:

fun say(vararg strArray: String) {
    for(str in strArray) {
        print(str)
    }
}

调用:

say("a", "b", "c")

输出:

abc

如果可变参数不是最后一个,则必须以命名参数的方式调用。

命名参数

即在调用函数时显式指定参数名,在参数多的情况下能令可读性更好:

fun test(str: String = "hi", i: Int, bool: Boolean) {
    print(str + i + bool)
}

调用:

test(i = 1, bool = true)

以命名参数形式调用具有可变参数的函数,需要在传入的参数前加*号,还是以上面那个say函数举例:

fun say(vararg strArray: String) {
    for(str in strArray) {
        print(str)
    }
}

调用:

say(strArray = *arrayOf("a", "b", "c"))  //arrayOf()方法作用是返回包含指定元素的数组

局部函数

即在函数内部定义函数,内部函数可访问外部函数的局部变量:

fun say(str: String) {
    fun hi() = print("hello,")

    hi()
    print(str)
}

调用:

say("kotlin")

输出:

hello,kotlin

调用方式

Kotlin中的类调用方式比较特殊,不需要new
比如有一个类:

class Person {
   fun sayHello() {
       print("hello")
   }
}

调用时只需要:

Person().sayHello()

构造函数

在 Kotlin 中的一个类可以有一个主构造函数和一个或多个次构造函数。主构造函数是类头的一部分,使用constructor关键字声明,它跟在类名(和可选的类型参数)后:

class Person constructor(name: String) {
}

如果主构造函数没有任何注解或者可见性修饰符,可以省略这个constructor关键字。主构造函数不能包含任何的代码。初始化的代码可以放到以init关键字作为前缀的**初始化块(initializer blocks)**中,init块可以有多个,按在类中的顺序执行:

class Person(name: String) {
    init {
        print("name is $name")
    }
    
    init {
        print("Second init")
    }
}

Kotlin可以有多个构造函数,只有主构造函数可以写在类头,次构造函数需写在类体里,同样使用constructor关键字声明,并且如果类有一个主构造函数(无论有无参数),每个次构造函数需要直接或间接委托给主构造函数,用this关键字:

class Person(name: String) {
    init {
        println("name is $name")
    }

    constructor(name: String, age: Int): this(name) {
        print("age is $age")
    }
}

调用:

Person("xiaoming")
Person("xiaoming", 18)

输出(中间手动换行以示区分):

name is xiaoming

name is xiaoming
age is 18

需要注意的是,init块中的代码实际上是主构造函数的一部分。也就是说,init块中可以使用主构造函数的参数,并且无论在类中的顺序如何,init块都会在次构造函数前执行。

属性

对于类中需要初始化的属性,可以定义在类名后面的括号里来简写:

//传统方式
class Person {
    val name: String
}

//属性简写 调用时必须传参来初始化
class Person(val name: String) {
}

//这个是构造函数的参数
class Person(name: String) {
}

在类名后的括号里,没有varval的就是构造函数参数,否则就是属性。

自定义GettersSetters用法:调用时只用像Java调用字段一样,即<类的实例>.<属性>,就会自动调用get()set()。在GettersSetters中通过field访问这个属性:

class Person() {
    var name: String = "xiaoming"
        get() {
            return "my name is $field"
        }
        set(value) {
            field = value
        }
}

静态方法

静态方法和属性定义在companion object { }代码块里:

class Test {

    companion object {
        fun staticFun() {
            print("静态方法")
        }
    }

}

调用:

Test.staticFun()

继承

在Kotlin中所有类都有一个共同的基类Any,就像Java中所有类的基类都是Object一样。

Kotlin会默认会为每个变量和方法添加final修饰符。为每个类加了final也就是说,在Kotlin中默认每个类都是不可被继承的。

这么做的原因是为了性能和设计考虑,参考《Effective Java》中第四章的第17条:要么为继承而设计,并提供文档说明,要么就禁止继承。

要想一个类能够被继承,需要添加open修饰符。要继承一个类,只要在类头后加上: <父类>

open class Person(name: String) {
}

class Man(name: String) : Person(name) {
}

如果子类没有主构造函数,那么每个次构造函数必须使用super关键字初始化其基类型,或委托给另一个构造函数做到这一点。 注意,在这种情况下,不同的次构造函数可以调用基类型的不同的构造函数。

同样,要允许一个方法或属性被重写,也必须加上open修饰符。

重写方法与属性

在Kotlin中,重写必须用override关键字显式指定。如果想要禁止这个方法再被子类重写,加上final修饰符:

open class Person() {
    open fun say() {
    }
}

open class Man() : Person() {
    final override fun say() {
    }
}

重写属性时可以以var覆盖val,但反之则不行,并且重写的属性可以移到主构造函数作为声明的一部分:

open class Person() {
    open val name: String = "xiaoming"
    open fun say() {
    }
}

open class Man(override var name: String) : Person() {
    final override fun say() {
    }
}

覆盖规则

如果一个类从它的直接父类继承了多个同名方法或属性,它必须覆盖这个方法并提供其自己的实现。用super<父类名>区分:

open class A {
    open fun f() { print("A") }
    fun a() { print("a") }
}

interface B {
    fun f() { print("B") } // 接口成员默认就是“open”的
    fun b() { print("b") }
}

class C() : A(), B {
    // 编译器要求覆盖 f():
    override fun f() {
        super<A>.f() // 调用 A.f()
        super<B>.f() // 调用 B.f()
  }
}

其他

可空值与空值检测

Kotlin是空指针安全的,不会再像Java一样天天碰到头疼的NullPointerException
Kotlin中每个类型的对象都分为可空非空,正常的对象都是非空的,也就是不能为null
如,下面这段代码就会编译错误:

var a: String     //不可空
a = null

可空对象必须显式指定,在类型后面加?

var b: String?    //可空
b = null

这时,b就为可空对象了,在调用该对象的一些方法时,由于可能为空,编译器为了防止空指针异常,会报编译错误:

var len = b.length

必须进行空值判断:

var len = if(b != null) b.length else -1

这显然太麻烦了,我们可以使用一个语法糖:

b?.length

如果b非空,就返回b?.length,否则返回null,这个表达式返回值的类型是Int?
这个语法糖还可用在链式调用上,环节中有任何一个为空,都会返回null

a?.b?.c?.d

之前那行代码就可以写成:

var len = b?.length ?: -1

?:操作符的作用是:左侧表达式非空就返回左侧表达式,否则返回右侧表达式。

还有别的一些使用方法,例:

b?.let {    //b不为空时执行代码块
}
b?:let {    //b为空时执行代码块
}

//也可以简写 例:
b?: print("b is null")

但还是觉得麻烦呢?可以使用!!操作符令编译器忽略空值校验:

var len = b!!.length

这段说了一堆,但总结下就四点:

  • 使用<对象类型>?来定义可空对象
  • 调用对象方法或属性时,使用<对象实例>?.<方法或属性>使对象为空就返回null
  • 使用?:操作符简化空值判断
  • 使用!!操作符令编译器忽略空值校验

Kotlin语法十分简洁,有编程基础花上半个小时就能快速入门了,但离接触Kotlin的精髓,还远得很。


Kotlin与Java互操作

直接转换

IDEA和Android Studio都提供了一键转换代码

Code - Convert Java File To Kotlin File可以将Java代码转换成Kotlin。

Tools - Kotlin - Show Kotlin Bytecode - Decompile也可以将Kotlin转回Java。

不过我都没用过,具体效果有待考证。

Kotlin与Java互相调用

事实上,在Kotlin中调用Java异常地简单,因为大部分Java代码、类和方法都可以直接运行在Kotlin上!反之亦然。

例如直接使用Java的ArrayList

import java.util.*

fun main(args : Array<String>) {
    val list = ArrayList<String>()
    list.add("a")
    list.add("b")
    list.add("c")
    for(str in list) {
        print(str)
    }
}

官方文档

参考 - Kotlin 语言中文站