「码上开学——hencoder」Kotlin笔记(Kotlin 里那些「更方便的」)

1,524 阅读17分钟

构造器

主构造器

我们之前已经了解了Kotlin中constructor的写法:

class User {
    var name: String
    constructor(name: String) {
        this.name = name
    }
}

其实Kotlin中还有更简单的写法来写构造器:

class User constructor(name: String) {
    //                   这里与构造器中的name是同一个
    var name: String = name
}

这里有几处不同点:

  • constructor构造器移到了类名之后
  • 类的属性name可以引用构造器中的参数name

这个写法叫「主构造器primary constructor」。与之相对的在第二篇,写在类中的构造器被称为「次构造器」。在Kotlin中一个类最多只能有一个主构造器(也可以没有),而此构造器是没有个数限制。

主构造器中的参数除了可以在类的属性中使用过,还可以再init代码块中使用:

class User constructor(name: String) {
    var name: String
    init {
        this.name = name
    }
}

其中init代码块是紧跟在主构造器之后执行的,这是因为主构造器本身没有代码体,init代码块就充当了主构造器代码体的功能。

另外,如果类中有主构造器,那么其他的次构造器都需要通过this关键字调用该主构造器,可以直接调用或者通过别的次构造器间接调用。如果不调用IDE就会报错:

class User constructor(var name: String) {
    constructor(name: String, id: Int) {
    //👆🏻这样写会报错,Primary constructor call expected
    }
}

为什么当类中有主构造器的时候就强制要求次构造器调用主构造器呢?

  • 必须性:创建类的对象时,不管使用那个构造器,都需要主构造器的参与
  • 第一性:在类的初始化过程中,首先滞后性的就是主构造器

这也就是主构造器的命名由来。 当一个类中同时有主构造器与次构造器的时候,需要这样写:

class User constructor(var name: String) {
                                      // 👇🏻 直接调用主构造器
    constructor(name: String, id: Int) : this(name) {
    }
    
                                                 //👇🏻 通过上一个次构造器,间接调用主构造器
    constructor(name: String, id: Int, age: Int) : this(name, id) {
    
    }
}

在使用次构造器创建对象时,init代码块是先于次构造器执行的。如果把主构造器看成身体的头部,那么init代码块就是颈部,次构造器就相当于身体其余部分。

细心的你也许会发现这里又出现:符号,它还有其他场合出现过,例如:

  • 变量的声明: var id: Int
  • 类的继承: class MainActivity: AppCompatActivity() {}
  • 接口的实现:class User: Impl{}
  • 匿名类的创建:object: ViewPager.SimpleOnPageChangeListener() {}
  • 函数的返回值:fun sum(a: Int, b: Int): Int

可以看出:符号在Kotlin中非常高频出现,它其实表示了一种依赖关系,在这里表示依赖于主构造器。

通常情况下,主构造器中的constructor关键字可以省略:

class User(name: String) {
     var name: String = name
}

但有些场景,constructor是不可以省略的,例如在主构造器上使用「可见性修饰符」或者「注解」:

  • 可见性修饰符我们之前已经讲过,它修饰普通函数与修饰符构造器的用法是一样的,
class User private constructor(name: String) {
    // 主构造器被修饰符为私有的,外部就无法调用该构造器
}

既然主构造器可以简化类的初始化过程,那我们就帮人帮到底,送佛送到西,用主构造器把属性的初始化也一并给简化了。

主构造器里声明属性

之前我们讲了主构造器中的参数可以在属性中进行赋值,其实还可以在主构造器中直接声明属性:

class User(var name: String) {
}
//等价于:
class User(name: String) {
    var name: String = name
}

如果在主构造器的声明时加上var或者val,就等价于在类中创建了该名称的属性(property),并且初始值就是主构造器中该参数的值。 以上讲了所有关于主构造器相关的知识,让我们总结一下的初始化写法:

  • 首先创建一个User类:
class User {
}
  • 添加一个参数为nameid的主构造器
class User (name: String, id: String) {
}
  • 将主构造器中的nameid声明为类的属性
class User(val name: String, val id: String) {
}
  • 然后在init代码块中添加一些初始化逻辑:
class Useer(val name: String, val id: String) {
    init {
        ...
    }
}
  • 最后再添加其他次构造器:
class User(val name: String, val id: String) {
    init {
        ...
    }
    constructor(person: Person) : this(person.name, person.id) {
    }
}

当一个类有多个构造器时,只需要把最基本、最通用的那个写成主构造器就行了。这里我们选择将参数为nameid的构造器作为主构造器。 到这里,整个类的初始化就完成了,类的初始化顺序就和上面的步骤一样。 除了构造器,普通函数也是有很多简化写法的。

函数简化

使用 = 连接返回值

我们已经知道了Kotlin中的函数的写法:

fun area(width: Int, height: Int): Int {
    return widthh * heighht
}

其实,这种只有一行代码的函数,还可以这么写:

fun area(width: Int, height: Int): Int = width * height

{}return没有了,使用=符号连接返回值。 我们之前讲过,Kotlin有「类型推断」的特性,那么这里函数的返回类型还可以隐藏掉:

//                               👇🏻 省略了返回类型
fun area(width: Int, height: Int) = width * height

不过,在实际开发中,还是推荐显式地讲返回类型写出来,增加代码可读性。 以上是函数有返回值的情况,对于没有返回值的情况,可以理解为返回值是Unit:

fun sayHi(name: String) {
    println("Hi " + name)
}

因此也可以简化成下面这样:

fun sayHi(name: String) = println("Hi " + name)

简化完函数体,我们再来看看前面的参数部分。

对于Java中的方法重载,我们都不陌生,那Kotlin中是否有更方便的重载方式呢? 接下来我们看看Kotlin中的「参数默认值」的用法

参数默认值

Java中,允许在一个类中定义多个名字相同的方法,但是参数的类或个数必须不同,这就是方法重载:

public void sayHi(String name) {
    System.out.println("Hi "+ name);
}

public void sayHi() {
    say("world");
}

在Kotlin中,也可以使用这样的方式进行函数重载,不过哦还有一种更简单的方式,那就是「参数默认值」:

fun sayHi(name: String = "world") = println("Hi " + name)

这里world是参数name的默认值,当调用该函数时不穿参数,就会使用该默认值。

这就等价于上面Java写的重载方法,当调用sayHi函数时,参数是可选的:

sayHi("kaixue.io")
sayHi()//使用了默认值"world"

既然与重载函数的效果相同,那Kotlin中的参数默认值有什么好处呢?仅仅只是少写了一些代码吗?

其实在Java中,每个重载方法的内部实现可以各不相同,这就无法保证重载方法内部设计上的一致性,而Kotlin的参数默认值就解决了这个问题。

不过参数默认值在调用时也不是完全可以放飞自我的。

来看下面这段代码,这里函数中有默认值的参数在无默认值参数的前面:

fun sayHi(name: String = "world", age:Int) {
    ...
}
sayHi(10)
//这时想使用默认值进行调用,IDE会报一下两个错误
// The integer literal does not conform to the expected type String
// No value passed for parameter 'age'

这个错误就是告诉你参数不匹配,说明我们的「打开方式」不对,其实Kotlin里是通过「命名参数」来解决这个问题的。

命名参数

具体用法如下:

fun sayHi(name: String = "world", age: Int) {
    ...
}
sayHi(age = 21)

在调用函数时,显示地指定了参数age的名称,这就是「命名参数」。Kotlin中的每一个函数参数都可以作为命名参数。

再来看一个非常多参数的函数的例子:

fun sayHi(name: String = "world", age: Int, isStudent: Boolean = true, isFat: Boolean = true, isTall: Boolean = true) {
    ...
} 

当函数非常多的参数时,调用该函数就会写成这样

sayHi("world", 21, false, true, false)

当看到后面一长串的布尔值时,我们很难分清楚每个参数的用处,可读性很差。通过命名参数,我们就可以这么写:

sayHi(name = "wo", age = 21, isStudent = false, isFat = true, isTall = false)

与命名参数相对应的一个概念被称为「位置参数」,也就是按位置是顺序进行参数填写。

当一个函数被调用时,如果混用位置参数与命名参数,那么所有的位置参数都应该放在第一个命名参数之前:

