kotlin 语法详解

-

你可以在这个网站中在线编译并运行kotlin代码


前言

  • kotlin是一门野心非常大的语言,它不但能与能与现有的java框架并存,向人证明它可以以更加简洁的方式代替java,还不断向着更多的领域发展。android开发者可以结合anko编写应用,永久告别xml,前端开发者可以编写kotlin代码然后编译成JavaScript,Kotlin_Native更是支持直接编译机器码在linux,ios,mac os上运行
  • kotlin最大的特点是简洁的语法,自动类型推断,互操作性,成吨语法糖
  • java中已有并且在kotlin中相同的特性不会拿出来讲


函数和变量


函数


基础

构成kotlin程序的基本要素是函数和变量,这即是某些语言以及函数式编程中强调的“函数是第一等公民”的概念的体现

fun main(args:Array<String>){
    println("hello world")
}
  • 上面是一个hello world的kotlin实现,从这个简单的例子中我们已经可以看到kotlin与java有很多不同
1.使用关键字 fun 声明一个函数,也就是方法
2.参数类型写在名称的后面,与go语言如出一辙,对于能够自动推断类型的语言来说,我们总是更关注变量的名名称所表达的抽象含义而不是它的类型
3.函数可以定义在文件的最外层,而不是类里面,你可以直接把这个函数放到任何一个kt文件中运行,不需要找到一个合适的花括号然后把它塞进去
4.println代替了system.out.println,kotlin简化了很多这样啰嗦的代码
5.可以省略代码结尾的分号,这点和python很像,你不用担心你的代码会被go编译器那样胡乱的添加上分号


主函数 main

  • 两种写法
fun main(){
//...
}
fun main(Array<String>){
//...
}


代码块体 {}

fun max(a: Int, b: Int): Int {
    return if (a > b) a else b
}
  • 函数以fun开头,然后是函数名和参数列表,参数类型写在参数名后,以冒号隔开,最后是冒号和返回类型
  • 注意在kotlin中大多数控制结果都是表达式,而不是语句,语句与表达式的最大区别在于表达式有值,允许直接嵌套使用


表达式体 =

fun max(a: Int, b: Int): Int = if (a > b) a else b
  • 表达式也可以作为一个完整的函数体,现在你知道为什么kotlin要把返回类型放在函数头的最后面了

  • 我们说过kotlin支持自动类型推断,因此上面的代码还可以简洁一下

fun max(a: Int, b: Int) = if (a \> b) a else b
  • 在函数中支持表达式体的意义在于,向kotlin这种支持闭包的语言可以在任何地方将代码重用的粒度降到最细


变量 var val


省略类型

val aString = "I am String"
val aInt = 1;
var aDouble = 2e6
var aFloat:Float
  • 在Java(jdk 10之前)中,声明变量时必须在最前面写出变量类型,在kotlin中,你甚至都不需要给出类型,编译器根据初始化的值可以知道你要的是什么,如果你不想那么快赋值,那就必须给出变量类型了
  • 上面我们用到了var和val来声明变量,var表示可变引用,val表示不可变引用(final),并且val仅仅保证引用的不可变性,它指向的对象仍然是可变的
val aList = arrayListOf("java")
aList.add("kotlin")
  • 你应该尽可能地使用val来声明所有的变量,这会让代码更安全,也更接近函数式编程的风格


字符串模板 ${}

val he = "Albert"
val languages = arrayListOf("java")
languages.add("kotlin")
println("$he said, ${languages[1]} is the best language in the world ")

//猜猜输出什么?
  • 字符串模板的作用是允许字符串字面量中存在变量和表达式,这显然让字符串拼接显得更加简洁紧凑,易于理解

  • 可以使用$中支持变量,使用${}支持表达式


类基础和属性

Java常为人诟病的一点就是创建一个类需要非常多的样板代码(特别是对于bean类而言),为此不少ide或插件都提供了自动生成getter,setter,构造器,代码模板,万能tab的功能

但就像很多人认为的那样,某种程度上Java逐渐变成了一门“依赖ide”的语言

kotlin的开发者们为了解决这些问题可谓下足了功夫


属性 field

class Person(var name:String)
  • 上面这行kotlin代码展开成Java代码之后如下,你一定见过很多这样的Java代码,尽管从语法的角度来讲它们处处都有意义,但实际上你需要的关键词真的只有 class,Person,var,name ,String,这就是kotlin所谓的简洁,在kotlin当中,类通常很小
public class Person{
    private String name;

    public Person(String name){
        this.name = name;
    }

    public String getName(){
        return name;
    }

    public void setName(String name){
        this.name = name;
    }
}
  • 对于使用var修饰的属性,kotlin编译器会自动为其生成getter,setter,对于使用val修饰的属性,编译器则只生成getter,同时在访问和修改任何属性时,你不需要显式地调用它们的访问方法,对于构造方法而言,你也不用再写关键字new了
val albert = Person("Albert")
albert.name = "Albert Lin"
println(albert.name)
  • 所有属性都像Java中访问公有属性那样访问,但实际上走的是内部的getter和setter方法,当然这也包括了你所编写的getter和setter方法
class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean
        get() {
            return height == width
        }

    //也可以简化为val isSquare: Boolean get() = height == width
}
  • 对于val属性,只允许有get方法而不允许设置set方法,同时val属性本身不允许修改。然而由于get方法本身并非需要与关联的属性有实际关系,所以get方法返回值不一定是固定的
class Rectangle(var height:Int, var width:Int){
    val isSquare: Boolean
        get() {
            return width == height
        }
}
fun main() {
    val p = Rectangle(1,1)
    assert(p.isSquare)
    p.width = 2
    assert(!p.isSquare)
}
  • 对于var属性,同时支持get和set方法,var属性的需要设置初始值,可以借助构造函数提供的属性。还要注意不管是get还是set方法,需要访问关联的属性时最好都使用关键字field,而不是变量名,否则容易因为自动调用的get和set方法造成循环递归导致栈溢出
class Rectangle(var height: Int, var width: Int) {
    var isSquare: Boolean = width == height && width * height != 0
        get() {
            return width == height
        }

    set(value){
        field = value
    }
}
  • 具体来说,原因是在get和set方法中,get和set的自动调用依然是开启的,所以容易出现以下情况。注意有的情况lint无法检测出来,同时只有运行时访问到该属性时才会抛出StackOverflowError。本来get和set方法中就不应该有过多复杂的操作,尽量使用fiel关键字,谨慎调用其他方法即可
//无限递归get,ide有提示  
var isSquare: Boolean = false
        get() {
            if (isSquare) return true
            return false
        }
//无限递归set,ide有提示
var isSquare: Boolean = false
        set(value) {
            isSquare = value
        }
//get与set循环造成栈溢出,ide无提示
var isSquare: Boolean =false
        get() {
            isSquare = false
            return field
        }

            set(value){
            if(isSquare){
                field = true
            }else{
                field = value
            }
        }
//get与普通方法循环造成栈溢出,ide无提示
    var isSquare: Boolean =false
        get() {
            test()
            isSquare = false
            return field
        }

    private fun test(){
        var a = isSquare
    }
  • 如果需要限制访问权限,可以使用在get和set方法前使用修饰符,注意get的修饰符需要与属性前的修饰符一直,而set的修饰符访问权限需要小于或等于属性前的修饰符
var isSquare: Boolean = width == height && width * height != 0
    get() {
        return width == height
    }
 private set(value) {
        field = value
    }


包 import package

  • kotlin当中依然使用import和package关键字来组织代码结构,不同的是,在Java中,包结构需要与实际上的文件目录结构一致,kotlin则不做这种要求,你可以把所有文件都放到一个目录,只在package关键字后面声明它们所在的包
  • 在通常情况下,程序员依然应该使用Java那样去管理文件,但是对于那些短小的类而言,你应该按照具体职责将它们放到同一个文件中
