第二章:快速入门Kotlin编程

2.5 面向对象的编程

2.54 数据类与单例类

在一个规范的系统架构中,数据类通常占据非常重要的角色,它们用于将服务器或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。

Kotlin中,只需要在类的前面加上data关键词即可

1
data class Cellphone(val brand: String, val price: Double)

只要声明了data关键词,则这个类就是数据类

测试代码

1
2
3
4
5
6
fun main() {
val cellphone1 = Cellphone("Samsung", 1299.99);
val cellphone2 = Cellphone("Samsung", 1299.99);
println(cellphone1)
println(cellphone1 == cellphone2)
}

当一个类中没有任何代码时,还可以将尾部的大括号省略

Kotlin特有的功能——单例类

Kotlin中创建一个单例类只需要将class关键字改成object关键字即可

1
2
object Singleton {
}

此时的函数已经是单例类,我们只需要在函数里编写方法即可

1
2
3
4
5
object Singleton {
fun singletonTest(){
println("singleton is called")
}
}

调用单例类中的方法代码实现

1
Singleton.singletonTest()

2.6 Lamda编程

2.6.1 集合创建与遍历

集合主要包括ListSet,广泛一点就包括Map这样的键值对数据结构。

ListSetMap在java中都是接口,List的主要实现类是ArrayListlinkedListSet的主要实现方法时HashSetMap的主要实现类是 HashMap

现在提出创建一个包含许多水果名称的集合需求。如果是在java中,会先创建一个ArrayList实例,然后再一个个添加。

Kotlin提供一个内置的listOf()函数来简化初始化集合的写法。如下:

1
2
3
4
5
6
fun main() {
val list = listOf("apple","banana","orange")
for (fruit in list){
println(fruit)
}
}

注意点

listOf()创建的是一个不可变的集合不可变集合指的是该集合只能用于读,不能对集合进行添加,修改,或删除操作。

如果需要创建一个可变的集合,可以使用mutableListOf()函数就可以了。

1
2
3
4
5
6
7
fun main() {
val list = mutableListOf("apple","banana","orange")
list.add("watermelon")
for (fruit in list){
println(fruit)
}
}

Set集合的用法与List的一模一样,只是将创建集合的方式换成了setOf()mutableSetOf()函数。

1
2
3
4
5
6
fun main() {
val list = setOf("apple","banana","orange")
for (fruit in list){
println(fruit)
}
}

注意点

Set集合中是不可以存放重复元素的,如果存放了多个相同的元素,只会保留其中的一份。

Kotlin中不推荐使用put()和get()方法来对Map进行添加和读取数据操作,而是推荐使用一个类似于数组下标的语法结构

添加一条数据

1
map["apple"] = 1

读取一条数据

1
val number = map["apple"]

Kotlin同样提供了mapOf()mutableMapOf()函数继续简化Map用法

1
2
3
4
5
6
fun main() {
val map = mapOf("apple" to 1, "orange" to 2, "banana" to 3)
for ((fruit, number) in map) {
println("fruit is " + fruit + ", number is " + number)
}
}

其实to不是关键字,而是一个infix函数。这段代码的只要区别在于,在for-in循环中,我们将Map的键值对变量一起声明到了一对括号中,这样当进行循环遍历时,每次遍历的结果就会赋值给这两个键值对变量。

2.6.2 集合的函数式API

重点学习函数式API的语法结构,也就是Lamda表达式的语法结构

如何在一个水果集合里面找到单词最长的那个水果?

使用集合的函数式API

1
2
3
val list = listOf("Apple","Banana","Orange","Pear","Grape","Watermelon")
val maxLengthFruit = list.maxBy { it.length }
println(maxLengthFruit)

Lamda就是一小段可以作为参数传递的代码,正常情况下,我们向某个函数传参时只能传入变量,而借助Lamda允许传入一小段代码

Lamda语法结构

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

如果有参数传入到Lamda表达式,需要声明参数列表参数列表结束接函数体函数体中可以编写任意行代码。最后一行代码会自动作为Lamda表达式的返回值

没有化简时的代码

1
2
3
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val lamda = { fruit: String -> fruit.length }
val maxLengthFruit = list.maxBy { it.length }

不需要额外定义一个lamda变量

1
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })

Kotlin中规定,当Lamda参数是函数的最后一个参数时,可以将Lamda表达式移到函数括号外面

1
val maxLengthFruit = list.maxBy(){ fruit: String -> fruit.length }

Lamda参数是函数的唯一一个参数时,函数括号可以省略

1
val maxLengthFruit = list.maxBy{ fruit: String -> fruit.length }

由于Kotlin出色的类型推导机制,Lamda表达式中的参数列表在大多数情况下不必声明参数类型

1
val maxLengthFruit = list.maxBy { fruit -> fruit.length }

Lamda表达式的参数列表只有一个参数时,不必声明参数名,直接使用it关键字代替

1
val maxLengthFruit = list.maxBy { it.length }

map函数

集合中map函数是最常用的一种函数式API用于将集合中的每个元素都映射成一个另外的值,映射的规则在Lamda表达式中指定,最终生成一个新的集合

比如希望所有水果名都变成大写模式

1
2
3
4
5
6
7
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.map { it.toUpperCase() }
for (fruit in newList){
println(fruit)
}
}

map函数可以按照我们的需求对集合中的元素进行任意的映射转换

filter函数

用来过滤集合中的数据,可以单独使用,也可以配合map函数使用

保留5个字母以内的水果