fun sayHi(name: String = "world", age: Int) {
    ...
}

sayHi(name = "wo", 21) //IDE会报错, Mixing named and positioned arguments is not allowed
sayHi("wo", age = 21)//这是正确的写法

本地函数(嵌套函数)

首先来看下这段代码,这是一个简单的登录的函数:

fun login(user: String ,password: String, illegalStr: String) {
    // 验证user是否为空
    if (user.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
    //验证password是否为空
    if (password.isEmpty()) {
        throw IllegalArgumentException(illegalStr)
    }
}

该函数中,检查参数这个部分有些冗余,我们又不想将这段逻辑作为一个单独的函数对外暴露。这时可以使用嵌套函数,在login函数内部声明一个函数。

fun login(user: String, password: String, illegalStr: String) {
    fun validate(value: String, illegalStr: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(illegalStr)
        }
    }
    
    validate(user, illegalStr)
    validate(password, illegalStr)
}

这里我们将共同的验证逻辑放进了嵌套函数validate中,并且login函数之外的其他地方无法访问这个嵌套函数。

这里的illegalStr是通过参数的方式传进嵌套函数中的,其实完全没有这个必要,因为嵌套函数中可以访问在它外部的所有常量和变量,例如类中的属性、当前函数中的参数与变量等。

我们稍加改进:

fun login(user: String, password: String, illegalStr: String) {
    funvalidate(value: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException(illegalStr)
        }
    }
}

这里省去了嵌套函数中的illegalStr参数,在该嵌套函数内直接使用外层函数login的参数illegalStr

上面login函数中的验证逻辑,其实还有另一种更简单的方法:

fun login(user: String, password: String, illegalStr: String) {
    require(user.isNotEmpty()) { illegalStr }
    require(password.isNoEmpty()) { illegalStr }
}

其中用到了lambda表达式以及Kotlin内置的require函数。

字符串

字符串模板

在Java中,字符串与变量之间是使用+符号进行拼接的,Kotlin中也是如此:

val name = "world"
println("Hi "+ name)

但是当变量比较多的时候,可读性会变差,写起来也比较麻烦。

Java给出的解决方法是String.format

System.out.print(String.format("Hi %s", name));

Kotlin为我们提供了一种更加方便的写法:

val name = "world"
// 用'$'符号加参数的方式
println("Hi $name")

这种方式就是把name从后置改为前置,简化代码的同时增加了字符串的可读性。

除了变量,$后还可以跟表达式,但表达式是一个整体,所以我们要用{}给它包起来:

val name = "world"
println("Hi ${name.length}")

其实就跟四则运算的括号一样,提高语法上的优先级,而单个变量的场景可以省略{}

字符串模板还支持转义字符,比如使用转义字符\n进行换行操作:

val name = "world!\n"
println("Hi $name")//会多大一个空行

字符串模板的用法对于我们Android工程师来说,其实一点都不陌生。

首先,Gradle所用的Groovy语言就已经有了这种支持:

 def name = "world"
 println "Hi ${name}"

在Android的资源文件中,定义字符串也有类似用法:

<string name="hi">Hi %s</string>
getString(R.id.hi, "world")

raw string(原生字符串)

有时候我们不希望写过多的转义字符,这种Kotlin情况通过「原生字符串」来实现。

用法就是使用一堆"""将字符串括起来:

val name = "world"
val myName = "kotlin"

val text = """
    Hi $name!
  My name is $myName.\n
"""
println(text)

这里有几个注意点:

  • \n并不会被转义
  • 最后输出的内容与写的内容完全一致,包括实际的换行
  • $符号引用变量仍然生效

这就是「原神字符串」。输出结果如下:

    Hi world!
  My name is kotlin.\n

但对其方式看起来不太优雅,原生字符串还可以通过trimMargin()函数去除每行前面的空格:

val text = """
    |Hi world!
  |My name is kotlin.
