Kotlin语法基础

本章节建议重点学习!!!基础不牢,地动山摇!!

QQ_1736060951550

语法

这里列举的都是Kotlin和其他语言相比所独有的特征,Kotlin基本语法需要习惯化的点:

  1. 只使用val(不可变)var(可变) 来定义变量,强制指定使用冒号加类型;
  2. 基本数据类型类似Python是对象,更加强调面向对象编程;
  3. Kotlin完全舍弃了new关键字,因此创建匿名类实例的时候就不能再使用new了,而是改用了object关键字。

函数

以下是函数的一些语法糖:

  1. 当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可(注意Kotlin的自动推导很厉害的,一行代码如果有返回值,可以不用显示强调返回类型,会自动推导出来)

    1
    2
    3
    // 下面两句话等价,并且编辑器提示会正确出现返回值类型
    fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
    fun largerNumber(num1: Int, num2: Int) = max(num1, num2)

条件

if的语法糖

  1. Kotlin中的if语句有一个额外的功能,它是可以有返回值的,返回值就是if语句每一个条件中最后一行代码的返回值;

    1
    2
    3
    4
    5
    fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) { 
    num1
    } else {
    num2
    }

when语法和语法糖

  1. when语句允许传入一个任意类型的参数,然后可以在when的结构体中定义一系列的条件,格式 是:

    1
    匹配值 -> { 执行逻辑 } 
  2. 基本匹配和类型匹配:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 基本匹配
    fun getScore(name: String) = when (name) {
    "Tom" -> 86
    "Jim" -> 77
    "Jack" -> 95
    "Lily" -> 100
    else -> 0
    }
    // 类型匹配 is 是核心
    fun checkNumber(num: Number) {
    when (num) {
    is Int -> println("number is Int")
    is Double -> println("number is Double")
    else -> println("number not support")
    }
    }
  3. 不带参数写法,拓展性强:

    1
    2
    3
    4
    5
    6
    7
    fun getScore(name: String) = when { 
    name == "Tom" -> 86
    name == "Jim" -> 77
    name == "Jack" -> 95
    name == "Lily" -> 100
    else -> 0
    }

循环

区间

  1. 区间[0, 10]

    1
    val range = 0..10 
  2. 区间[0, 10)

    1
    val range = 0 until 10 
  3. 步进

    1
    val range = 0 .. 10 step 2
  4. 倒序

    1
    val range = 10 downTo 0

for-in

已丢弃掉for-i循环,转投for-in

对象

继承与构造

  1. 在Kotlin中任何一个非抽象类默认都是不可以被继承的,加上open关键字之后,类就允许被继承;

  2. 主构造函数的特点是没有函数体,Kotlin给我们提供了一个init结构体,所有主构造函数中的逻辑都可以写在里面:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    open class Person { 
    ...
    }
    class Student(val sno: String, val grade: Int) : Person() {
    init {
    println("sno is " + sno)
    println("grade is " + grade)
    }
    }
  3. 调用父类构造函数,传递参数不需要变量声明,否则会错误。

    1
    2
    3
    4
    5
    6
    7
    open class Person(val name: String, val age: Int) { 
    ...
    }
    class Student(val sno: String, val grade: Int, name: String, age: Int) :
    Person(name, age) {
    ...
    }

    我们在Student类的主构造函数中增加name和age这两个字段时,不能再将它们声明成val,因为在主构造函数中声明成val或者var的参数将自动成为该类的字段,这就会导致和父类中同名的name和age字段造成冲突。因此,这里的name和age参数前面我们不用加任何关键字,让它的作用域仅限定在主构造函数当中即可

  4. Kotlin规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    open class Person(val name: String, val age: Int) { 
    ...
    }
    class Student(val sno: String, val grade: Int, name: String, age: Int) :
    Person(name, age) {
    // 次构造函数1
    constructor(name: String, age: Int) : this("", 0, name, age) {
    }
    // 次构造函数2
    constructor() : this("", 0) {
    }
    }

    val student1 = Student() // 本质是先调用次构造函数2,由次构造函数2调用次构造函数1,次构造函数1再调用主构造函数
    val student2 = Student("Jack", 19) // 本质是先调用次构造函数1,次构造函数1再调用主构造函数
    val student3 = Student("a123", 5, "Jack", 19) // 直接调用主构造函数

  5. 只有次构造函数,没有主构造函数

    1
    2
    3
    4
    5
    6
    7
    open class Person(val name: String, val age: Int) { 
    ...
    }
    class Student : Person {
    constructor(name: String, age: Int) : super(name, age) {
    }
    }

    总之,必须调用到root类的主构造函数,这是本质。注意这里的代码变化,首先Student类的后面没有显式地定义主构造函数,同时又因为定义了次构造函数,所以现在Student类是没有主构造函数的。那么既然没有主构造函数,继承 Person类的时候也就不需要再加上括号了。

接口

  1. 接口函数使用interface定义;
  2. 实现接口和继承类似,但是接口没有构造函数,因此不用加括号;
  3. Kotlin 使用override关键字来重写父类或者实现接口中的函数;
  4. 函数接受接口类作为参数,在函数内部调用接口方法,叫面向接口编程,简称多态

可见性修饰符

  1. private,当前类内部可见;
  2. public,所有类可见,是默认项
  3. protected,当前类和子项可见;
  4. internal,同一模块中可见。

数据类和单例类

  1. 数据类通常需要重写equals(),hashCode(),toString() 这几个方法;
  2. Kotlin 只需要声明data关键字,表明这个类是一个数据类,其中固定和无实际逻辑的方法自动生成;
  3. Kotlin 中,单例类生成使用object关键字取代class关键字即可,不需要私有化构造函数;
  4. 调用单例类,不需要实例化,可以类似静态方法的调用方式执行,但是其实背后是Kotlin 自动创建了一个实例。