1
2
3
4
5
6
7
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val newList = list.filter { it.length <= 5 }.map { it.toUpperCase() }
for (fruit in newList) {
println(fruit)
}
}

先是调用了filter函数再调用map函数,如果先调用map函数再调用filter函数,效率会差很多

anyall函数

any****函数用于判断集合中是否至少存在一个元素满足条件

all****函数用于判断集合中是否所有元素都满足指定条件

1
2
3
4
5
6
7
fun main() {
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon")
val anyResult = list.any { it.length <= 5 }
val allResult = list.all { it.length <= 5 }
println("anyResult is " + anyResult + ", allResult is " + allResult)

}

2.6.3 Java函数式API的使用

Java原生API中最常见的但抽象方法接口–Runnable接口

Java代码写法

1
2
3
4
5
6
new Thread(new Runnable(){
@Override
public void run(){
System.out.println("Thread is running");
}
}).start();

这里使用匿名内部类的写法。
Kotlin版本

1
2
3
4
5
6
Thread(object : Runnable{
override fun run() {
TODO("Not yet implemented")
println("Thread is running");
}
}).start();

Kotlin完全舍弃了new 关键字,而是使用object关键字。
简化代码

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

因为Runnable类中只要一个待实现的方法,即使这里没有显式地重写run方法,Kotlin也能自动明白Runable后面的Lambda表达式就是要在run方法中实现的内容。
如果Java方法的参数列表中有且只有一个Java但抽象方法接口参数,可以直接将Runnable接口名省略

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

Lamda表达式是方法的最后一个参数时,可以将Lamda表达式移到方法括号的外面。如果Lamda还是方法的唯一一个参数,直接将括号省略

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

Java函数式API使用例子:

Android中常用的点击事件接口OnclickListener

使用Kotlin函数式API的写法就可以简化成

1
button.setOnClickListener{}

2.7空指针检查

2.7.1 可空类型系统

Kotlin非常科学的解决了这个问题,它利用编译时判空检查的机制几乎杜绝了空指针异常

Kotlin默认所有的参数和变量都不为空,所以传入参数一定也也不为空。

图片

Kotlin将空指针异常的检查提前到了编译时期,如果程序存在空指针异常的风险,那么在编译的时候就会直接报错,修正才可以正常运行。

如果需要某个参数或者变量为空,可以在类名后面加上一个问号。

例如,**Int表示为不为空的整型,而Int?就表示可为空的整型。**

图片

这时调用参数方法可能出现空指针异常。这里直接进行判断处理就行

图片

2.7.2 判空辅助工具

?. 操作符。这个操作符就是当对象不为空时正常调用相应方法,当对象为空就什么都不做。

1
2
3
4
if(study!=null) {
study.readBook()
study.doHomework()
}

使用操作符

1
2
3
4
fun doStudy(study: Study?) {
study?.doHomework()
study?.readBook()
}

?: 操作符。这个操作符左右两边都接受一个表达式,如果左边的表达式的结果不为空就返回左边的结果,否则就****返回右边表达式的结果

例子:****没有使用关键字

1
2
3
4
5
val c = if(a!=null){
a
}else{
b
}

使用 ?: 关键字

1
val c = a?:b

结合两个关键字使用。

1
2
3
4
5
6
7
//获得一段文本长度
fun getTextLength(text : String?): Int{
if (text !=null){
return text.length
}
return 0
}

使用操作符简化

1
fun getTextLength(text: String?) = text?.length ?: 0

!! 非空断言工具,确信这里的对象不为空,强行通过编译。

使用场景

1
2
3
4
5
6
7
8
9
10
fun main(){
if(content!=null){
printUpperCase()
}

}
fun printUpperCase(){
val upperCase = content.toUpperCase()
println(upperCase)
}

这里再content.toUpperCase()编译无法通过,虽然在外部进行非空判断,

但是在函数里面不知道外部做了非空判断。

可以使用非空断言工具,在对象后面加上!!

1
2
3
4
fun printUpperCase(){
val upperCase = content!!.toUpperCase()
println(upperCase)
}

这样写就是让编译器不用做空指针检查了,出现问题就直接抛出空指针异常。

let函数。这个函数提供函数式API接口,并将原始调用对象作为参数传递到Lamda表达式中。

1
2
3
obj.let{obj2->
//编写逻辑
}

这里调用了obj的let函数,然后Lamda表达式的代码会立即执行。并且这个obj对象本身还会作为参数传递到Lamda表达式中。let函数属于kotlin中的标准函数。

下面这段代码其实进行了两次if判断语句。由于?.操作符的限制必须对每个对象进行一次判断

1
2
3
4
fun doStudy(study: Study?) {
study?.doHomework()
study?.readBook()
}

结合使用?.和let函数来对代码进行优化

1
2
3
4
5
6
fun doStudy(study: Study?) {
study?.let { stu ->
stu.readBook()
stu.doHomework()
}
}

?.操作符表示对象为空时什么都不做,对象不为空就调用let函数,而let函数会将study对象本身作为参数传入Lamda表达式中。此时对象肯定不为空。
Lamda表达式只有一个参数时,不用声明。

1
2
3
4
5
6
fun doStudy(study: Study?) {
study?.let {
it.readBook()
it.doHomework()
}
}

let函数处理全局变量的判空问题,if无法做到这点。

因为全局变量的值随时都有可能被其他线程所修改,即是做了判空处理仍然无法保证if语句中的study变量没有空指针风险。使用let可以正常工作。

图片

原本study是方法参数时,是可以编译通过的。