从头学习 Kotlin — 第一章(Kotlin 基础)


一、Kotlin基础语法学习

1 变量声明

1.1 通过val声明变量

在学习 Kotlin 语法的时候,第一个感到和 java 语法不同的点是在变量声明时。Java会将类型名放在变量名前面,而Kotlin会将类型名放在变量名后面或者直接省略变量名。例如我在编译器中输入以下语句声明一个变量,然后分别输出它们的类型名:

val string = "Kotlin"
val int = 1234
val long = 1234L
val float = 12.34f
val double = 12.34
println(string.javaClass.name)
...
println(double.javaClass.name)

将会得到以下结果:

java.lang.String
int
long
float
double

1.2 通过var声明变量

还有一种变量声明方法,即通过 var (variable) 关键字声明,JavaScript、Go 等其他语言也会使用 var 声明一个变量,它对应的其实就是 java 中的普通变量。而 val 可以理解成 var + final ,即不可变变量。例如:
在这里插入图片描述
通过 val 声明变量并对其进行修改时会提示使用var关键字声明,如果忽略提示强行执行会报 val cannot be reassigned 错误

在我查到的一些资料中都推荐优先使用 val、不可变对象和纯函数(没有副作用的函数)来设计程序。副作用简单来说就是由于修改了某处的某个数据导致程序不能按预想的结果执行,例如:

fun main(args: Array<String>) {
    count(1)  // 输出结果为3
    count(1)  // 输出结果为4
}
var a = 1
fun count(x: Int) {
    a++
    println(x+a)
}

以上这段代码受到了外部变量a的影响,这就是典型的副作用。

2 控制流

2.1 if-else语句

Kotlin 中的 if-else 语句和 java 中语法基本一致。但在 Kotlin 中,if-else可以作为一个表达式使用,也就是说它可以作为一个值赋值给变量或者作为函数返回值

val max = if (x > y) x else y
fun maxNumber(x: Int, y: Int) = if (x > y) x else y

2.2 when语句

when 语句类似于 java 中的 switch 语句,每个分支通过->连接,不再需要break,由上到下匹配,如果没有匹配到则执行else分支的逻辑(类似于switch语句中的default)。它同样可以作为表达式给变量赋值或者作为函数返回值,在if-else语句中分支过多时可以通过when语句进行优化。

fun main(args: Array<String>) {
    println(schedule("SUN"))
}
fun schedule(day: String) = when(day) {
    "SAT" -> "basketball"
    "SUN" -> "football"
    "FRI" -> "running"
    else -> "study"
}

2.3 for循环语句

Kotlin 中的 for 循环语句最大的特点在于其循环体的创建上,这个地方很像 python 的循环体,例如:

for (i in 1..10) println(i)
for (i in 1 until 10) println(i)
for (i in 1..10 step 2) println(i)
for (i in 10 downTo 1) println(i)

“1…10”为一种范围表达式,即表示i的取值在范围1到10内。除此之外,还可以通过until来表示一个半开区间,从上面的截图中也可以看出,通过…表示范围时两边都是闭区间,通过until表示范围是左边是闭区间右边是开区间。通过step函数可以设定迭代的步长,通过downTo可以实现倒序迭代。

3 函数和lambda表达式

3.1 函数参数

在声明一个Kotlin函数时,使用fun关键字 + 函数名 + 参数列表 + :返回值类型 + 函数体的方式声明,其中参数列表可以为空,但当参数存在时,必须指明参数类型,当参数存在默认值时参数类型同样不可省略

fun function (parameter1: Int, parameter2: String, parameter3: String = "String") {
    // 参数3的类型不可省略(Kotlin 不支持根据默认值推断类型)。
}

在声明可变参数时,使用 vararg 关键字修饰参数,可变参数一般在参数列表的最后,否则需要在调用函数时使用命名参数即通过该参数名称+参数值的方式进行传参

fun function (vararg parameter: Int) = parameter.sum()