Lambda编程

Lambda表达式的语法结构:

1
{参数名1 : 参数类型, 参数名2 : 参数类型 -> 函数体}

ArrayList和HashSet

ArrayList简称List,HashSet简称Set。

  1. Kotlin 专门提供内置函数listOf() 来简化初始化集合,创建的是一个不可变的集合;
  2. mutableListOf() 函数创建的是一个可变的集合;
  3. Set集合不可以存放重复元素,这是异于List 集合的最大不同。
  4. Kotlin 同样提供了setOf()mutableSetOf() ,以简化set用法

HashMap

HashMap简称map。

  1. Kotlin 中不建议使用put()get() 方法对map进行添加和读取数据操作,而是更加推荐使用类似于数组下标的语法结构。

    1
    2
    map["Apple"] = 1 // 写入
    val number = map["Apple"] // 读取
  2. Kotlin 同样提供了mapOf()mutableMapOf() ,以简化map用法。

    1
    val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5) 
  3. 键值对使用to关联,而to不是关键字,这是一种infix函数,在后续学习。

集合函数式API

  1. maxBy就是一个普通的函数,它接收的是一个Lambda类型的参数,并且会在遍历集合时将每次遍历的值作为参数传递给Lambda表达式。maxBy函数的工作原理是根据 我们传入的条件来遍历集合,从而找到该条件下的最大值;

    1. Kotlin 规定,当Lambda参数是函数最后一个参数时,可以将Lambda表达式移到函数括号外面;

      1
      val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length } 
    2. 如果Lambda参数是函数唯一一个参数的话,还可以将函数括号省略;

      1
      val maxLengthFruit = list.maxBy { fruit -> fruit.length } 
    3. 当Lambda表达式的参数列表中只有一个参数时,不必声明参数名,可以使用it 关键字替代。

      1
      val maxLengthFruit = list.maxBy { it.length } 
  2. map 它用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lambda表达式中指定,最终生成一个新的集合;

    1
    val newList = list.map { it.toUpperCase() }  // 将名字全部转化为大写
  3. filter是用来过滤集合中的数据的;

    1
    val newList = list.filter { it.length <= 5 } // 获取长度小于5的名字
  4. any用于判断集合中是否至少存在一个元素满足指定条件;

  5. all用于判断集合中是否所有元素都满足指定条件。

Java函数式API

针对Runnable接口进行讲解。

  1. 因为Runnable类中只有一个待实现方法,即使这里没有显式地重写run()方法,Kotlin也能自动明白Runnable后面的Lambda表达式就是要在run()方法中实现的内容,否则使用override重写多个方法函数。

    1
    2
    3
    Thread(Runnable { 
    println("Thread is running")
    }).start()
  2. 一个Java方法的参数列表中有且仅有一个Java单抽象方法接口参数,我们还可以将接口名进行省略;

    1
    2
    3
    Thread({ 
    println("Thread is running")
    }).start()
  3. 当Lambda表达式是方法的最后一个参数时,可以将Lambda表达式移到方法括号的外面。同时,如果Lambda表达式还是方法的唯一一个参数,还可以将方法的括号省略。

    1
    2
    3
    Thread { 
    println("Thread is running")
    }.start()

空指针检查

Kotlin默认所有的参数和变量都不可为空。

判空辅助工具

  1. 最常用的是 ?. 操作符,这个操作符的作用非常好理解,就是当对象不为空时正常调用相应的方法,当对象为空时则什么都不做;

    1
    a?.doSomething() 
  2. 另外一个非常常用的 ?: 操作符,这个操作符的左右两边都接收一个表达式,如果左边表达式的结果不为空就返回左边表达式的结果,否则就返回右边表达式的结果;

    1
    val c = a ?: b 
  3. 如果我们想要强行通过编译,可以使用非空断言工具,写法是在对象的后面加上 !! ,如下所示:

    1
    2
    3
    4
    fun printUpperCase() { 
    val upperCase = content!!.toUpperCase()
    println(upperCase)
    }
  4. 一个比较与众不同的辅助工具——let。let既不是操作符,也不是什么关键字,而是一个函数。这个函数提供了函数式API的编程接口,并将原始调用对象作为参数传递到 Lambda表达式中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    obj.let { obj2 -> 
    // 编写具体的业务逻辑
    }

    var study: Study? = null
    fun doStudy(study: Study?) {
    study?.let {
    it.readBooks()
    it.doHomework()
    }
    }

    这里调用了obj对象的let函数,然后Lambda表达式中的代码就会立即执行,并且这个obj对象本身还会作为参数传递到Lambda表达式中。不过,为了防止变量重名,这里我将参数名改成了obj2,但实际上它们是同一个对象,这就是let函数的作用。

  5. 全局变量的值随时都有可能被其他线程所修改,即使做了判空处理,仍然无法保证if语句中的study变量没有空指针风险,仍会报错:

    1
    2
    3
    4
    5
    6
    7
    var study: Study? = null
    fun doStudy() {
    if (study != null) {
    study.readBooks()
    study.doHomework()
    }
    }

Kotlin 中的小魔术

  1. 字符串内嵌表达式,Kotlin允许我们在字符串里嵌入 ${} 这种语法结构的表达式,并在运行时使用表达式执行的结果替代这一部分内容:

    1
    "hello, ${obj.name}. nice to meet you!" 
  2. 函数的参数默认值,可以通过键值对的方式来传参。

    次构造函数在Kotlin中很少用,因为Kotlin 提供了给函数设定参数默认值的功能,它在很大程度上能够替代次构造函数的作用。