""".trimMargin()
println(text)

输出的结果如下:

Hi world!
my name is kotlin.

这里的trimMargin()函数有以下几个注意点:

  • |符号为默认的边界前缀,前面只能有空格,否则不会生效
  • 输出的|符号以及它前面的空格都会被删除
  • 边界前缀还可以使用其他字符,比如trimMargin("/"),只不过上方的代码生死用的参数默认值的调用方式。

数组和集合

数组与集合的操作符

在之前的文章中,我们已经知道了数组和集合的基本概念,其实Kotlin中,还为我们提供了许多使数组与集合操作起来更加方便的函数。

首先声明如下IntArrayList

val intArray = intArrayOf(1, 2, 3)
val strList = listOf("a", "b", "c")

接下来,对它们的操作函数进行讲解:

  • forEach:遍历每一个元素
// lambda表达式,i表示数组的每个元素
intArray.forEach { i ->
    print(i + " "_
}
//输出:1 2 3

除了「lambda」表达式,这里也用到了「闭包」的概念,这又是另一个话题了,这里先不展开。

  • filter:对每个元素进行过滤操作,如果lambda表达式中的条件成立则留下该元素,否则踢出,最终生成新的集合
// [1, 2, 3]
👇🏻
// {2, 3}
// 注意,这里变成了List
val newList: List = intArray.filter {i ->
    i != 1//过滤掉数组中等于1的元素
}
  • map:遍历每个元素并执行给定表达式,最终形式新的集合。
// [1, 2, 3]
👇🏻
// {2, 3, }
val newList: List = intArray.map{ i ->
    i +1 // 每个元素加1
}
  • flatMap:遍历每个元素,并为每个元素创建新的集合,最后合并到一个集合中.
// [1, 2, 3]
👇🏻
// {"2","a","3","a", "4", "a"}
intArray.floatMap {i -> 
    listOf("${i + 1}", "a"/)
}

这里是以数组intArray为例,集合strList也同样有这些操作函数。Kotlin还有许多类似的操作函数。

除了数据和集合,Kotlin中还有另一种常用的数据类型:Range.

Range

在Java语言中并没有Range的概念,Kotlin中的Range表示区间的意思,也就是范围。区间的常见写法如下:

val range: IntRange = 0..1000

这里的0..1000就表示从0到1000的范围,包括1000,数学上称为闭区间[01000]。除了这里的IntRange,还有CharRange以及LongRange

Kotlin中没有纯的开区间的定义,不过有半开区间的定义:

val range: IntRange = 0 until 1000

这里的0 until 1000表示从0到1000,但不包括1000,这就是半开区[0,1000)。

Range这个东西,天生就是用来遍历的:

val range = 0..1000
// 默认步长为1, 输入:0,1,2,3,4,5,6,7....1000
for (i in range) {
    print("$i, ")
}

这里的in关键字可以与for循环结合使用,表示挨个遍历range中的值。关于for循环控制的使用,后面会做具体讲解。

除了使用默认的步长1,还可以通过step设置步长:

val range = 0..1000
// 步长为2, 输出:0,2,4,6,8,10....1000
for(i in range step 2) {
    print("$i, ")
}

以上试试递增区间,Kotlin还提供了递减区间downTo,不过递减没有半开区间的用法:

// 输出:4,3,2,1
for (i in 4 downTo 1) {
    print('$i, ")
}

其中4 downTo 1就表示递减的闭区间[4,1].这里的downTo以及上面的step都叫做「中缀式表达」,之后的文章会做介绍。

Sequence

序列Sequence又被称为「惰性集合操作」,关于什么是惰性,我没听你通过下面的例子来解释:

val sequence = sequenceOf(1, 2, 3, 4)
val ressult: Sequence<Int> = sequence
    .map {i -> 
        println("Map $i")
        i * 2
    }
    .filter { i ->
        println("Filter $i")
        i % 3 == 0
    }
👇🏻
println(result.first())// 只取集合的第一个元素
  • 惰性的概念首先就是说在「👇🏻」标注之前的代码运行时不会立即执行,它只是定义了一个执行流程,只有result被使用道德时候才会执行。
  • 当「👇🏻」的println执行时是数据处理流程是这样的:
    • 取出元素1 -> map为2->filter判断2是否能被3整除
    • 取出元素2 ->map为4->filter判断4是否能被3整除
    • ...

惰性指当出现满足条件的第一个元素的时候,Sequence就不会执行后面的元素遍历了,即跳过了4的遍历。 而List是没有惰性的特性的:

List是没有惰性的特性的:

val list = listOf(1, 2, 3, 4)
val result: List = list
    .map {i ->
        println("Map $i")
        i * 2
    }
    .filter { i ->
        println("Filter $i")
        i %3 == 0
    }
👇🏻
println(ressult.first())//只取集合的第一个元素

包括两点:

  • 声明之后立即执行
  • 数据处理流程如下:
    • {1, 2, 3, 4,} -> {2, 4, 6, 8}
    • 遍历判断是否能被3整除

Squence这种类似懒加载的实现有下面这些优点:

  • 一旦满足遍历退出的条件,就可以省略后续不必要的遍历过程。
  • List这种实现Iterable接口的集合类,每调用一次函数就会生成一个新的Iterable,下一个函数再基于新的Iterable执行,每次函数调用产生的临时Iterable会导致额外的内存消耗,而Sequence在整个流程中只有一个。

因此,Sequence这种数据类型可以在数据量比较大或者数据量未知的时候,作为流式处理的解决方案。

条件控制

相比Java的条件控制,Kotlin中对条件控制进行了许多的优化及改进。

if/else

首先来看下Java中的if/else写法:

int max;
if (a > b) {
    max = a;
} else {
    max = b;
}

在Kotlin中,这么写当然也可以,不过,Kotlin中if语句还可以作为一个表达式赋值给变量:

val max = if (a > b) a else b

另外,Kotlin中启用了三元运算符(条件 ?然后:否则),不过我们可以使用if/else来代替它。

上面的if/else的分支中是一个变量,其实还可以是一个代码块,代码块的最后一行会作为结果返回:

val max = if (a > b) {
    println("max: a")
    a //返回a
} else {
    println("max: b")
    b //返回b
}

when

在Java中,用switch语句老判断一个变量与一系列之中某个值是否相等:

switch (x) {
    case 1: {
        System.out.println("1");
        break;
    }
    case 2: {
        System.out.println("2");
        break;
    }
    default: {
        System.out.println("default");
    }
}

在Kotlin中变成when

when (x) {
    1 -> { println("1") }
    2 -> { println("2") }
    else -> { println("else") }
}

这里与Java相比的不同点有:

  • 省略了casebreak,前者比较好理解,后者的意思就是Kotlin自动为每个分支加上了break的功能,防止我们像Java那样写错
  • Java中的默认分支使用的是default关键字,Kotlin中是使用的是else

if/else一样,when也可以作为表达式进行是使用,分支中最后一行的结果只作为返回值。需要注意的是,这时就必须要有else分支,使得无论怎样都会有结果返回,除非已经列出了所有的情况:

val value: Int = when (x) {
    1-> { x + 1 }
    2-> { x * 2 }
    else -> { x + 5 }
}

在Java中,当多种情况执行同一份代码时,可以这么写:

switch (x) {
    case 1:
    case 2:{
        System.out.println("x== 1 or x == 2);
        break;
    }
    default: {
        System.out.println("default");
    }
}

而Kotlin中多种情况执行同一份代码时,可以将多个分支条件放在一起,用,符号隔开,表示这些情况都会执行后面的代码:

when (x) {
    1, 2 -> print("x == 1 or x == 2")
    else -> print("else")
}

when语句中,我们还可以使用表达式作为分支的判断 条件:

  • 使用in检测是否在一个区间或者集合中:
when (x) {
    in 1..10 -> print("x 在区间 1..10 中")
    in listOf(1, 2) -> print("x 在集合中"// not in
    !in 10..20 -> print("x 不在区间 10..20中")
    else -> print("不在任何区间上")
}

-或者使用is进行特定类型的检测:

val isString = when(x) {
    is String -> true
    else -> false
}

还可以省略when后面的参数,每一个分支条件都可以是一个布尔表达式:

when {
    str1.contains("a") -> print("字符串 str1 包含 a")
    str2.length == 3 -> print("字符串str2的长度为 ")
}

当分支的判断条件为表达式时,哪一条件先为true就执行哪个分支的代码块。

for

我们知道Java对一个集合或数组可以这样遍历:

int[] array = {1, 2, 3, 4};
for (int item : array) {
    ...
}

而Kotlin中对数组的遍历是这么写的:

val array = intArrayOf(1, 2, 3, 4)

for (item in array) {
    ...
}

这里与Java有几处不同:

  • Kotlin中,表示单个元素的item,不用显示的声明类型
  • Kotlin使用的是in关键字,表示itemarray里面的一个元素

另外,Kotlin的in后面的变量可以是任何实现Iterable接口的对象。

在Java中,我们还可以这么写for循环:

for (int i = 0; i <= 10; i++) {
    //遍历从010
}

但Kotln中没有这样的写法,那应该怎样实现一个0到10的遍历呢? 其实使用书上面讲过的区间就可以实现啦,代码如下:

for (i in 0..10) {
    println(i)
}

try-catch

关于try-catch我们都不陌生,在平时开发中难免都会遇到异常需要处理,那么在Kotlin中是怎样处理的呢,先看下Kotlin中捕获异常的代码:

 try {
     ...
 }
 catch (e: Exception) {
     ...
 } finally {
     ...
 }

可以发现Kotlin异常处理与Java的异常处理基本相同,但也有几个不同点:

  • 我们知道在Java中,调用一个抛出异常的方法时,我们需要对异常进行处理,否则就会报错:
public class User {
   void sayHi() throws IOException {
   }
   void test() {
       sayHi();
       // IDE报错,Unhandled exception: java.io.IOException
   }
}

但在Kotlin中,调用上方User类的sayHi方法时:

val user = User()
user.sayHi()// 正常调用,IDE不会报错,但运行时会报错

为什么这里不会报错呢?因为Kotlin中的异常是不会被检查的,只有在运行时如果sayHi抛出异常,才会出错。

  • Kotlin中try-catch语句也可以是一个表达式,允许代码块的最后一行作为返回值:
val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

?.和?:

我们在之前的文章中已经讲过Kotlin的控安全,其实还有另外一个常用的复合符号可以让你判断空时更加方便,那就是Elvis操作符?:

我们知道控安全调用?.,在对象非空时会执行后面的调用,对象为空时就会返回null。如果这时将该表达式赋值给一个不可空的变量:

val str: String? = "Hello"
val length: Int = str?.length
// IDE报错,Type mismatch, Required:Int. Found:Int?

报错的原因就是str为null时我们没有值可以返回给length 这时就可以使用Kotlin中额度Elvis操作符?:来兜底:

val str: String? = "Hello"
val length: Int = str?.length ?: -1

它的意思是如果左侧表达式str?.length结果为空,则返回右侧的值-1。 Elvis操作符还有另外一种常见用法,如下:

fun validate(user: Usser) {
    val id = user.id ?: return//验证user.id是否为空,为空时return
}
// 等同于
fun validate(user: User) {
    if (user.id == null) {
        return
    }
    val id = user.id
}

看到这里,想必你对Kotlin的控安全有了更深入的了解了,下面我们再看看Kotlin的相等比较符。

== 和 ===

我们知道在Java中,==比较的如果是基本类型数据则判断值是否相等,如果比较的是String则表示引用地址是否相等,String字符串的内容比较使用的是euqlas()

String str1 = "123", str2 = "123";
System.out.println(str1.euqlas(str2));
System.out.println(str1 == str2);

Kotlin中也有两种相等比较方式:

  • ==:可以对基本数据类型以及String等类型进行内容比较,相当于Java中的queals
  • ===:对引用的内存地址进行比较,相当于Java中的==

可以发现,Java中的equals,在Kotlin中与之相对应的是==,这样可以使我们的代码更加简洁。

下面再来看看代码实例:

val str1 = "123"
val str2 = "123"
println(str1 == str2) // 内容相等,输出:true

val str1 = "字符串"
val str2 = str1
val str3 = str1
print(str2 == str3)// 引用地址相等,输出true

其实Kotlin中的queals函数是==的操作符重载。

版权声明

本文首发于:rengwuxian.com/kotlin-basi…

微信公众号:扔物线