可变参数同样支持泛型:

fun main(args: Array<String>) {
    printAll("Kotlin", "Java", "C++")
    printAll(1, 2, 3, 4, 5)
}

fun <T> printAll(vararg items: T) {
    for (item in items) {
        println(item)
    }
}

3.2 函数返回值

虽然 Kotlin 在很大程度上支持类型推导,但是并不是所有的函数返回值类型都可以省略,例如一个常见的函数声明中:

fun sum(x: Int, y: Int): Int { return x + y }

如果将函数类型去掉则会提示以下错误:
在这里插入图片描述
因为没有声明返回值的类型,函数会默认将返回值类型当成 Unit,然而实际上返回的时 Int,所以会出现编译报错。以上这种函数声明方式叫做代码块函数体,除此之外还有一种表达式函数体将 {} 去掉,同时用 = 定义一个函数:

fun sum(x: Int, y: Int) = x + y

在使用表达式函数体的情况下可以一般可以不声明返回值类型。有一般情况就必然有二般情况,在使用递归表达式时则必须声明返回值类型,如:

fun foo (n: Int): Int = if (n == 0) 1 else n * foo(n - 1) 

如果去掉返回值类型则会出现以下错误:
在这里插入图片描述
因此关于是否需要显示声明类型的情况可以总结如下:

  • 函数的参数 必须声明
  • 非表达式定义的函数除了返回 Unit,其他情况必须声明
  • 递归的函数必须声明

3.3 高阶函数

Kotlin中的高阶函数即将其他函数作为参数返回值的函数,它的基本格式为:(Int) -> Unit。它必须遵循以下几点:

  • 通过->符号组织参数类型和返回值类型,左边是参数类型右边是返回值类型;
  • 必须用一个括号包裹参数类型;
  • 返回值类型必须显示声明,即使它是Unit;
  • 如果该函数没有参数类型,也必须用()表示() -> Unit不可省略

如果该函数具有多个参数,它们之间通过逗号分隔:

(Int, String) -> Unit

高阶函数还支持返回另一个函数,如:

(Int) -> ((Int) -> Unit)

它表示接收一个 Int 类型的参数,并返回一个 (Int) -> Unit 类型的函数。

3.4 方法和成员引用

接下来的问题是如何将一个函数进行传参,我原本以为直接将函数名传递进去就可以,但实际上函数名不是一个表达式,它的类型为函数的返回值类型,我们需要的是一个参数类型 -> 返回值类型的东西。因此直接将函数名作为参数传入就会报类型不匹配的错误。
在这里插入图片描述
正确做法是通过双冒号对于某个类的方法进行引用,例如:Country::isEuropeanCountry,如果该方法属于当前类则可省去前面的类型直接引用,如:::isEuropeanCountry。这样就可以成功将函数作为参数传入了。

3.5 匿名函数

除了上面提到的引用方法进行传参以外,我们还可以使用一种“随用随写”的方法进行传参,这就是匿名函数。Kotlin允许在没有函数名的情况下定义一个函数,即匿名函数,例如:

filterCountries(countries, fun (country: String): Boolean {
        return country == "EU"
    })

我们在调用某个高阶函数去现写它的参数的函数,这和后面学到的Lambda表达式非常相似。

3.6 Lambda表达式

Lambda表达式其实就是一种简化后的匿名函数,如果我们想通过一个匿名函数声明一个加法函数,需要以下语句:

val sum = fun(x: Int, y: Int): Int { return x + y }

我们可以通过Lambda表达式进行简化,简化后变成:

val sum:(Int, Int) -> Int = {x, y -> x + y}

Lambda表达式的基本语法总结如下:

  • 一个Lambda表达式必须通过 {} 包裹
  • 如果Lambda声明了参数部分的类型,且返回值类型支持类型推导,那么Lambda变量就可以省略函数类型声明;
  • 如果Lambda变量声明了函数类型,那么Lambda的参数部分类型可以省略。

