Kotlin语法初探

本文参考资料

《kotlin实践》
《Kotlin中文文档》
《kotlin中国 - Kotlin 极简教程》
《zxc123e的CSDN博客》

引言

最近接手一个项目是用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语言更加简单。

函数编程核心概念

头等函数

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

不可变性

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

表达式函数体

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

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

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

可变变量和不可变变量

val : 不可变变量

var :可变变量

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

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

1
2
3
4
5
6
7
abstract class Person{
abstract val name : String
}

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

延迟初始化

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

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

1
2
3
4
5
6
7
8
9
10
11
public class MyTest {
lateinit var subject: TestSubject

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

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

字符串模板

1
2
3
4
5
6
7
var a = 1
// 模板中的简单名称:
val s1 = "a is $a"

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

类和属性

类的定义

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 关键字是必需的,并且这些修饰符在它前面:

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

自定义访问器

1
2
3
4
5
6
class Person(val name:String, var age:Int? = null){
val isMarried: Boolean
get() {
return age > 30
}
}

枚举和when

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

1
enum class Color {RED, YELLOW, BLUE}

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

1
2
3
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是一个有返回值的表达式

1
2
3
4
5
6
7
fun getColor(color: Color){
when(color){
Color.RED -> "红色"
Color.YELLOW -> "黄色"
Color.BLUE -> "蓝色"
}
}

可以写成这样

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

类型智能转换

1
2
3
4
5
6
7
8
9
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不仅仅检查是否相等,而且可以检查类型。

1
2
3
4
5
6
7
8
fun  eval(e: Expr): Int =
when(e){
is Num -> e.value
is Sum ->
eval(e.right) + eval(e.left)
else ->
println("else")
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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标准的集合类

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

函数参数

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

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

1
2
3
4
5
6
fun <T> joinToString(
collection: Collection<T>,
separator: String = ", ",
prefix: String = "",
postfix: String = ""
): String

顶层函数和属性

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

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

1
2
3
4
5
package com.test.util;

fun dp2px(val dp: Int): Int {

}

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

1
2
3
4
5
6
7
package com.test.util;

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

}
}

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

1
2
3
4
5
6
7
@file:JvmName("TestUtil")

package com.test.util;

fun dp2px(val dp: Int): Int {

}

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

1
var redpointCount = 0

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

1
const val TYPE_NEW = "happy new year";

扩展函数

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

1
2
3
package strings

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

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

循环

for循环

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

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

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

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

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

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

或者你可以用库函数 withIndex:

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

遍历map

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

while循环

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

1
2
3
4
5
6
7
while (x > 0) {
x--
}

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

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

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

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

对象和接口

接口

1
2
3
interface Clickable{
fun click()
}

实现接口

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

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

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

抽象类

1
2
3
abstract class Clickable{
abstract fun click()
}

继承类

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

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

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

1
class User(val nickname: String)

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

1
2
3
4
5
6
7
class User constructor(_nickname: String){
val nickname: String

init{
nickname = _nickname
}
}

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

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

open和final

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

1
2
3
4
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

1
2
3
4
5
class Outer{
inner class Inner{
fun getOuterReference(): Outer = this@Outer
}
}

data修饰符

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

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

object关键字

1
2
3
object SingleInstance{

}

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

伴生对象

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

1
2
3
4
5
6
7
8
9
class A{
companion object{
fun bar(){
println(" ")
}
}
}

直接调用:A.bar()

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

其他

获取javaClass

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

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

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

1
2
3
4
5
companion object {
fun toMain(context: Context): Intent{
return Intent(context, MainActivity::class.java)
}
}