package  com.linjiamin.kt.play
class Rectangle(val height: Int, val width: Int) {
    val isSquare: Boolean get() = height == width
}
class Person(var name:String)


枚举 enum

  • 在kotlin当中,声明一个枚举类,需要 enum class 两个关键字
enum class Color{
    RED,ORANGE,YELLOW,GREEN,BLUE,INDIGO,VIOLET
}
  • 更常规的用法,为其添加构造方法和其他特定的方法,注意枚举列表必须与其他方法用分号隔开,这是kotlin中唯一一个必须使用分号结尾的地方。虽然使用val属性可以让其看起来更像枚举,但var属性也是可以的,这里和java没什么区别
enum class Color(val r: Int, val g: Int, val b: Int) {
    RED(255, 9, 9), ORANGE(255, 165, 0),
    GREEN(0, 255, 0), YELLOW(255, 255, 0),
    BLUE(0, 0, 255), INDIGO(75, 0, 130), VIOLET(238, 130, 238);

    fun rgb() = (r * 256 + g) * 256 + b
}
fun main(args:Array<String>){
    println(Color.BLUE.rgb())
}


条件和分支


分支 when

  • when 是 switch 的高级替代品
  • when 不但是分支控制语句,还是一个表达式,因此它具有返回值,并且不需要使用break语句,同时这个例子也体现了kotlin可以在函数中声明函数
fun main(args: Array<String>) {
    fun getColorName(color: Color) =
            when (color) {
                Color.RED -> "Red"
                Color.ORANGE -> "Orange"
                Color.GREEN -> "Green"
                Color.BLUE -> "Blue"
                Color.YELLOW -> "Yellow"
                Color.INDIGO -> "Indigo"
                Color.VIOLET -> "Violet"
            }

    println(getColorName(Color.BLUE))
}
  • 你可以使用列表的形式合并分支减少重复代码,使用import语句可以导入枚举类的所有常量,不再需要使用类名来引用
import com.linjiamin.kt.play.Color.\*
fun main(args: Array<String>) {
    fun getWarmth(color: Color) =
            when (color) {
                RED,
                ORANGE,
                YELLOW -> "warm"

                BLUE,
                INDIGO,
                VIOLET -> "cold"

                GREEN -> "neutral"
            }

    println(getWarmth(Color.ORANGE))
}
  • 在java 12(预览版,需使用参数开启)当中已经在switch中提供了上面这两个例子中一模一样的特性,下面为java暂时不会提供的特性,估计以后也不太可能提供这样的语法糖


when 支持任意对象

  • 在java中,case标签只支持枚举常量,字符串和数字字面量,而在 kotlin中,when支持所有对象,在下面这个例子当中,我们传入两个Color并使用setOf函数将他们合成为一个set对象,编译器能够推断出这种set共有多少种可能的情况,但注意当前版本中下面的else不是必须的,也就是说不必列出所有可能的情况
fun mix(c1: Color, c2: Color) =
        when (setOf(c1, c2)) {
            setOf(RED, YELLOW) -> ORANGE
            setOf(YELLOW, BLUE) -> GREEN
            setOf(BLUE, VIOLET) -> INDIGO
            else -> throw Exception("Dirty Color")
        }
fun main(args: Array<String>) {
    println(mix(RED,YELLOW))
}


when 不带参数

  • 在 when 当中 构造 setOf 会带来性能上的损耗,使用不带参数的when可以解决这种问题,但是可读性会下降,else同样不是必须的
fun mixOptimized(c1: Color, c2: Color) = when {
    (c1 == RED && c2 == YELLOW) || (c1 == YELLOW && c2 == RED) ->
        ORANGE

    (c1 == YELLOW && c2 == BLUE) || (c1 == BLUE && c2 == YELLOW) ->
        GREEN

    (c1 == BLUE && c2 == VIOLET) || (c1 == VIOLET && c2 == BLUE) ->
        INDIGO

    else -> throw Exception("dirty color")
}
fun main(args: Array<String>) {
    println(mixOptimized(RED, YELLOW))
}


类型检查 is

  • 使用 is 关键字可以进行类型检查和自动转换类型,类似java中的instanceOf,但 is 显然更加简洁
class Dog {
    fun bark() {
        println("bark")
    }
}
class Duck {
    fun swim() {
        println("swim")
    }
}
fun test(animal: Any) = when (animal) {
    is Dog -> {
        print("I am a dog,I can ")
        animal.bark()
        "wan !"
    }

    is Duck -> {
        print("I am a duck,I can ")
        animal.swim()
        "ga !"
    }
    else -> throw IllegalArgumentException()
}
fun main(args: Array<String>) {
    println(test(Dog()))
    println(test(Duck()))
}
//输出
//I am a dog,I can bark
//wan !
//I am a duck,I can swim
//ga !
  • 注意在kotlin中,分支可以是代码块,这个时候代码块中的最后一个表达式会作为when或其他表达式的值,实际上在局部定义域中可以放入下面这样的代码,除了最后一行有可能作为表达式值利用以外,其他都是无意义的
1
1.0
"1"
true


迭代和区间


while

  • 在 kotlin 中的 while 和 do while 结构和 java 没有任何区别,这里就不讲解了


区间 in

  • 使用 in 可以使用在for中指示一个区间,在kotlin中区间是包含的,或者说闭合的
fun main(args: Array<String>) {
    for(i in 1..100){
        print(" $i")
    }
}
  • 上面的操作没什么新意,看看下面这几行,kotlin 中的迭代区间不仅支持数字还支持字符,在区间可以使用downto指定方向,使用step指定步长
fun main(args: Array<String>) {
    for(c in 'A'..'Z' step 3 )
        print("$c")
}
fun main(args: Array<String>) {
    for(i in 100 downTo 1 step 3 )
        print("$i")
}
  • 更多时候我们需要的是半闭合的区间,这种时候可以使用 until 关键字,下面这个例子中100不会被输出
fun main(args: Array<String>) {
    for(i in 0 until 100 )
        print(" $i")
}


map 迭代

  • for 循环允许展开迭代中的集合的元素,例如下面这个例子中将map的键值对存储到两个独立的变量当中,在这个例子中集合看起来更像数组,是的,kotlin中可以使用中括号来代替get和put方法
val binReps = TreeMap<Char,String>()
fun test() {
    for(c in 'a' .. 'f'){
        val bin = Integer.toBinaryString(c.toInt())
        binReps[c] = bin
    }
    for ((letter,binary) in binReps){
        println("$letter = $binary")
    }
}
val list = arrayListOf("1","2","3")
fun test() {
    for ((index,element) in list.withIndex()){
            println("$index = $element")
    }
}


in 检查区间

  • 使用in和!in可以检查区间中的成员,即检查某个值是否在区间当中
fun isLetter(c: Char) = c in 'a'..'z' || c in 'A'..'z'
fun isNotDigit(c: Char) = c !in '0'..'9'
fun main(args: Array<String>) {
    print(isLetter('c'))
    print(isNotDigit('c'))
}


异常

  • kotlin中异常与java相似,不过不必使用new关键字来创建一个异常的实例,同时throw结构是一个表达式
val percentage = if (number in 0..100)
    number
else throw IllegalArgumentException("a percentage value must be between 0 and 100:$number")


try catch finally

  • kotlin不区分受检异常和未受检异常,因此不需要使用throw关键字来声明方法可能抛出的异常
fun readNumber(reader: BufferedReader) :Int? {
    try {
        val line = reader.readLine()
        return Integer.parseInt(line)

    }catch (e:NumberFormatException){
        return null
    }finally {
        reader.close()
    }
}