还有需要注意的一点是,如果Lambda表达式返回的不是Unit,那么默认最后一行表达式的值就是返回值。

4 类和对象

4.1 创建一个类和对象

在Kotlin中通过关键字class创建一个类,其中 constructor 关键字可省略:

class Bird constructor(weight: Double, color: String, age: Int){
    fun fly() {}
}

通过object关键字创建一个对象:

object Bird{
    val weight: Double = 500.0
    val color: String = "blue"
    val age: Int = 1
    fun fly() {}
}

它们在语法上不同的一定是:class中的属性通过()包裹,object中的属性和方法通过{}包裹。这是因为class需要通过构造函数进行实例化,()内可以理解成class类的构造函数的参数,而object是单例的它没有构造函数也不允许实例化,因此用{}包裹。

4.2 构造函数

上述通过constructor关键字声明的构造函数为主构造函数,Kotlin中还有一种次构造函数,它使用this关键字委托给主构造函数,在声明此构造函数时constructor关键字不可省略

class Bird (weight: Double, color: String, age: Int){
    fun fly() {}
    constructor(color: String, age: Int) : this(100.0, color, age)
}

Kotlin还支持在构造函数中使用默认参数:

class Bird (weight: Double = 500.0, color: String = "Blue", age: Int = 1)

在创建对象时只需要传递需要修改的参数值即可:

val bird1 = Bird(color = "red")

4.3 init语句块

Kotlin中的主次构造函数只能对参数进行赋值,如果我们想在初始化时对它进行一些额外的操作就可以使用init语句块实现,例如:

class Bird (weight: Double, color: String, age: Int){
    val sex: String
    init {
    	println("这是一个init语句块")
        this.sex = if (color == "blue") "male" else "female"
    }
}

在上面的代码中,通过init代码块实现了对sex属性的初始化,该属性根据color属性而变化。

4.4 类的继承

Kotlin中通过“:”替代Java中的 extends 和 implements 关键字来实现类的继承和接口实现。需要注意的一点是:Kotlin中类和方法默认是不可被继承或重写的,因此需要在被继承的类和重写的方法前面加上open修饰符。

open class Bird (weight: Double, color: String, age: Int){
    open fun fly(){
        println("Bird can fly")
    }
}

class Cuckoo(weight: Double, color: String, age: Int) : Bird(weight, color, age){
    override fun fly(){
        println("Cuckoo can fly")
    }
}

在类的继承和对父类方法重写是需要遵循里氏替换原则。

4.5 接口和抽象类

Kotlin中的接口和抽象类声明方法与Java中相同,都是通过 interface 和 abstract 声明。Kotlin中的接口支持抽象属性和接口方法的默认实现:

interface Flyer{
    val speed: Int
    fun kind()
    fun fly() {
        println("I can fly")
    }
}

如上面的代码,其中speed为一个抽象属性,fly方法为一个默认实现的接口方法。将以上代码转换成java代码可以清楚的看到它们具体是怎么实现的:

public interface Flyer{
    int getSpeed();
    void kind();
    void fly();
    public static final class DefaultImpls {
        String var1 = "I can fly";
        System.out.println(var1);
    } 
}

Kotlin编译器通过定义了一个静态的内部类 DefaultImpls 来提供fly方法的默认实现。它的属性声明是通过一个get方法实现的,因此接口中的属性不能像Java中的接口那样直接赋值
Kotlin中声明一个抽象类方法如下:

abstract class Animal(val name: String) {
    abstract fun makeSound()

    open fun sleep() {
        println("$name is sleeping")
    }
}

需要注意的是,抽象类中的非抽象方法默认是 final 的,因此如果想要重写非抽象方法也需要通过open进行修饰。

4.6 枚举类

枚举类是一种特殊的类,它的定义和使用与Java基本相同,不做过多赘述:

enum class Color {
    RED, GREEN, BLUE
}
enum class Color(
    val rgb: Int,
    val description: String
) {
    RED(0xFF0000, "热情"),
    GREEN(0x00FF00, "自然"),
    BLUE(0x0000FF, "冷静");
    
    // 成员函数
    fun printInfo() {
        println("$description: #${rgb.toString(16).uppercase()}")
    }
}

以上两段代码分别定义了基本的枚举类和带有枚举常量和方法的枚举类,在使用时可通过Color.RED.printInfo()语句调用枚举类中的方法。

5 集合

Kotlin中的集合可以分为只读集合和可变集合,只读集合实现的是 Collection<T> 接口,提供了最基本的集合操作,可变集合实现的是 MutableCollection<T> 接口,该接口继承自 Collection<T>,增加了一些对结合的更改操作如 add 和 remove 。具体细分,只读集合和可变集合又可分为List、Set和Map:

// 只读集合
val list = listOf(1, 2, 3)          
val set = setOf("a", "b", "c")          
val map = mapOf(1 to "one", 2 to "two") 
// 可变集合
val mutableList = mutableListOf(1, 2, 3)
val mutableSet = mutableSetOf("a", "b")
val mutableMap = mutableMapOf(1 to "one")

对结合的操作可以分为过滤、映射、聚合、查找等:

val numbers = listOf(1, 2, 3, 4, 5)
// 过滤
val even = numbers.filter { it % 2 == 0 }  // [2, 4]
// 映射
val squared = numbers.map { it * it }      // [1, 4, 9, 16, 25]
// 聚合
val sum = numbers.sum()                    // 15
val product = numbers.reduce { acc, i -> acc * i }  // 120 (本质上为一个累乘操作)
// 查找
val firstEven = numbers.find { it % 2 == 0 }  // 2

这些操作的具体含义一般不难理解,但其中it这个关键字对第一次学习Kotlin的人来说会比较懵,包括我自己。其实它是Kotlin简化Lambda表达式的一种语法糖,叫做单个参数的隐士名称。例如上面的过滤操作,如果不用it则可以写成:val even = numbers.filter { item -> item % 2 == 0 },其实就是将一个复杂的Lambda表达式进行了简化。
除了这些常见的集合操作外,Kotlin还支持链式调用:

val result = numbers
    .filter { it > 2 }      // [3, 4, 5]
    .map { it * 2 }         // [6, 8, 10]
    .firstOrNull { it > 7 } // 8

以及不同类型集合之间的转换:

val list = listOf(1, 2, 3)

// 转换为其他类型
val set = list.toSet()        // Set<Int>
val array = list.toIntArray() // IntArray

// 转换为 Map
val map = list.associateWith { it * 2 }  // {1=2, 2=4, 3=6}

6 异常处理

Kotlin的基本用法与Java相似,同样包括try-catch-finally,但最不同的一点是:Kotlin 没有检查型异常(Checked Exceptions),只处理运行时异常(Unchecked),不像 Java 那样强制要求捕获 IOException 等。
并且Kotlin不需要在函数签名中声明 throws。如果为了和java进行互操作可以通过@Throws注解:

@Throws(IOException::class)
fun readFile() {
    throw IOException("File not found")
}

在 Kotlin 中,try-catch 语句和 throws 语句都可以作为一个表达式给变量赋值:

val result = try {
    10 / 2  // 正常情况返回 5
} catch (e: Exception) {
    0  // 异常时返回 0
}
val message: String = throw IllegalArgumentException("No message")

Kotlin同样可以自定义异常类型:

class MyCustomException(message: String) : Exception(message)

fun riskyOperation() {
    throw MyCustomException("Something went wrong!")
}

最后,还有非常重要的一点:Kotlin 可以使用 runCatching 捕获异常并返回 Result 对象

val result = runCatching {
    "Result: " + (10 / 0)
}

result
    .onSuccess { println(it) }
    .onFailure { println("Caught exception: ${it.message}") }

7 泛型

Kotlin中泛型的使用非常简单,通过<T>或者<E>来表示泛型类型。

