Java
中的 final
变量。Java
中的非 final
变量。fun main() { val a = 10 println("a = " + a) } 复制代码
运行结果:
Kotlin
在赋值时会进行自动推导,可以根据值的类型推导出变量的类型,如果使用下面这种延迟赋值的方式,那么 Kotlin
将无法推到值得类型,这样程序就变报错
fun main() { val a: Int = 10 println("a = " + a) } 复制代码
由于上面使用的是不可变的变量,所以想要更改变量的值就会报错,所以需要将 val
改成 var
类型
fun main() { var a: Int = 10 a = a * 10 println("a = " + a) } 复制代码
总结:永远优先使用 val
来声明变量,当 val
无法满足你的需求时再使用 var
,这样设计出来的程序更加健壮,也更加符合高质量的编码规范。
/** * 创建一个有两个参数的 Int 返回类型的方法 */ fun methodName(param1: Int, param2: Int): Int { return 0 } 复制代码
fun main() { val a = 37 val b = 40 val value = largerNumber(a, b) println("larger number is " + value) } /** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int): Int { return max(param1, param2) } 复制代码
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int): Int = max(param1, param2) 复制代码
进一步简化版:
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int) = max(param1, param2) 复制代码
Kotlin 中的条件语句有 if 和 when,其中 if 和 Java 中的 if 没有区别,这里简单了解一下。
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int): Int { var value = 0 if (param1 > param2) { value = param1 } else { value = param2 } return value } 复制代码
Kotlin 中的 if 用法和 Java 相比有一个额外的功能,它可以有返回值,返回值就是 if 语句每一个条件中最后一行代码的返回值,因此可以进行如下格式的书写:
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int): Int { val value = if (param1 > param2) { param1 } else { param2 } return value } 复制代码
在这里由于 value
只需要进行一次赋值,所以可以将 var
更改为 val
。
进一步简写:
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int): Int { return if (param1 > param2) { param1 } else { param2 } } 复制代码
再一次精简:
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int) = if (param1 > param2) { param1 } else { param2 } 复制代码
或者
/** * 对比 param1 和 param2 返回较大的那个数 */ fun largerNumber(param1: Int, param2: Int) = if (param1 > param2) param1 else param2 复制代码
Kotlin 中的 when 语句有点类似于 Java 中的 switch 语句,但是比 switch 更加精简。
使用格式:匹配值 -> {执行逻辑}
/** * 通过名字返回分数 */ fun getScore(name: String) = if (name == "Tom") { 86 } else if (name == "Jim") { 77 } else if (name == "Jack") { 95 } else if (name == "Lily") { 100 } else { 0 } /** * 使用 when 语句实现通过名字返回分数 */ fun getScore(name: String) = when (name) { "Tom" -> 86 "Jim" -> 77 "Jack" -> 95 "Lily" -> 100 else -> 0 } 复制代码
注意:Java 中的 switch
语句支持的类型有限,再 JDK1.7 中支持了字符串类型,但是有些类型却仍然不支持,但是 when
语句却解决了以上 痛点。
/** * 判断传入的 number 是什么数据类型 */ fun checkNumber(num: Number) { when (num) { is Int -> println("number is Int") is Double -> println("number is Double") else -> println("number not support") } } 复制代码
上述代码中,is
关键字是匹配类型的核心,它相当于 Java 中的 instanceof
关键字。由于 checkNumber()
函数接收一个 Number
类型的参数,这是 Kotlin 中内置的抽象类,比如 Int、Double、Float、Long 都属于它的子类。
/** * 使用 when 表达式实现通过名字返回分数 */ fun getScore(name: String) = when { name == "Tom" -> 86 name == "Jim" -> 77 name == "Jack" -> 95 name == "Lily" -> 100 else -> 0 } 复制代码
通常 when
语句的括号里都是有参数的,如果不在括号里写参数就要再匹配项前面添加参数。
在 Java 中提供了 for、while 新欢,在 Kotlin 中同样也提供了这两种循环,其中 while 循环没有一点差异,所以这里直接讲解 for 循环。
在使用循环之前先说明一下如何声明区间,例如在 Kotlin 中声明 [0, 10] 之间的区间使用val range = 0..10
的形式,其中 .. 是创建两端闭区间的关键字。
val range = 0..10 for (i in range) { println(i) } 复制代码
如果想声明[0, 10)这个区间可以使用 until
替代 ..
for (i in 0 until 10) { println(i) } 复制代码
默认情况下,i 会每次自增 1,如果想让 i 一次加 2 的话可以使用 step 2
实现,3,4,5.。。n 也是同样的道理。
for (i in 0 until 10 step 2) { println(i) } 复制代码
注意:在进行遍历时左边的数值必须小于右边的数值,如果想实现降序的话要使用downTo
替代。
for (i in 20 downTo 10 step 2) { println(i) } 复制代码
/** * 创建 Person 实体类,由于需要创建对象后再给属性赋值, * 所以这里使用 var 而不是 val */ class Person { var name = "" var age = 0 fun eat() { println(name + " is eating. He is " + age + " years old") } } fun main() { val p = Person() p.name = "Jack" p.age = 19 p.eat() } 复制代码
在 Kotlin 中取消了 new 关键字,因为调用构造函数就是为了实例化,所以进行了精简。
如果定义一个学生类他的里面会包含如学号、年级等属性,但学生也是人,也需要姓名、年龄等属性,如果再重新添加姓名和年龄属性会有冗余代码。所以这里可以使用继承的概念,这样Student类就自动拥有了Person类的属性。
class Student { var sno = "" var grade = 0 } 复制代码
要是想继承 Person 类,必须让 Person 类具有可以被继承的能力,这也是 Kotlin 与 Java 不同的地方,这么设计的原因和 val 的设计理念时相同的,因为如果一个类可以随便被继承就有可能会产生风险,在 Effective Java 一书中就指出,如果一个类不是专门为继承而设计的,那么就应该主动加上 final
关键字,禁止它可以被继承。
很明显 Kotlin 在设计时就遵循了这个规范,默认所有非抽象类时不可以被继承的,之所以一直说非抽象类,是因为抽象类本身是无法创建实例的,一定要由子类去继承它才可以创建实例,因此抽象类必须要被继承,否则就没有意义了。
在 Kotlin 中要想让一个类有被继承的能力,只需要在类前面添加 open
关键字。
open class Student { var sno = "" var grade = 0 } 复制代码
/** * 创建 Person 实体类,由于需要创建对象后再给属性赋值, * 所以这里使用 var 而不是 val。 * 添加 open 让类可以被继承 */ open class Person { var name = "" var age = 0 fun eat() { println(name + " is eating. He is " + age + " years old") } } /** * Kotlin 中的继承与 Java 不同,Java 中使用 extends 关键字, * 在 Kotlin 中使用 : 代替,被继承的类必须要调用它的构造函数, * 否则会报错 */ class Student : Person() { var sno = "" var grade = 0 } 复制代码
在 Kotlin 中每个类都默认自带一个无参的主构造函数(在 Kotlin 中有主构造函数和次构造函数之分),你也可以主动的指明参数,主构造函数是最常用的构造函数,它没有函数体,直接定义在类名后面即可。
class Student(val sno: String, val grade: Int) : Person() {} val student = Student("a123", 5) 复制代码
构造函数的参数直接写在类后面即可,如果想在主构造函数中编写一些逻辑的话,可以使用 init
声明结构体,
class Student(val sno: String, val grade: Int) : Person() { // 将主构造函数的逻辑写在 init 结构体中 init { println("sno is " + sno) println("grade is " + grade) } } val student = Student("a123", 5) 复制代码
这样书写后可以在初始化 Student
类时打印 sno
和 grade
的值,这里的一个规范与 Java 中相同,就是在初始化子类时必须调用父类的构造函数。但是这么写会调用父类的哪个构造方法呢,这取决于 Person()
中的括号中有几个参数,这里没有传入参数,所以会调用父类的无参构造函数。
将 Person 和 Student 的构造函数进行一下修改
open class Person(val name: String, val age: Int) { } class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) { } val student = Student("a123", 5, "Jack", 19) 复制代码
注意:在 Student 的主构造函数中添加 name 和 age 字段时,不能再将它们声明为 val,因为在主构造函数中声明成 val 或者 var 的参数会自动成为该类的字段,这回导致和父类中同名的 name 和 age 字段造成冲突,因此在这里的 name 和 age 前面不需要加任何关键字,让它的作用域仅限定在主构造函数中即可。
Kotlin 提供了一个给函数设定参数默认值的功能,基本上可以替代次构造函数的作用,但是考虑到知识结构的完整性,还是说一下此构造函数的相关知识并探讨一下括号的问题在次构造函数上的区别。
一个类只能有一个主构造函数,但是可以有多个次构造函数,次构造函数也可以用于实例化一个类,这一点和主构造函数没有什么不同,只不过它有函数体。
Kotlin 规定,当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用),这里通过一个例子进行简单的阐明。
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) { constructor(name: String, age: Int) : this("", 0, name, age) { } constructor() : this("", 0){ } } 复制代码
这里定义了两个次构造函数,第一个次构造函数接收 name 和 age 参数,然后又通过 this
调用主构造函数,并将 sno
和 grade
参数赋值,第二个次构造函数不接收任何参数,通过 this 调用了上面的次构造函数,并将 name
和 age
参数也成功进行了赋值,由于第二个次构造函数间接的调用了主构造函数,所以这也是合法的。
这么写完之后就拥有了三种初始化 Student 类的方式
val student1 = Student() val student2 = Student("Jack", 19) val student3 = Student("a123", 5, "Jack", 19) 复制代码
在一个类中显式的设置了次构造函数并且没有显式的设置主构造函数,此时是没有主构造函数的,这种操作在 Kotlin 中是允许的。
class Student : Person { constructor(name: String, age: Int) : super(name, age) { } } 复制代码
这里的 Student 类的后面没有显式的定义主构造函数,同时又因为定义了次构造函数,所以现在 Student 类是没有主构造函数的,那么在继承 Person 类是就不需要再添加括号了,另外由于没有主构造函数,次构造函数只能显式的调用父类的构造函数,所以可以将 this
换成 super
。
Kotlin 中的接口和 Java 几乎完全一样,我们都知道 Java 是单继承结构的语言,任何一个类最多只能继承一个父类,但是却可以实现多个接口,Kotlin 也是如此。我们可以定义一系列抽象行为,然后由具体的类去实现。下面还是通过代码进行演示。
interface Study { fun readBooks() fun doHomework() } 复制代码
让 Student 类实现 Study 接口
class Student(name: String, age: Int) : Person(name, age), Study { override fun readBooks() { println(name + " is reading.") } override fun doHomework() { println(name + " is doing homework") } } 复制代码
在 Java 中实现接口使用 implements
关键字,在 Kotlin 中无论是继承还是实现接口都是用 “:” 替代,中间使用逗号(,)隔开即可,另外在实现接口时不需要在接口后面加括号,因为接口没有构造函数。
在 main 方法中调用方法
fun main() { val student = Student("Jack", 19) doStudy(student) } fun doStudy(study: Study) { study.readBooks() study.doHomework() } 复制代码
interface Study { fun readBooks() fun doHomework() { println("do homework default implementation.") } } 复制代码
如果像之前那么写,在实现接口时里面的两个方法都必须实现,如果改成这样的话,只需要强制实现 readBooks()
函数了,doHomework()
可以选择写或者不写,不写的话则会打印do homework default implementation.
。
在 Java 中一共由 public、private、protected、default(什么都不写)这四种修饰符,在 Kotlin 中有 public、private、protected、internal 这四种修饰符,想要使用那种修饰符时直接将修饰符写在 fun 前面即可。
首先 private 修饰符在两种语言中的作用一模一样,都表示只对当前类内部可见,public 修饰符的作用也是一致的,标识对所有类可见,但是在 Kotlin 中 public 修饰符是默认项,而在 Java 中是 default,前面书写的函数都没有加访问修饰符,那么这些函数的访问权限全部是 public。protected 在 Java 中表示对当前类,子类和同一个包路径下的类可见,在 Kotlin 中则表示只对当前类和子类可见。Kotlin 抛弃了 Java 中的 default 可见性(同一包路径下的类可见)。引入了一种新的可见性概念,只对同一模块中的类可见,使用的是 internal
修饰符。
比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将函数声明为 internal
修饰的。
Java 和 Kotlin 可见性修饰符对照表
在一个规范的系统中,数据类通常占据者非常重要的角色,它们用于将服务器端或数据库中的数据映射到内存中,为编程逻辑提供数据模型的支持。其中常用的 MVC、MVP、MVVM 这些架构模式中的 M 值得就是数据类。
在 Java 中数据类需要重写 equals()
、hashCode()
、toString()
方法,其中equals()
用于判断两个数据类是否相等,hashCode()
和 equals()
方法配套使用,toString()
方法可以让输出打印更加清晰。
public class Cellphone { String brand; double price; public Cellphone(String brand, double price) { this.brand = brand; this.price = price; } @Override public boolean equals(Object obj) { if (obj instanceof Cellphone) { Cellphone other = (Cellphone) obj; return other.brand.equals(brand) && other.price == price; } return false; } @Override public int hashCode() { return brand.hashCode() + (int) price; } @Override public String toString() { return "Cellphone(brand=" + brand + ", price" + price + ")"; } } 复制代码
data class Cellphone(val brand: String, val price: Double) 复制代码
在 Kotlin 中只需要这一行代码即可,其中神奇的地方在于 class 前面的 data 关键字,有了这个关键字就表明我们想要声明一个数据类,Kotlin 会根据主构造函数中的参数帮你将 equals()
、hashCode()
、toString()
方法自动生成,从而减少了开发的工作量。
编写 main 函数进行测试
fun main() { val cellphone1 = Cellphone("Samsung", 1299.99) val cellphone2 = Cellphone("Samsung", 1299.99) println(cellphone1) println("cellphone1 equals cellphone2 " + (cellphone1 == cellphone2)) } 复制代码
注意:如果将 class 前面的 data 去掉,那么它们的返回值就会变为 false。
在讲解单例类之前先说一下 Java 中的单例模式,单例模式主要是为了防止为一个对象创建多个实例,在 Kotlin 中如果想实现类似功能可以使用单例类
public class Singleton { private static Singleton INSTANCE = null; private Singleton() {} public synchronized static Singleton getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton(); } return INSTANCE; } public void singletonTest() { System.out.println("singletonTest is called."); } } 复制代码
object Singleton { fun singletonTest() { println("singletonTest is called.") } } 复制代码
在 Kotlin 中实现单例要比 Java 中简单的多,只需要使用 object
关键字即可,在这其中 Kotlin 帮我们创建了一个 Singleton 类的实例,并且保证全局只存在一个 Singleton 实例。
在 JDK1.8 中引入了 lambda 表达式,实现相同的功能时 lambda 表达式写法会使用更少的代码,从而提升开发效率。在 Kotlin 中也有 lambda 表达式,下面将对此进行介绍。
现在有一个需求,创建一个包含许多水果名称的集合,如果在 Java 中会创建一个 ArrayList 然将水果的名称一个个的添加进集合中,当然在 Kotlin 中也可以这么做。
fun main() { val list = ArrayList<String>() list.add("Apple") list.add("Banana") list.add("Orange") list.add("Pear") list.add("Grape") } 复制代码
数据少的时候这么写一点问题都没有,但是问题在于数据量多的时候这么写就会显得很罗嗦,所以可以使用 Kotlin 中内置的 listOf()
函数来简化初始化集合的写法,写法如下:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape") for (fruit in list) { println(fruit) } 复制代码
注意:在这里使用 listOf()
函数创建的是一个不可变的集合。在 Java 中没有不可变的集合,但是在 Kotlin 中不可变的集合指的是,该集合中的元素只能用于读取,不能进行添加、修改或者删除。
这么设计的理由和 val、类默认不可继承是一样的,可见 Kotlin 在不可变性方面的控制及其严格。那么如果我们确实需要创建一个可变的集合,可以使用mutableListOf()
函数即可。
val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape") list.add("Watermelon") for (fruit in list) { println(fruit) } 复制代码
前面介绍的 List 集合的用法其实和 Set 一模一样,只需要将创建集合的方法换成 setOf()
和 mutableSetOf()
即可。
val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape") for (fruit in set) { println(fruit) } println("==========================") val mutableSet = mutableSetOf("Apple", "Banana", "Orange", "Pear", "Grape") mutableSet.add("Watermelon") for (fruit in mutableSet) { println(fruit) } 复制代码
接下来讲解的 Map 和前面的 List 和 Set 有很大的不同,传统的 Map 用法是先创建一个 HashMap 的实例,然后将一个个的键值对添加到 Map 中,比如给每个水果一个对应的编号。
val map = HashMap<String, Int>() map.put("Apple", 1) map.put("Banana", 2) map.put("Orange", 3) map.put("Pear", 4) map.put("Grape", 5) 复制代码
这种写法与 Java 中的写法相似,但是在 Kotlin 中并不建议使用 put()
和 get()
方法对 Map 进行添加和读取操作,而是更加建议使用一种类似于数组下标的语法结构,比如向 Map 中添加一条数据可以这么写:
map["Apple"] = 1 复制代码
从 Map 中读取一条数据可以这么写
val number = map["Apple"] 复制代码
因此可以将代码优化为一下形式
val map = HashMap<String, Int>() map["Apple"] = 1 map["Banana"] = 2 map["Orange"] = 3 map["Pear"] = 4 map["Grape"] = 5 复制代码
这样的写法也不是最简便的,在 Kotlin 中提供了一个 mapOf()
和 mutableMapOf()
函数来继续简化 Map 的用法。在 mapOf()
函数中,我们可以直接传入初始化的键值对组合来完成对 Map 集合的创建:
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5) // for (entry in map) { // println(entry.key + "\t" + entry.value) // } for ((fruit, number) in map) { println("fruit is " + fruit + ", number is " + number) } 复制代码
需求:如何在一个水果集合中找到单词最长的哪个水果?
val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape") var maxLengthFruit = ""; for (fruit in list) { if (fruit.length > maxLengthFruit.length) { maxLengthFruit = fruit } } println("max length fruit is " + maxLengthFruit) 复制代码
val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape") val maxLengthFruit = list.maxBy { it.length } println("max length fruit is " + maxLengthFruit) 复制代码
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体} 复制代码
这是 Lambda 表达式最完整的语法结构定义,首先最外层是一对大括号,如果有参数传入到 Lambda 表达式中的话,还需要声明参数列表,参数列表的结尾使用 ->
符号,表示参数列表的结束以及函数体的开始,函数体中可以编写任意行代码,并且最后一行代码自动作为返回值。
最初写法
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val lambda = { fruit: String -> fruit.length } val maxLengthFruit = list.maxBy(lambda) 复制代码
简化版本1
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length }) 复制代码
简化版本2
Kotlin 规定当函数的最后一个参数是 Lambda 时,可以将 Lambda 表达式写在最外面.
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length } 复制代码
简化版本3
当 Lambda 参数是函数的唯一一个参数的话,可以省略函数的括号。
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val maxLengthFruit = list.maxBy { fruit: String -> fruit.length } 复制代码
简化版本4
由于 Kotlin 的推导机制,Lambda 的参数列表在大多数情况下不必声明参数类型,因此代码可以进一步简化。
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val maxLengthFruit = list.maxBy { fruit -> fruit.length } 复制代码
简化版本5
当 Lambda 表达式的参数列表中只有一个参数时,可以不必声明参数名,可以用 it
代替。
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val maxLengthFruit = list.maxBy { it.length } 复制代码
集合中的 map 函数时最常用的一种函数式 API,它用于将集合中的每一个元素都映射成一个另外的值,映射的规则在 Lambda 表达式中指出,最终生成一个新的集合。
需求:让所有的水果命都变成大写模式
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val newList = list.map { it.toUpperCase() } for (fruit in newList) { println(fruit) } 复制代码
filter 函数是用来过滤集合中的数据的,它可以单独使用。
需求:只保留集合中字符长度大于5的水果名,并将符合条件的水果名转换为大写
val list = listOf("Apple", "Orange", "Pear", "Grape", "Watermelon") val newList = list.filter { it.length <= 5 }.map { it.toUpperCase() } for (fruit in newList) { println(fruit) } 复制代码
在这个例子中如果先调用 map()
再调用 filter()
也是可以的,但是效率会有影响,因为这么做会让转换的次数增加。
any 函数用于判断集合种是否至少存在一个元素满足指定条件,all 函数用于判断集合中是否所有元素都满足给定条件。
val list = listOf("Apple", "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) 复制代码
如果我们再 Kotlin 代码中调用了一个 Java 方法,并且该方法接收一个 Java 单抽象方法接口参数,就可以使用函数式 API。
Java 中
@FunctionalInterface public interface Runnable { public abstract void run(); } 复制代码
对于任何一个 Java 方法,只要它接收 Runnable
参数,就可以使用函数时 API。不过 Runnable
接口主要还是结合线程来一起使用的,因此这里就通过 Java 的线程类 Thread
进行学习。
new Thread(new Runnable() { @Override public void run() { System.out.println("Thread is running."); } }).start(); 复制代码
Kotlin 中
Thread(object : Runnable { override fun run() { println("Thread is running.") } }).start() 复制代码
与 Java 写法不同的是,Kotlin 中使用 object
关键字代替了 new
关键字。
简化1
Thread(Runnable { println("Thread is running.") }).start() 复制代码
由于 Runnable
接口中只有一个方法,所以没有手动实现的话,Kotlin 就会推导出 Lambda 表达式里要写的是 run()
方法中的内容。
简化2
由于 Java 方法的参数列表中不存在一个以上 Java 单抽象方法接口参数,所以可以将接口名省略。
Thread({ println("Thread is running.") }).start() 复制代码
简化3
由于 Lambda 中只有一个参数,所以可以将括号花括号内的内容移动到外面,并且还可以将函数的括号省略,所以简写成如下形式:
Thread { println("Thread is running.") }.start() 复制代码
总结:本小节学习的 Java 函数式 API 的使用都现定于 Kotlin 中调用 Java 方法,并且单抽象方法接口也必须是用 Java 语言定义的,这么设计是因为 Kotlin 中有专门的高阶函数来实现更加强大的自定义函数式 API 功能,从而不需要像 Java 这样借助单抽象方法接口来实现。
Java 程序在运行时遇到空指针异常导致运行崩溃的例子数不胜数,究其原因是因为空指针异常时一种运行时异常,需要开发者手动进行检测。
public void doStudy(Study study) { study.readBooks(); study.doHomework(); } 复制代码
以上的代码就很有可能出现空指针异常,具体能否出现完全要看传入的 study
是否为空,为
了避免空指针异常的发生,通常都会做如下操作:
public void doStudy(Study study) { if (study != null) { study.readBooks(); study.doHomework(); } } 复制代码
这只是一小段代码,如果在一个比较大的工程中要想完全避免空指针异常并不现实。
Kotlin 就很科学的解决了这个问题,它利用编译时判空检查的机制几乎杜绝了空指针异常。虽然编译时判空检查的机制会导致代码变得比较难写,但是不用担心,Kotlin 提供了一整套辅助工具,让我们可以轻松的完成判空任务。
fun doStudy(study: Study) { study.readBooks() study.doHomework() } 复制代码
这段代码看上去和 Java 的没有什么区别,但是在 Kotlin 中所有参数和变量都不能为空,所以这段代码不可能出现空指针。
经过 Kotlin 的检测,避免了所有对象为空的可能,但是有时候就是需要传入空对象,这该怎么办呢?
Kotlin 提供了一套可为空的类型系统,只不过在使用可为空的类型系统时,我们需要在编译时期就将所有潜在的空指针异常处理掉。
使用可为空类型的系统时只需要在类型参数后面添加一个 ?
即可,例如
当对象不为空时进行正常调用,为空就什么都不做
传统写法:
fun doStudy(study: Study?) { if (study != null) { study.readBooks() study.doHomework() } } 复制代码
优化写法:
fun doStudy(study: Study?) { study?.readBooks() study?.doHomework() } 复制代码
这个操作符两边都接收一个表达式,如果左边表达式的结果不为空就返回左边的结果,否则返回右边的。
传统写法
val c = if (a != null) { a } else { b } 复制代码
优化写法
val c = a ?: b 复制代码
需求:编写一个函数用来获得一段文本的长度
传统写法:
fun getTextLength(text: String?): Int { if (text != null) { return text.length } return 0 } 复制代码
优化写法:
fun getTextLength(text: String?) = text?.length ?: 0 复制代码
Kotlin 有的时候也不很智能,比如已经做了非空判断,但是调用时依然无法通过编译,那么此时可以使用非空断言工具!!。
即可。
注意:这种写法存在风险,这样写意在告诉 Kotlin,我这里一定不为空,如果为空后果我自己承担。
let 函数提供了函数式 API 的编程接口,并将原始调用对象作为参数传递到 Lambda 表达式中。
obj.let { obj2 -> // 编写具体的业务逻辑 } 复制代码
可以看到这里调用了 obj
对象的 let
函数,然后 Lambda 表达式中的代码就会立即执行,并且这个 obj
对象本身还会作为参数传递到 Lambda 表达式中。不过为了防止变量重名,我将 obj
改为了 obj2
,但是它们是同一个对象。
使用 let 函数配合 ?. 操作符检查空指针
原代码
fun doStudy(study: Study?) { study?.readBooks() study?.doHomework() } 复制代码
这种写法与传统的 if
判断的写法的区别在于使用 ?.
替代了 if
,但是这里要调用的方法很多的话就需要写多次 ?.
,这种重复的操作就可以使用 let
函数配合解决。
优化版本1:
fun doStudy(study: Study?) { study?.let { stu -> stu.readBooks() stu.doHomework() } } 复制代码
这样会在对象不为空时调用 let
函数,并且只需要写一遍 ?.
。
优化版本2:
在 Kotlin 中,Lambda 表达式如果只有一个参数,可以省略,使用 it
代替。
fun doStudy(study: Study?) { study?.let { it.readBooks() it.doHomework() } } 复制代码
使用字符串表达式再也不需要傻傻的拼接 字符串了,在 Kotlin 中,可以直接使用字符串内嵌表达式,即使是非常复杂的字符串也可以轻而易举地完成。
"hello, ${obj.name}. nice to meet you!" 复制代码
在 Kotlin 中允许我们在字符串里嵌入 ${}
这种语法结构的表达式,并在运行时使用表达式的执行结果替代这一部分的内容。另外,当表达式中只有一个变量的时候,可以直接使用 $name
的形式进行简写,无需添加花括号了。
val brand = "Samsung" val price = 1299.00 println("Cellphone(brand=$brand, price=$price)") // 使用字符串表达式 println("Cellphone(brand = "+ brand +", price = " + price + ")") // 不使用 复制代码
前面学习次构造函数的用法时提到过,次构造函数在 Kotlin 中很少使用,因为 Kotlin 提供了给函数设定参数默认值的功能,它在很大程度上能够替代次构造函数的作用。
具体来讲,我们可以在定义函数的时候给任意参数设定一个默认值,这样当调用此函数时就不会强制要求调用方为此参数传值,在没有传值的情况下会自动使用参数的默认值。
fun printParams(num: Int, str: String = "hello") { println("num is $num, str is $str") } printParams(1) printParams(1, "哈哈") 复制代码
fun printParams(num: Int = 100, str: String) { println("num is $num, str is $str") } 复制代码
如果我们想为 num
设置默认值,只传字符串的参数值的话,像上面那么写就会报错了
解决:将传递的参数指定参数名
fun printParams(num: Int = 100, str: String) { println("num is $num, str is $str") } printParams(str = "world") 复制代码
原来的代码
class Student(val sno: String, val grade: Int, name: String, age: Int) : Person(name, age) { constructor(name: String, age: Int) : this("", 0, name, age) { } constructor() : this("", 0){ } } 复制代码
这个构造函数的功能主要就是在调用无参构造函数时会对两个参数的构造函数进行调用,并赋初始值,两个参数的构造函数会调用四个参数的构造函数,并赋初始值,这完全可以使用函数默认值的方式进行替代。
优化后的代码
class Student(val sno: String = "", val grade: Int = 0, name: String = "", age: Int = 0) : Person(name, age) { } 复制代码