try 表达式

  • 在kotlin中try和when以及if一样,引入了表达式,如果语句中包括多个表达式,那么最后一个表达式的值就是try表达式的值,如果中间抛出了异常,那么catch表达式的值就是最终的值
fun readNumber(reader: BufferedReader) {
    val number = try {
        Integer.parseInt(reader.readLine())
    }catch (e:NumberFormatException){
        null
    }
    print(number)


集合

  • 使用 xxxOf 可以直接创建集合实例,对于map这种基于键值对的集合,使用to关键字
var arrayList = arrayListOf(1, 2, 3)
var hashSet = hashSetOf(1, 2, 3)
var hashmap = hashMapOf(1 to "one", 2 to "two", 3 to "3")

var list = listOf(1,2,3)
var set = setOf(1,2,3)
var map = mapOf(1 to "one", 2 to "two", 3 to "3")
  • kotlin没有使用自己的集合类,而是直接使用了java的集合类,比如arrayList,hashSet这种指定了明确类型的就对应到指定的类,list,set,map则对应到,arraylist,linkedHashSet,linkedHashMap
println(list.javaClass)
  • 除了java集合中本来提供的方法外,kotlin还添加了许多很方便的方法以供调用
println(list.first())
println(list.last())
println(list.max())
println(list.asReversed())


函数参数


命名参数

  • 使用 参数名 = 参数值 可以显式指定参数名称,让代码可读性更强,一旦一个参数使用了命名,随后的参数都需要命名,开始命名之后,参数的位置就不重要了,不需要按函数声明的顺序一样传入
fun main() {
    println(joinToString(
            listOf(1, 2, 3)
            , separator = ","
            , postfix = "]"
            , prefix = "["
        )
    )
}
fun <T> joinToString(
    collection: Collection<T>,
    separator: String,
    prefix: String,
    postfix: String
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}


默认参数

  • 使用 xxx:xxx = 默认值 可以指定参数的默认值,某参数指定默认值之后就可以不传入其值了
fun main() {
    println(joinToString(listOf(1, 2, 3)))
}
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ",",
    prefix: String = "[",
    postfix: String = "]"
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
  • 但是引入默认值之后时的函数的动态绑定会更加复杂,比如再上面的例子中我们再加入名称相同的函数,但添加一个具有默认值的参数,这个时候如果不知道内部调用规则我们无法判断哪个函数会被执行。实际上简单的函数的优先级更高,所以下面这个函数不会被调用
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ",",
    prefix: String = "[",
    postfix: String = "]",
    test: Boolean = false
): String {
    return ""
}
  • 假如现在我们给原来的 joinToString 函数也添加一个带默认值的参数,这个时候编译会不通过,因为无法判断需要调用那个函数,但值得注意的是如果这个时候没有任何地方调用了这(两)个函数,那么编译是不会报错的,因为没有地方需要进行静态类型检查
fun main() {
    println(joinToString(listOf(1, 2, 3),prefix = "["))
}
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ",",
    prefix: String,
    postfix: String = "]",
    test:Int = 0
): String {
    val result = StringBuilder(prefix)
    for ((index, element) in collection.withIndex()) {
        if (index > 0) result.append(separator)
        result.append(element)
    }
    result.append(postfix)
    return result.toString()
}
fun <T> joinToString(
    collection: Collection<T>,
    separator: String = ",",
    prefix: String = "[",
    test: Boolean = false,
    postfix: String = "]"
): String {
    return ""
}


可变参数

  • kotlin提供java类似的可变参数,但是使用起来比java更具有弹性。使用可变参数,需要指定关键字vararg,可变参数既可以传入不定个数量的指定类型参数,也可以传入数组,传入数组时需要使用展开运算符*,展开运算符的意义是将数组中的对象展开为参数列表,因此数组的前后依然可以添加参数,比java方便
fun test(vararg values:Int){
}
fun main(){
    test(1,2,3)

    var array = intArrayOf(1,2,3)
    test(0,*array,4)
}


扩展功能


包扩展

  • 给已有的包提供扩展的函数,在一个独立的文件中将package指定为需要扩展的包即可,这时候函数不属于任何类,在需要调用时直接import即可,比起java中常见的util类,由于不用使用类名,相对简洁,同时也减少了重复功能的函数,因为这样的函数相对容易查找。
package strings
fun <T> joinToString ...
import strings.\*
//或者
import strings.joinToString
fun test() {
    joinToString()
}
  • 包扩展函数的实现原理是生成将函数编译为带对应静态方法的java类,类名默认为首字母大写的文件名+Kt,也可以使用 @file:JvmName(\“类名\”)进行自定义
//in join.kt  
JoinKt.joinToString();
  • 同样可以给已有的包提供扩展的属性,可以指定为val或var,get set特性的在这里依然支持,如果需要指定常量(java中的public static final)可以使用const val,const val不支持get set特性
package strings
var test:Int = 0
get() {
    return field
}


类扩展

  • 给已有的类提供扩展的函数,只是在普通的函数声明中的名称之前加上 类名. 即可,在方法体中使用this来访问对象,但依然受访问权限的限制,比如可能无法访问prviate或protected成员和函数
fun String.lastChar():Char = this.get(this.length-1)
//...
fun main(){
    println("a".lastChar())
}
  • 扩展函数和成员函数之间都可以互相调用
fun AAA.aTest(){
}
fun AAA.BTest(){
    this.aTest()
}
  • 类的扩展函数同样被编译为静态函数,只是第一个参数是类的实例,其他规则和包扩展函数一致,这里不重复
  • 同样也可以对属性进行扩展,由于并不能实际给类添加属性,因此内部并没有实际的空间来存储这个属性的值,所以扩展属性不能初始化,也不能使用field关键字。kotlin实战中的例子不是很好,书中说不可变类型不能使用var,这非常容易让人误解,实际上不管什么类型,从语法上都是可以使用var和val的,只是对于不可变类型,一般没有可以修改属性的方法,同时内部属性通常为final,所以常规实现中我们在set函数中没有途径修改对象的属性,对于可变类型,就没有这种约束,我们可以修改它的属性。以上只针对不借用其他对象来手动存储的情况。
//不可变类
//val.
val String.lastChar: Char
get() = get(length-1)
//var
var String.lastChar: Char
get() = get(length-1)
set(value:Char){
//nothing we can do here
}
fun main(){
    println("a".lastChar)
}
//可变类
var StringBuilder.lastChar: Char
get() = get(length-1)
set(value:Char){
    this.setCharAt(length-1,value)
}


局部函数

  • 相比java,kotlin支持真正的闭包,在函数当中可以定义局部函数,局部函数可以调用外部函数中的局部变量,局部函数在扩展函数,get set函数中都适用
fun checkUser(user:User) {
    val throwEx = true
    fun checkEmpty(str:String){
        if(str.isEmpty()){
           if(throwEx)throw IllegalArgumentException()
        }
    }
    checkEmpty(user.name)
    checkEmpty(user.address)
}


中缀和解构

  • infix:我们在使用map时遇到过to来构造键值对,实际上to不是一个关键字,而是一个特殊的函数,只不过需要使用中缀调用,中缀函数和类扩展函数的声明类似,只是需要在前面加入infix关键字
fun main() {
    val array = 1 fillTo 100
    println(array[2])
}
infix fun Int.fillTo(other: Int): Array<Int> {
    val result = Array(init = {0}, size = other - this)

    for (i in this until other) {
        result[i - this] = i
    }
    return result
}
  • to函数的实现如下
infix fun Any.to(other: Int) = Pair(this,other)
  • 对于pair和其他key value的地方我们可以使用类似js中的解构声明来创建类实例
val(number,name) = 1 to "one"


字符串


正则扩展

  • split:string 支持和java相同的正则功能,不过对于常用的split函数,kotlin更加人性化,类似 . | 等在正则中使用的符号会被当做普通的字符来使用,同时支持可变参数