fun <T> identity(item: T): T {
    return item
}
class Box<T>(val value: T) {
    fun getValue(): T = value
}

和java一样,kotlin可以通过类型约束指定泛型的上界:

fun <T : Number> doubleValue(x: T): Double {
    return x.toDouble() * 2
}

这段代码表示泛型只能 Number 类或者 Number 类的子类。而 Kotlin 中的下界限定却不同于 java 中的 ? super T 语法,它是通过逆变操作(in)实现的:

fun addStrings(list: MutableList<in String>) {
    list.add("Hello")
    list.add("World")
}

这段代码表示泛型类型为 String 或其父类型。相对于逆变操作,out 协变操作类似于 java 中的 ? extends T,不同点是 out 只能做输出而in只能做输入,例如:

// out 协变,类似 Java 的 ? extends T
val listOut: List<out Number> = listOf(1, 2.0)  // 读取安全,不能写入

// in 逆变,类似 Java 的 ? super T
val listIn: MutableList<in String> = mutableListOf<Any>()
listIn.add("Hello")  // 可以写入 String

Kotlin同样支持多个类型参数和约束的情况:

fun <T, R> combine(a: T, b: R): String {
    return "$a and $b"
}

fun <T> whereExample(item: T) where T : CharSequence, T : Comparable<T> {
    println(item.length)
    println(item.compareTo(item))
}

与java不同的是,kotlin 在实现多约束时通过 where 语法实现,而 java 通过 & 取交集实现。

8 扩展函数与扩展属性

8.1 扩展函数

扩展函数的定义非常简单,首先需要一个接收者类型(通常是类名或者接口的名称)作为扩展函数的前缀,例如为MutbableList<Int> 扩展一个 exchange 方法:

fun MutableList<Int>.exchange(fromIndex: Int, toIndex: Int) {
    val tmp = this[fromIndex]
    this[fromIndex] = this[toIndex]
    this[toIndex] = tmp
}

Kotlin的this要比Java的更灵活,这里扩展函数体里的this地比哦的是接收者类型的对象。需要注意的是Kotlin严格区分了接收者是否为空。如果扩展函数是可空的,需要重写一个可空类型的扩展函数。我们可以将扩展函数理解成一个静态方法,它独立于该类的任何对象,且不依赖类的特定实例,被该类的所有实例共享。
综上可以得出,扩展函数不会带来额外的性能消耗。

8.2 扩展属性

与扩展函数类似,我们可以为一个类添加扩展属性。比如还是上面的 MutableList<Int> 类,为其添加一个判断和是否为偶数的属性:

val MutableList<Int>.sumIsEven: Boolean 
    get(){
        return this.sum() % 2 == 0
    }

和扩展函数一样,扩展属性本质上也是Java中的静态属性,因此不能为这个属性添加一个默认值。它的行为只能由显示提供的getters和setters定义。

9 智能转换和空安全

9.1 智能转换

Kotlin的智能转换指的是在编译器判断一个对象类型后,自动将其“转换”为特定类型,而不需要显式地进行强制类型转换。例如:

fun smartCastExample(obj: Any) {
    if (obj is String) {
        // 编译器自动将 obj 视为 String
        println(obj.length)
    } else {
        println("Not a String")
    }
}

这里 obj 原本是 Any,但在 is String 的检查后,编译器智能地把它当成 String,可以直接访问 length。需要注意的一点是:只有在确认对象不可变(val 或局部变量)时,才进行智能转换。如果变量是 var,编译器无法保证其在检查后未被改变,因此不会进行智能转换。

9.2 定义非空类型

与Java不同,Kotlin可区分非空类型和可空类型,例如以下代码:

// java
Long x = null;
// kotlin
val x: Long = null // 报错 Null can not be a value of a non-null type Long

java在声明一个变量时可以完全无误地将其初始化为null,但是在kotlin中这是不允许的。因为在kotlin中Int、Long、String等等这些类型均为非空类型,为它们赋值null就会报错。
在这里插入图片描述
编译器提示我们可以在这些类型的后面加上“?”,比如 Long?,实际上 Long? = Long or null。

9.3 非空类型的安全调用?.

想象这样一个场景,我们为班级上的座位创建一个类,该对象包含学生属性。学生同样为一个类,它包含眼镜属性。眼镜也被定义为一个类,它包含度数属性。我们知道,不是所有的座位都有学生,也不是所有的学生都戴眼镜,但是所有的眼镜都有度数。所以在创建上述情况的类时需要如下代码:

data class Glasses(val degree: Double)
data class Student(val glasses: Glasses?)
data class Seat(val student: Student?)

其中学生类中的眼镜属性和座位类中的学生属性为可空类型,眼镜度数为非空属性。如果想知道某个座位上学生的眼镜度数可以这么写:

println("该位置上学生的眼镜度数为:${seat.student?.glasses?.degree}")

这里的“?.”为安全调用,也就是当student存在时才会调用其下的glasses。

9.4 Elvis操作符?:

我们可以通过?:操作符为某个变量提供默认值。还是上一小节的例子,如果某个座位没有学生或者该学生不戴眼镜,我们希望得到眼镜度数为-1。那么可以通过以下代码:

val degree = seat.student?.glasses?.degree?:-1
    println("该位置上学生的眼镜度数为:${degree}")

“?:”运算符被称为Elvis运算符,或者合并运算符

10 委托

Kotlin中委托是一种设计模式,它的作用是一个类将部分职责转交给另一个对象,允许对象通过组合而非继承来复用行为。委托包括接口委托属性委托,通过by关键字来实现。

10.1 接口委托

它的核心思想是:通过by关键字将接口实现委托给另一个对象,避免手动编写样板代码。

fun main(args: Array<String>) {
    val bird = Bird()
    val superBird = SuperBird(bird)
    superBird.fly()

}

interface Flyable {
    fun fly()
}

class Bird : Flyable {
    override fun fly() = println("Bird can fly")
}

以上代码中的 SuperBird 类没有自己实现 fly 方法,而是将其委托给 Bird 类实现。

10.2 属性委托

属性委托的主要思想是将属性的 getter/setter 逻辑委托给另一个对象,实现延迟加载可观察属性等功能。

属性委托最常见的用法就是延迟加载,延迟加载的属性在首次调用时才会被赋值,并且一旦被赋值将不再被更改。因此使用延迟加载的属性必须是引用不可变的,不能通过var来声明。

class Bird(val weight: Double, val age: Int, val color: String) {
    val sex: String by lazy { 
        if (color == "yellow") "male" else "female"
    }
}

by lazy的背后原理是接受一个 lambda 表达式并返回一个 Lazy<T> 实例的函数,第一次访问该属性时,会执行 lazy 对应的 lambda 表达式并记录结果,后续访问该属性时只是返回记录的结果。

可观察属性则是通过将属性委托给Delegates.observable来实现属性变化过程的可视化,例如:

class User {
    var name: String by Delegates.observable("Kotlin") {
        property, oldValue, newValue ->
        println("属性${property}${oldValue}变成了${newValue}")
    }
}

10.3 自定义委托

自定义委托中,委托类需要实现 getValue() 方法或者 setValue() 方法,前者对应 val 声明的变量,后者对应 var 声明的变量。

fun main(args: Array<String>) {
    val value: String by Delegate()
    println(value) // 输出Kotlin
    var value2: String by Delegate()
    value2 = "Java" // 输出Java 已经赋值给 value2
}

class Delegate {
    operator fun getValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>): String {
        return "Kotlin"
    }

    operator fun setValue(thisRef: Any?, property: kotlin.reflect.KProperty<*>, value: String) {
        println("$value 已经赋值给 ${property.name}")
    }
}

11 本文参考资料

  • 《Kotlin核心编程》,水滴技术团队著,机械工业出版社。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值