println("a.b.c-d|e".split(".","-","|"))
  • Regex:相对的如果想要使用正则字符串,则可以通过string的扩展函数toRegex,转化为正则对应的类
println("a.b.c-d-e".split("\\.|-".toRegex()))


三重引号

  • 三重引号可以用于避免转义字符,并且三重字符串中包括了缩进,可以包括换行
var str =
    """
    .
    ""
    \AAAA
    [BBBBB
    ////
    ///
    //
    /
    /
    ""
  • trimMargin:string还提供了一个很方便的trimMargin扩展函数,可以将三重字符串中的字符用空格和特殊字符串将整体内容移动到合适的位置,在需要使用的时候调用该函数进行还原
var str =
    """
    |.
    |""
    |\AAAA
    |[BBBBB
    |////
    |///
    |//
    |/
    |/
    """

println(str.trimMargin("|"))


类和接口

kotlin的类和接口和java中的类和接口具有一定的区别,比如接口可以包含属性,嵌套类默认不是内部类,类同时提供默认了


接口

  • kotlin和java中接口类似,只是用冒号替代了implements和extends关键字,同时提供了override关键字,override关键字是强制要求的,如果缺少将不能通过编译
interface  Clickable{
    fun click()
}
class Button:Clickable{
    override fun click() = println("click")
}
  • 接口中支持默认函数,不同于java8的默认方法,它不需要default关键字,注意不管是在kotlin还是在java当中,如果类或接口相关联的多个接口中有相同的函数(方法),则该类或接口必须对该函数(方法)进行重写,否则编译不通过
interface  Clickable{
    fun click() = println("default")
}


继承

  • open & final:kotlin使用open和final关键字来表面类是否允许继承以及函数是否允许重写,若不声明则默认为final,这是基于如果没有为继承做好准备就不应该提供继承功能这种思想而设计的,子类默认还是final的,但子类中重写子父类的函数默认是open的,除非显式指定为final
open class Button: Clickable{
    override fun click(){}
    open fun disable(){}
}
class SubButton: Button() {
    override fun disable(){}
}
  • abstract:kotlin支持和java类似的abstract关键字,没有需要特别注意的地方
abstract class Button: Clickable{ 
    override fun click(){}
    abstract fun animate()
}


访问修饰符

  • protected:kotlin支持 public protected private 修饰符,作用与java中的类似,需要关注的是默认的修饰符是public而不是protected,并且protected的行为与java有差异,protected只提供类自身和子类范围的可见性而不提供包级别的可见性,同时限制于之前提到的protected的实现机制,扩展函数也不允许访问private和protected属性
open class Button : Clickable {
     protected val aProtectedField: Int = 0
}
class Test {
    fun test() {
        var obj = Button()
        //报错
        obj.aProtectedField
    }
}
  • internal:另外kotlin还支持internal关键字,用于将类型限制在模块范围内,类似dart提供的特性,模块也就是idea或android studio中的module,本质是一组一起编译的文件,可以在函数或类级别上使用internal关键字
internal class ClassInAnotherMoudle{
    internal fun test(){
        println()
    }
}


内部类 inner

  • inner:kotlin支持类似java中的内部类和静态内部类,但kotlin中并没有static关键字,因为在kotlin中定义在一个class中的class默认是嵌套类(类比java中的静态内部类),如果需要指定为内部类则需要使用inner关键字,嵌套类不会持有外部类的引用,因此更加有利于实现序列化和反序列化
class Button : Clickable {
    var aField: Int = 0

    inner class Test {

        fun test{ aField = 1 }

        fun getOuter():Button = this@Button
    }
}
  • this@:在内部类获取外部类的引用不是使用java中的 类型.this 而是使用 this@ 类型


封装类 sealed

  • 在使用when控制分支时,如果使用了when的返回值,则需要覆盖所有分支情况,因此通常需要使用else关键字处理省缺的情况,这样在软件迭代过程中很出现漏判的情况,kotlin提供了解决这种问题的方法
interface Expr
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
fun eval(e: Expr): Int = when (e) {
    is Num -> e.value
    is Sum -> eval(e.right) + eval(e.left)
    else -> throw IllegalArgumentException()
}
  • sealed:使用封装类将需要的类型全部定义为嵌套类,则不需要指定所有分支,但注意前提条件是指定为sealed的类型没有其嵌套类以外的子类,sealed可以使添加新类型之后,未经过处理的when语句报错,而不是像上一段代码一样正常通过。注意sealed修饰的类默认是open的
sealed class Expr{
class Num(val value: Int) : Expr()
class Sum(val left: Expr, val right: Expr) : Expr()
}
fun eval(e: Expr): Int = when (e) {
    is Expr.Num -> e.value
    is Expr.Sum -> eval(e.right) + eval(e.left)
}


主构造函数

  • 在kotlin中可以使用非常简短的代码生成一个构造完整的类,下面的代码其实是主构造函数的简略形式,所有类都包含一个主构造函数
class User(val nickname:String)
  • constructor & init:上面的代码展开之后其实是这样子的,constructor用于声明一个构造函数,当用在类声明的行中说明是主构造函数,当用在类内部时则是从构造函数。init关键字类似java中的初始化块,因为在class的代码块体内只能做属性的声明和初始化语句,以及函数的声明和定义,而init闭包中可以有普通的逻辑代码,可以处理复杂的初始化情况,当然对于简单的情况则完全可以省略
class User constructor(nickname:String){
    val nickname:String 

    init {
        this.nickname = nickname
    }
}
//等价于
class User constructor(nickname:String){
    val nickname:String = nickname
}
//等价于
class User constructor(val nickname: String)
//等价于
class User (val nickname: String)
  • 和普通函数一样,构造函数也支持使用默认参数
class User (val nickname: String = "")
  • 如果子类的主构造函数需要调用父类的主构造函数,则可以直接在父类声明之后调用,如果子类没有提供任何构造函数或只声明了主构造函数,则需要显示调用父类的构造函数,当一个基类没有声明任何构造函数,编译器将会自动生成一个没有参数的构造函数,即()
open class Person (val nickname: String){
    //...
}
class User (nickname:String): Person(nickname)
  • 如果希望类不能被外部实例化(用于静态工具或单例)则可以使用private关键字
class User private constructor() : Person()
//报错
val usr = User()


从构造函数

  • 为了给一个类提供更多的构造函数,可以使用从构造函数,不像主构造函数,从构造函数可以定义多个,需要调用同一类的其他构造函数可以使用this关键字,同时init关键字依然可以使用
class User{
    init{
        //...
    }

    constructor(aInt:Int){
        //...
    }

    constructor(aInt:Int,aStr:String):this(aInt,aStr,false){
        //...
    }

    constructor(aInt:Int,aString:String,aBoolean:Boolean){
        //...
    }
}
  • 如果一个类具有主构造函数,则从构造函数必须调用主构造函数
class User(){
    constructor(aInt:Int) : this() {
        //...
    }
}
  • 另外一个特殊的限制是,如果父类没有无参的构造函数,则子类的从构造函数必须调用super或者this,这里的无参构造函数即可以是父类的主构造函数又可以是从构造函数
class NewUser: User {
    constructor(aInt:Int,aBoolean:Boolean) : super(aInt) {

        //...
    }
}


接口属性

  • kotlin的接口中允许声明属性,但这种属性声明是抽象的,需要由实现类来实现,实现的方法由三种,通过主构造函数,通过属性覆盖,通过get函数,注意其中get函数的实现,不会为属性创建空间
interface User{
    val nickname:String
}
class User1(override var nickname:String):User
class User3 :User{
    override val nickname = ""
}
class User2 :User{
    override val nickname:String
    get() = ""
}


object 函数

  • 在kotlin中所有类的根类型都是Any,Any类只提供了和java中object类相似的几个方法,包括 equals,hashcode,toString,any类如下
package kotlin
/\*\*
 * The root of the Kotlin class hierarchy. Every Kotlin class has [Any] as a superclass.
 \*/
public open class Any {
    /**
     * Indicates whether some other object is "equal to" this one. Implementations must fulfil the following
     * requirements:
     *
     * * Reflexive: for any non-null value `x`, `x.equals(x)` should return true.
     * * Symmetric: for any non-null values `x` and `y`, `x.equals(y)` should return true if and only if `y.equals(x)` returns true.
     * * Transitive:  for any non-null values `x`, `y`, and `z`, if `x.equals(y)` returns true and `y.equals(z)` returns true, then `x.equals(z)` should return true.
     * * Consistent:  for any non-null values `x` and `y`, multiple invocations of `x.equals(y)` consistently return true or consistently return false, provided no information used in `equals` comparisons on the objects is modified.
     * * Never equal to null: for any non-null value `x`, `x.equals(null)` should return false.
     *
     * Read more about [equality](https://kotlinlang.org/docs/reference/equality.html) in Kotlin.
     */
    public open operator fun equals(other: Any?): Boolean

    /**
     * Returns a hash code value for the object.  The general contract of `hashCode` is:
     *
     * * Whenever it is invoked on the same object more than once, the `hashCode` method must consistently return the same integer, provided no information used in `equals` comparisons on the object is modified.
     * * If two objects are equal according to the `equals()` method, then calling the `hashCode` method on each of the two objects must produce the same integer result.
     */
    public open fun hashCode(): Int

    /**
     * Returns a string representation of the object.
     */
    public open fun toString(): String
}
  • any类的方法都支持重写,同时在java中的经验同样也适用于kotlin
class User(val name:String){
    override fun toString():String = name
}
val a = "" + User("a")
  • 特别需要注意的是,equals和 == 在 kotlin当中是等价的,equals返回true则 == 表达式的值也是 true
fun main() {
    println(User("") == User(""))
}
class User(val name:String){
    override fun equals(other: Any?): Boolean {
        if(other == null || other !is User){
            return false
        }
        return this.name.equals(other.name)
    }
}


数据类 data

  • data:java的idea通常都提供快捷生成toString,equals方法的功能,但在kotlin当中则直接提供了语言层面上的支持,使用data关键字声明为数据类即可,数据类非常适合用于我们java中的bean(pojo)类
fun main() {
    println(Client("",1).toString())
    println(Client("",1).hashCode())
    println(Client("",1) == Client("",1))
}
data class Client(val name:String,val postalCode:Int)


类委托 by

  • 如果不依赖语言特性,在kotlin使用装饰器模式是想下面这个样子的,非常繁琐
class DelegatingCollection<T> : Collection<T> {
    private val innerList = arrayListOf<T>()

    override val size: Int get() = innerList.size
    override fun contains(element: T) = innerList.contains(element)
    override fun containsAll(elements: Collection<T>) = innerList.containsAll(elements)
    override fun isEmpty() = innerList.isEmpty()
    override fun iterator() = innerList.iterator()
}
  • by:kotlin为委托提供了语言层面上的支持,需要在主构造函数中声明被委托的实例,然后使用by关键字,委托类中的函数会自动生成,如有需要可以自己重写
class DelegatingCollection<T> (
    innerList:Collection<T> = ArrayList()
) : Collection<T> by innerList{
    override fun isEmpty(): Boolean {
        // ...
    }
}


对象类声明 object

  • object:object关键字用于在声明类的同时创建唯一实例,原本在class内部中的语法都适用,在顶层object声明对象可以像其他顶层对象一样访问
fun main() {
    Singleleton.meth()
    Singleleton.aInt = 1
}
object Singleleton {
    var aInt: Int = 1

    fun meth(){
        println(aInt)
    }
}
  • object声明也支持嵌套类,访问时需要加上类名
fun main() {
    val persons = listOf(Person("bob"),Person("Alice"))
    persons.sortedWith(Person.NameComparator)
}
data class Person(val name:String){
    object NameComparator : Comparator<Person>{
        override fun compare(o1: Person?, o2: Person?) 
                = o1.name.compareTo(o2.name)
    }
}


伴生对象 companion

  • 引用嵌套类对象声明,看起来像在引用一个类的静态成员,但是kotlin中类并不能拥有静态成员,实际上kotlin也没有static关键字,但可以使用伴生对象实现类似的语法功能
fun main() {
    Person.fromJson("{ ... }")
}

class Person(val name:String){
    companion object{
        fun fromJson(jsonText:String) : Person = 
    }
}
  • 伴生对象可以有可选的命名,默认命名为Companion,调用时命名也是可选的
class Person(val name:String){
    companion object Loader{
        var aInt:Int = 1
        fun fromJson(jsonText:String) : Person = Person("")
    }
}
  • 伴生对象可以有父类或实现接口,同时可以使用其命名添加扩展函数
class Person(val name:String){
    companion object: Test() {
        var aInt:Int = 1
        fun fromJson(jsonText:String) : Person = Person("")
    }
}
fun Person.Companion.meth(){
}
  • object还可以用于声明匿名内部类,与java不同函数中的局部变量可以被匿名内部类访问和修改,不需要被final限制
window.addMouseListener(object : MouseAdapter(){
    override fun mouseClicked(e: MouseEvent?) {

    }

    override fun mouseEntered(e: MouseEvent?) {

    }
})

var count = 0
val listener = object : MouseAdapter(){
    override fun mouseClicked(e: MouseEvent?) {
        count ++
    }

    override fun mouseEntered(e: MouseEvent?) {

    }
}

window.addMouseListener(listener)


lambda和序列


lambda

  • lambda表达式一般由 花括号 参数声明 箭头 一般表达式 组成,在kotlin当中lambda表达式可以作为变量存储和传递
fun main() {
    var lambda = { x:Int, y:Int -> x + y}
}
  • 调用值:lambda表达式可以使用括号调用,也可以使用run函数调用,后者只支持无参的lambda表达式,当lambda表达式被调用时,花括号中最后一个表达式的值就是整个lambda表达式的值
fun main() {
    val result = { x:Int, y:Int -> x + y}(1,1)
    //或
    val lambda = { x:Int, y:Int -> x + y}
    lambda(1,1)

    run { println(1) }
}
  • 简化:kotlin中使用lambda时支持一定的简化,表达式是最后一个参数,花括号可以移动到小括号的后面,当表达式是唯一参数时小括号可以省略,同时lambda支持类型推断,无需指明类型,而且如果表达式是唯一参数那么可以直接使用默认的参数命名it。注意如果表达式是用变量存储的话,类型声明就不可以省略,因为无法推断
//最完整的写法  
var oldest = people.maxBy({ p: Person -\> p.age })
//1.因为表达式是最后一个参数
var oldest = people.maxBy(){ p: Person -\> p.age }
//2.因为表达式是唯一的参数
var oldest = people.maxBy{ p: Person -\> p.age }
//3.因为可以进行类型推断
var oldest = people.maxBy{ p -\> p.age }
//4.编译器提供默认命名参数it,因此命名也不需要
var oldest = people.maxBy{ it.age }
  • lambda表达式中可以访问函数中的非final变量,实现的机制是将变量封装到包装器中,而包装器是final的,注意在异步调用的情况下,局部变量的修改只会在lambda执行时被修改,此时函数可能已经返回,不可能返回一个未来的值
fun printProblemCounts(response: Collection<String>){
    var clientErrors =0
    var serverErrors =0
    response.forEach{
        if(it.startsWith("4")){
            clientErrors++
        }else if(it.startsWith("5")){
            serverErrors++
        }
    }

    println("$clientErrors $serverErrors")
}


成员引用 ::

  • kotlin可以像java一样使用 :: 将函数转换为lambda表达式,如果使用的是顶层函数,则 :: 前面不需要类名
data class Person(val name: String, val age: Int)
var getAge = Person::age
fun main() {
    val people = listOf(Person("Alice", 29), Person("Bob", 31))

    var oldest = people.maxBy(getAge)
}
  • 如果需要将构造函数转换为lambda表达式,则使用 ::类名
var creator = ::Person
var obj = creator("",0)


SAM 构造方法

  • SAM构造方法允许我们使用lambda表达式来实现函数式接口,也就是Runnable和OnClickListener这样的接口,这样可以实现实例重用
fun main() {
    runnable.run()
}
val runnable = Runnable {
    println("run")
}
  • 需要注意的是SAM构造方法创建对象和使用object声明创建对象是有区别的,因为普通lambda本质上被视为代码块,因此普通lambda表达式内部不允许使用this关键字,做不到传出自身的引用,如果需要使用this,请改用object声明。另外在扩展函数时使用的lambda是一种特殊的情况,被称为带接受者的lambda,我们将在后面讲解这种情况


带接受者的lambda

  • 之前我们看过如何静态地声明类扩展函数,也可以动态地实现这种效果,下面动态声明了一个类的扩展函数并调用,该扩展函数的声明使用到了一种特殊的lambda,它是带接受者(也就是this)的,表达式中的this与静态声明的扩展函数中的this一致,指向的是被扩展的类的实例,可以省略。这个特性看起来有点多余,但它可以实现一些很重要的功能
fun main() {

    val newMeth:AClass.()->Boolean = {
        this.oldMeth()
        println("newMeth")
        true
    }
    val t = AClass().newMeth()
}

class AClass{

    fun oldMeth(){
        println("oldMeth")
    }
}


with

  • 有时候我们会经常对同一个对象进行操作,这个时候需要不但重复变量的名称,这时代码显得臃肿,不直观,特别是在变量名比较长的情况下
fun main() {

    println(alphabet())
}

fun alphabet():String{

    val result = StringBuilder()
    for(letter in 'A' .. 'Z'){
        result.append(letter)
    }

    result.append("\n now i know the alphabet")
    return result.toString()
}
  • with:使用with可以对上述情况进行简化,with是一个顶层函数,而不是内置在语法中的特性
fun alphabet():String = with(StringBuilder()){
    for(letter in 'A' .. 'Z'){
        append(letter)
    }

    append("\n now I know the alphabet")
    toString()
}
  • with的实现基于带接受者的lambda,从函数声明可以看出这一点
public inline fun \<T, R\> with(receiver: T, block: T.() -\> R): R 


apply

  • apply与with类似,只是apply返回的是被用于函数扩展的实例,这对于初始化对象的情况非常合适
fun alphabet()=StringBuilder().apply { 
    for (letter in 'A' .. 'Z'){
        append(letter)
    }
    append("\n now I know the alphabet")
}.toString()


集合函数式api

  • filter,对集合中满足判断式的元素进行过滤得到新集合
val numbers = listOf(1,2,3,4,5,6,7,8,9)
println(numbers.filter { it % 2 == 0 })

val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.filter{ it.age>30 })
  • map,将集合的元素进行转换得到新集合,元素类型可以改变
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9)
println(numbers.map { it * it })

val people = listOf(Person("Alice", 29), Person("Bob", 31))
println(people.map(Person::name))
println(
    people
        .filter { it.age > 30 }
        .map(Person::name)
)
  • all,判断是否所有元素都满足某个条件
val people = listOf(Person("Alice", 27), Person("bob", 31))
println(people.all { it.age > 30 })
  • any,判断是否存在符合条件的元素
val people = listOf(Person("Alice", 27), Person("bob", 31))
println(people.any { it.age > 30 })
  • count,计算符合条件的元素个数
val people = listOf(Person("Alice", 27), Person("bob", 31))
println(people.count { it.age > 30 })
  • find,查找符合条件的第一个元素
val people = listOf(Person("Alice", 27), Person("bob", 31))
val person = people.find { it.age > 30 }
  • groupBy,将符合元素的条件进行分组,返回一个map,元素依然还是集合
val people = listOf(
    Person("Alice", 27)
    , Person("bob", 31)
    , Person("Carol", 31)
)
val map = people.groupBy(getAge)
println(map)
  • flatten,用于将多个集合合并成为一个
    val listOfList = listOf(
        listOf(1, 2, 3)
        , listOf(4, 5, 6)
        , listOf(7, 8, 9)
    )

    println(listOfList.flatten())
//输出 [1, 2, 3, 4, 5, 6, 7, 8, 9]
  • flatMap,用于将集合中的元素通过某种方式构造集合,然后将每个元素得到的新结合合并,通常用于将一个集合装换为其他内容的集合
val list = listOf("123","45","6789")
println(list.flatMap { it.toList() })


序列 sequence

  • 类似filter和map的操作都会返回一个新的集合,如果较多或者操作或者元素较多,则会带来不必要的消耗
people.map(Person::name)
    .filter { it.startsWith("A") }
  • asSequence:可以使用asSequence将操作变成序列,对序列进行链式操作不会创建额外的集合来保存过程中产生的中间结果,序列的操作分为中间操作和末端操作,中间操作返回的是另一个序列,末端操作返回的是一个结果,中间操作都是惰性的。如下面的map和filter就是中间操作,而toList就是末端操作
people.asSequence()
    .map(Person::name)
    .filter { it.startsWith("A") }
    .toList()
  • 在不调用末端操作时中间操作是不会执行的
//没有输出  
listOf(1, 2, 3, 4).asSequence()
        .map { println("do map");it * it }
        .filter { println("do filter");it % 2 == 0 }
  • 惰性还有一个好处是,序列会依次对每个元素进行所有的操作,对于find这样并非需要访问所有元素的末端操作,则可以忽略不需要访问的元素,另外一些中间操作可以作为后续操作的先决条件,比如filter这样的操作优先执行可以减少操作的总次数
listOf(1, 2, 3, 4).asSequence()
    .map { println("do map");it * it }
    .filter { println("do filter");it % 2 == 0 }
    .find { it == 4 }
  • generateSequence:使用generateSequence可以不依赖集合类来创建一个序列,创建一个序列需要一个称谓seed的起始值(可以直接传入其值,也可以传入一个lambda表达式),还需要一个用于获取下一个元素的lambda表达式
println(
        //或者generateSequence({0}, { it + 1 })
    generateSequence(0) { it + 1 }
        .takeWhile { it < 100 }
        .sum()
)


可空性


非空类型

  • kotlin当中null也作为类型检查的一部分,默认情况下,所有类型都是非空的,所以当显式赋值为null时会报错
    var a:String
//Error:(14, 9) Kotlin: Null can not be a value of a non-null type String
a = null 
  • 对于函数调用也是如此,无法将null传给普通声明的参数
fun main() {
    strLen(null)
    //Error:(9, 12) Kotlin: Null can not be a value of a non-null type String
}
fun strLen(s:String) = s.length


可空类型

  • 可空类型:如果需要赋值null时,可以使用 ?进行可空类型声明
var a:String?
fun main() {
    strLen(null)
}
fun strLen(s:String?) = s?.length
  • 可空类型并不是简单地可以存储null值而已,因为编译器已经知道了变量很可能为空,因此可以做更严格的编译检查
  • 不能直接调用其函数
fun strLenSafe(s:String?) = s.length
  • 不能直接赋值给非空类型
val x: String? = null
val y: String = x
  • 不能直接传给非空参数
fun main() {

    val x: String? = null
    strLen(x)
}

fun strLen(s:String)=s.length


安全调用运算符 ?.

  • 我们需要一些特定的操作来使得上面对可空类型的调用可以工作,最直接的是添加判空,但这样的语句非常啰嗦,kotlin中可以使用安全调用运算符来简化这种语句
val x: String? = null
if (x != null) {
    strLen(x)
}
  • ?.: 安全调用运算符,用于调用可空类型的函数或获取属性(虽然非空类型也可以使用),可以理解为上面判空形式的简写,但注意安全调用在对象为null时,表达式的值也为null
fun main() {
    printAllCaps("abc")
    printAllCaps(null)
}

fun printAllCaps(s:String?){
    val allCaps: String? = s?.toUpperCase()
    println(allCaps)
}
  • 安全调用运算符也可以用于获取属性
    person.manager?.name

class Employee(val name: String, val manager: Employee?)
  • 安全调用运算符可以嵌套使用
person.manager?.manager?.manager?.manager


Elvis 运算符: ?:

  • Elvis: 用于赋值时为null提供可用的替换值,即如果符号左边的参数不为null,则使用左边的参数作为表达式值,否则使用右边的参数作为表达式值
fun foo(s:String?){
    val t:String = s?:""
}
  • Elvis可以像上面一样将可空类型转换成非空类型,在这种场景之下,替换值不可设置为null,当然如果是付给可空类型则可用使用null作为替换值,但这样做并没有什么意义
val t:String = s?:null //报错
val t:String? = s?:null//正确


安全转换 as?

  • as?:安全转换运算符,类似于is和as的结合,用于类型检测,如果类似匹配,返回原值并且自动进行as操作,否则返回null,注意通过与Elvis结合使用,我们还可以在类型不对时直接返回
fun isEmployee(obj: Any?):Boolean {

//或 val e = obj as? Employee?: Employee("",null)
    val e = obj as?Employee?: return false 
//as? 匹配成功之后即可以调用对应类型的属性和函数
    println(e.name)
    return true
}


非空断言 !!

  • !!:非空断言,顾名思义用于断言对象不是null,如果对象为null,则直接抛出nullpointerException,否则返回该对象,通常不建议使用非空断言,除非只是在开发阶段做测试调试
fun main() {
    test(null)
}

fun test(obj: Any?){
    obj!!
    //或者
    //val a = obj!!
}


let函数

  • let: 用于将对象传递到lambda函数当中,let与安全调用运算符一起使用,可以很方便地对一个可空类型进行频繁的操作
fun main() {
    val obj:String? = null
    obj?.let { send(it) }

    val map:HashMap<String,String>? = null
    map?.let { 
        it.clear()
        it.put("","")
    }
}

fun send(obj:String){
}


延迟初始化 lateinit

  • lateinit:用于进行延迟初始化,由于通常情况下kotlin要求所有字段在构造阶段初始化,有些需要在其他方法中初始化的字段,虽然我们可以使用可空类型赋值为null,但这样每次访问时就需要使用?或!!等操作,而使用 lateinit 则可以直接使用非空类型并且不需要马上初始化
class Test{
    lateinit var str:String
}

fun main() {
    val test = Test()
    //test.str = ""
    //Exception in thread "main" kotlin.UninitializedPropertyAccessException
    println(test.str)
}
  • 需要注意的是lateinit的字段在初始化前是不可以访问的,甚至不可以对他进行判空,只要访问了,就会抛出异常


可空类性扩展函数

  • 可以为可空类型添加特殊的扩展函数,这种函数在对象为空的情况下依然可以直接调用,而不需要添加安全调用运算符和其他操作
fun main() {
    val str:String? = null

    println(str.isNullOrBlank())
}

fun String?.isNullOrBlank():Boolean = 
    this == null || this.isBlank()


类型系统


平台类型

  • 平台类型指java当中不知道是否为空的类型,这个时候kotlin允许使用可空也允许使用非空类型来指向其实例,这种情况下,如果调用了空对象的属性或函数,则会抛出nullpointer异常
val a = Person().getName()
println(a.length) //nullpointer
  • 在继承java类时,重写方法可以将参数指定为可空或非空,原理类似上文


可空基本类型

  • kotlin当中不区分基本类型和包装类型,统一使用一种类型,同时为这些类型提供了很多便利的函数,大多数情况下kotlin当中的基本类型都会被编译成java中的基本类型,因此不用担心效率问题
val a = 10000
val b = a.coerceIn(0,1)
  • 不过当使用可空类型时就不可以用java中的基本类型来表示了,因为需要存储null值,泛型也是如此。注意对于可空类型,只有确定其为非空之后才可以


数字转换

  • 在kotlin当中基本类型之间赋值不会自动进行类型转换,而需要调用转换函数
val i = 1
val l:Long = i

val l:Long = i.toLong()
  • 对于参数类型也是如此需要手动调用转换函数
val x = 1
println(x.toLong() in listOf(1L, 2L, 3L))
  • 当表达式具有多种类型时,则编译器则会进行自动转换,同时由于我们使用类型推断,通常不用太关心这样的细节
val x:Int = 1
val y:Long = 1
val z = x+y
  • 字面常量支持使用下划线
fun main() {
    val x = 1_100_111
    println(x)
}


unit类型

  • Unit:类似java的void,kotlin当中如果一个函数没有需要显式指明的返回值,则使用Unit声明,如果我们显式地指明它们则是下面这样的,Unit也是一种实际的类型,但它只有一个唯一的实例 Unit,这就是它名称的由来
fun main() {
    val unit = test()
}

fun test():Unit{
    println("1")
    return Unit
}
  • 像void一样,编译器会自动为我们添加返回语句
fun main() {
    test()
}

fun test() {
    println("1")
}


nothing

  • Nothing:用于真正不需要返回值的函数,这种函数不需要返回值的原因是无法返回,函数内部不能使用return关键字,只有函数中有抛出异常或者死循环才能编译通过
fun fail(message: String): Nothing {

   throw IllegalStateException(message)
}

fun fail(message: String): Nothing {

    while (true){
    }
}
  • 可以尝试接受这种返回值,但实际上函数永远不会返回
val a :Nothing = fail("")
  • 一种很方便的用法是在Elvis运算符中
fun fail(message: String): Nothing {

    throw IllegalStateException(message)
}

fun main() {

    val str:String? = null
    val address = str?: fail("no address")
}


集合


可空性

  • kotlin的泛型当中支持可空类型和非空类型,所以在非空类型的集合中不允许加入null
val list = ArrayList<Int>()
list.add(null) //报错

val list = ArrayList<Int?>()
list.add(null) //正常


只读 可变

  • kotlin中将只读集合和可变集合分离,准确的说,可变集合接口一般是只读集合接口的子接口,前者扩展了修改数据的函数,这样的效果是我们可以通过函数参数判断集合是否会被修改,在集合的层级结构当中可变集合以Mutable开头比如MutableList,只读集合则为一般的集合类型名称比如List
fun read(collection: Collection<Int>) {
    println(collection)
}

fun write(mutableCollection : MutableCollection<Int>){
    mutableCollection.add(1)
}

    var list:List<Int> = listOf(1,2,3)
    var mutableList:MutableList<Int> = mutableListOf(1,2,3)

    read(list)
    read(mutableListOf(1,2,3))

    write(list)//报错
    write(mutableList)
  • kotlin环境下,java中的集合类如ArrayList,实现了kotlin当中的可变接口所以可以像上文一样使用,实际上如mutableListOf这样的函数内部调用的就是ArrayList
val list:List<Int> = ArrayList()
val mutableList:MutableList<Int> = ArrayList()
  • 注意如果需要将集合传给java方法则可以直接传递,不需要任何额外的操作,但是java方法可以任意修改集合,不管是否可变,但是当我们使用kotlin重写java当中的方法时,可以修改参数为可变或不可变类型,以及指定是否可空


数组

  • array:kotlin中的数组使用支持泛型的类型Array,并提供了一些扩展方法
fun main() {

    val letters = Array<String>(26){
        i -> ('a'+i).toString()
    }

    println(letters.joinToString(" "))
}
  • 和其他泛型的情况一样,类型参数会转换为对象类型,如果需要使用真正的基本类型的数组,kotlin还提供了专门的数据类型 IntArray, ByteArray,等等
val array:IntArray = intArrayOf(1,2,3)


运算符重载


算术运算符

  • operator:在kotlin当中运算符与特定的函数名绑定,使用operator可以进行运算符重载
fun main() {


    val point = Point(1,1) + Point(2,2)

    //Point(x=3, y=3)
    println(point)
}


data class Point(val x: Int, val y: Int) {

    operator fun plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
}
  • 也可以使用扩展函数来进行运算符重载
operator fun Point.minus(other: Point): Point {
    return Point(x - other.x, y - other.y)
}
  • 运算符重载时通常不限制参数类型,可以自己选择合适的类型
fun main() {

    val point = Point(1, 1) - 1   //Point(x=3, y=3)
    println(point)
}


data class Point(val x: Int, val y: Int)

operator fun Point.minus(other: Int): Point {
    return Point(x - other, y - other)
}
  • 算术运算符的函数名对应如下,kotlin并不支持重载位运算符
* : times
/ : div
% : mod
+ : plus
- : minus


复合运算符

  • 类似+=,-=的运算符也支持重载,但注意如果重载了-,+这样的运算符则可以直接使用复合运算符,如果即重载了同样的算术运算符又重载了同样的复合运算符,则会报错
var point = Point(1, 1) 
point-=1
  • ArrayList等集合重载了复合运算符,可以如下添加元素
val numbers = arrayListOf(1,2,3)
numbers +=4
println(numbers)//[1, 2, 3, 4]
  • 复合运算符对应的名称即在算术运算符后加上Assign,如timesAssign,plusAssign


一元运算符

  • 重载一元运算符不需要参数,注意自增自减在对象的左边和右边时的特性和一般语言中的一致,也是支持的
fun main() {
    val point =  Point(1,1)
    var test = -point;
}

operator fun Point.unaryMinus():Point{
    return Point(-x,-y)
}
  • 一元运算符对应的函数名如下
+ : unaryPlus
- : unaryMinus
! : not
++ : inc
-- : dec


比较运算符

  • equals,对应 == ,由于该函数已经在 any 中实现了,因此不能使用扩展函数实现它,注意java中的==运算符对应kotlin的===运算符,===运算符不允许重载
fun main() {
    println(Point(1,1) == Point(1,1))
}


data class Point(val x: Int, val y: Int) {

    override fun equals(other: Any?): Boolean {
        if (other === this) {
            return true
        }

        if (other !is Point) {
            return false
        }

        return other.x == x && other.y == y
    }
}
  • compareTo,对应 > \< ==,但注意需要实现Comparable接口,并且,如果重载了equals函数,则equals会覆盖Comparable的==
fun main() {
    println(Person("","") < Person("",""))
}

data class Person(val firstName:String,val lastName:String) : Comparable<Person>{
    override fun compareTo(other: Person):Int{
        return compareValuesBy(this,other,Person::lastName,Person::firstName)
    }
}


集合与区间的约定

  • get,set,对应中括号,类似map的集合就是实现了这样的函数,我们可以在普通的类上使用这种运算符,注意参数的类型和数量并没有限定,多个参数在调用时同样以逗号分隔
fun main() {
    println(Point(1,2)[0]) //1
}

operator fun Point.get(index: Int): Int {
    return when (index) {
        0 -> x
        1 -> y
        else ->
            throw  IndexOutOfBoundsException("invalid coordinate $index")
    }
}

fun main() {
    println(Point(1,2)[0]) //1
}

operator fun Point.set(index: Int, value:Int) {
    return when (index) {
        0 -> x = value
        1 -> y = value
        else ->
            throw  IndexOutOfBoundsException("invalid coordinate $index")
    }
}
  • in,可以使用in来判断元素是否在一个集合当中,我们也可以重载它来实现类似的功能
fun main() {

    val rect = Rectangle(Point(20,20),Point(50,50))

    println(Point(20,20) in rect)
}


data class Rectangle(val upperLeft: Point, val lowerRight:Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x
            && p.y in upperLeft.y until lowerRight.y
}
  • rangeTo,..运算符对应的函数,用来返回一个区间,通常我们不需要自己直接去实现这个函数,实现comparable接口既可以直接使用..运算符
  • iterator,为指定的区间添加iterator函数,则可以在for循环中使用in并自定义迭代器
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
    object : Iterator<LocalDate> {

        var current = start

        override fun hasNext() = current <= endInclusive

        override fun next() = current.apply {
            current = plusDays(1)
        }
    }

fun main() {


    val dayOffs =
        LocalDate.ofYearDay(2017, 1)..LocalDate.ofYearDay(2017, 3)

    for (dayOff in dayOffs){
        println(dayOff)
    }
}


解构


解构声明

  • componentX:类似js中的解构声明,componentX是一种特殊的运算符重载函数,用于声明变量所对应的属性,对于数据类型,编译器会自动为属性生成这样的函数,component后面的数字对应于变量的序号
class Point(var x: Int, var y: Int) {

    operator fun component1() =x

    operator fun component2()= y

}

fun main() {
    var p = Point(10,20)
    val(x,y) = p
    println(x)
    println(y)
}
  • 解构属性最有趣的一点是他可以实现类似 go 当中的多返回值函数,虽然,这只是从调用方的角度来看,函数本身的实现依然是返回带解构声明的对象。还有一点是,集合当中已经实现了解构,因此可以直接作用在集合对象上面
fun splitFileName(fullName:String):NameComponents{
    val (name,ext) = fullName.split(".",limit = 2)
    return NameComponents(name,ext)
}

fun main() {
    val (name,ext) = splitFileName("example.kt")
    println(name)
    println(ext)
}


循环

  • 可以直接在循环中使用解构,对于多个需要迭代的值不需要像java那样记录位置了
fun main() {
    printEntries(mapOf("oracle" to "java","jetbrains" to "kotlin"))
}


fun printEntries(map:Map<String,String>){
    for ((key,value) in map){
        println("$key -> $value")
    }
}


委托属性


by

  • by:上文中有提到by在类声明中的用法,by也可以使用在属性的声明上
fun main() {
    val p = Person()
    p.name = "testName"
    println(p.name)
}

class Person {
    var name by Delegate()
}

class Delegate{

    var realValue:String = ""

    operator fun getValue(person: Person, property: KProperty<*>): String {
        println("get")
        return realValue
    }

    operator fun setValue(person: Person, property: KProperty<*>, str: String) {
        println("set")
        realValue = str
  }
}
  • 集合类本身实现了getValue和setValue,对于需要动态扩展属性的场景可以使用集合进行委托
class Person {

    private val _attribute = hashMapOf<String,String>()

    fun setAttribute(attrName:String,value:String){
        _attribute[attrName] = value
    }

    val name:String by _attribute
}


by lazy()

  • 这是一种基于委托的懒加载方式,可以保证多线程下不重复加载,只能用于val引用,可以很方便的实现单例
class Person(val name:String){
    val emails by lazy { loadEmails(this) }

    private fun loadEmails(person: Person): Any {
        Thread.sleep(1000)
        return Any()
    }
}


高阶函数


泛型


注解和反射


注解声明


KAPI

##


DSL


文档注释

  • kotlin中的文档注释和java中的文档注释一样以/和两个星号开头,差别在于kotlin中的文档注释使用markdown语法而不是html,另外引用声明使用中括号表示



本篇未完