资源来自《第一行代码》(第三版)第二章
网络试读: https://www.ituring.com.cn/book/tupubarticle/30209
以下为笔记
val(value的简写)用来声明一个不可变的变量,这种变量在初始赋值之后就再也不能重新赋值,对应Java中的final变量。
var(variable的简写)用来声明一个可变的变量,这种变量在初始赋值之后仍然可以再被重新赋值,对应Java中的非final变量。
val a = 10
var b = 20
变量类型通过类型推导机制来判断,但也可以显式地声明变量类型
val a: Int = 10
Kotlin完全抛弃了Java中的基本数据类型,全部使用了对象数据类型。在Java中int是关键字,而在Kotlin中Int变成了一个类,它拥有自己的方法和继承结构。
fun main() { var a: Int = 10 a = a * 10 println("a = " + a) }
fun largerNumber(num1: Int, num2: Int): Int { return max(num1, num2) }
① fun(function的简写)是定义函数的关键字,无论你定义什么函数,都一定要使用fun来声明。
② 括号里面可以声明该函数接收什么参数,参数的数量可以是任意多个
③ 参数括号后面的部分是可选的,用于声明该函数会返回什么类型的数据。如果你的函数不需要返回任何数据,这部分可以直接不写。
特别注意:当一个函数中只有一行代码时,Kotlin允许我们不必编写函数体,可以直接将唯一的一行代码写在函数定义的尾部,中间用等号连接即可。
fun largerNumber(num1: Int, num2: Int): Int = max(num1, num2)
通过类型推导机制,可以进一步简化
fun largerNumber(num1: Int, num2: Int) = max(num1, num2)
3.1 if 条件语句
fun largerNumber(num1: Int, num2: Int): Int { var value = 0 if (num1 > num2) { value = num1 } else { value = num2 } return value }
特别注意:Kotlin 中的 if 语句相比于 Java 有一个额外的功能,它是可以有返回值的,返回值就是 if 语句每一个条件中最后一行代码的返回值。
所以可以精简为:
fun largerNumber(num1: Int, num2: Int): Int { val value = if (num1 > num2) { num1 } else { num2 } return value } //因为 value 值只等于返回值,没有变化。所以可以用 val 关键字来声明 value 变量
可以再次精简:
fun largerNumber(num1: Int, num2: Int): Int { return if (num1 > num2) { num1 } else { num2 } }
虽然此函数不止只有一行代码,但是它和只有一行代码的作用是相同的,只是返回了一下 if 语句的返回值。所以可以用语法糖再次精简:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) { num1 } else { num2 }
还可以再精简:
fun largerNumber(num1: Int, num2: Int) = if (num1 > num2) num1 else num2
3.2 when 条件语句
当判断条件非常多的时候,就应该考虑使用when语句
when语句也可以接收返回值
fun getScore(name: String) = when (name) { "Tom" -> 86 // 匹配值 -> { 执行逻辑 } "Jim" -> 77 "Jack" -> 95 "Lily" -> 100 else -> 0 }
when语句还允许进行类型匹配,这个函数就可以使用类型匹配来判断传入的参数到底属于什么类型。
fun checkNumber(num: Number) { when (num) { is Int -> println("number is Int") is Double -> println("number is Double") else -> println("number not support") } }
fun main() { val num = 10 checkNumber(num) }
is关键字就是类型匹配的核心,它相当于 Java 中的 instanceof 关键字。checkNumber()函数接收一个Number类型的参数。Number 是 Kotlin 内置的一个抽象类,像 Int、Long、Float、Double 等与数字相关的类都是它的子类。
不带参数的用法:(Kotlin中判断字符串或对象是否相等可以直接使用==关键字)
fun getScore(name: String) = when { name.startsWith("Tom") -> 86 // 当一个分支的条件为真时则执行该分支 name == "Jim" -> 77 name == "Jack" -> 95 name == "Lily" -> 100 else -> 0 }
3.3 循环语句 for
创建一个 0 到 10 的闭区间 [0, 10]
val range = 0..10 // 0-10
使用 until 关键字来创建一个左闭右开的区间 [0, 10)
val range = 0 until 10 // 0-9
使用downTo关键字创建一个降序的区间
有了区间之后,就可以通过for-in循环来遍历这个区间:
fun main() { for (i in 0..10) { println(i) } }
fun main() { for (i in 0 until 10 step 2) { // step 2 相当于 i = i + 2 println(i) } }
fun main() { for (i in 10 downTo 1) { // 打印 10 到 1,也是可以结合step关键字跳过区间中的一些元素 println(i) } }
3.4 循环语句 while
和 Java、C 等类似
Kotlin中也是使用class关键字来声明一个类
class Person { var name = "" // 使用 val 关键字的话,初始化之后就不能再重新赋值了 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中非抽象类,默认是不可以被继承的
第①步: 先让父类可以被继承
open class Person { ... }
第②步: 让子类继承父类
class Student : Person() { var sno = "" var grade = 0 }
(一)Kotlin将构造函数分成了两种:主构造函数和次构造函数。
每个类默认都会有一个不带参数的主构造函数(也可以显式地给它指明参数)
class Student(val sno: String, val grade: Int) : Person() { } // 这里子类的主构造函数有两个参数 sno、grade
在对类进行实例化的时候,必须传入构造函数中要求的所有参数。比如:
val student = Student("a123", 5) // 可以将参数声明成 val,是因为构造函数中的参数是在创建实例的时候传入的,不需要重新赋值。
如果想在主构造函数中编写一些逻辑,Kotlin 给我们提供了一个 init 结构体,所有主构造函数中的逻辑都可以写在里面:
class Student(val sno: String, val grade: Int) : Person() { init { println("sno is " + sno) println("grade is " + grade) } }
(注意:Java、Kotlin 都有一个原则:子类的构造函数必须调用父类的构造函数。)
(二)Kotlin 中,子类的主构造函数,调用父类中的哪个构造函数,在继承的时候通过括号的内容来指定。
class Student(val sno: String, val grade: Int) : Person() { // 调用的是父类的默认无参构造函数 }
如果父类构造函数是有参数的
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)
(三)任何一个类只能有一个主构造函数,但可以有多个次构造函数。当一个类既有主构造函数又有次构造函数时,所有的次构造函数都必须调用主构造函数(包括间接调用)
次构造函数是通过constructor关键字来定义的
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(0)这两个参数
第二个次构造函数不接收任何参数,它通过 this 关键字调用了第一个次构造函数,并初始化 name("")和 age(0)这两个参数,由于第二个次构造函数间接调用了主构造函数(第一个次构造函数直接调用了主构造函数),因此这仍然是合法的。
实际使用
val student3 = Student("a123", 5, "Jack", 19) // 通过主构造函数
val student2 = Student("Jack", 19) // 通过第一个次构造函数
val student1 = Student() // 通过第二个次构造函数
(三)特殊情况
当一个类没有显式地定义主构造函数,且定义了次构造函数时,它就是没有主构造函数的。
class Student : Person { // 既然没有主构造函数,继承 Person 类的时候就不需要加括号 constructor(name: String, age: Int) : super(name, age) { } }
由于没有主构造函数,次构造函数只能直接调用父类的构造函数,上述代码也是将 this 关键字换成了 super 关键字。
(New→Kotlin File/Class,在弹出的对话框中输入“Study”,创建类型选择“Interface”。)
Java 中继承使用的关键字是 extends,实现接口使用的关键字是 implements;Kotlin 中统一使用冒号,中间用逗号进行分隔。
Student 类继承了 Person 类,同时还实现了 Study 接口:
interface Study { fun readBooks() fun doHomework() }
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.") } }
Study 接口中定义了 readBooks() 和 doHomework() 这两个待实现函数,因此 Student 类必须实现这两个函数。Kotlin中 使用 override 关键字来重写父类或者实现接口中的函数
fun main() { val student = Student("Jack", 19) // 创建了一个 Student 类的实例 doStudy(student) // 将它传入到了 doStudy() 函数中 } fun doStudy(study: Study) { // doStudy() 函数接收一个 Study 类型的参数,由于 Student 类实现了 Study 接口,因此 Student 类的实例是可以传递给 doStudy() 函数的。 study.readBooks() study.doHomework() // 调用了 Study 接口的 readBooks() 和 doHomework() 函数,这种就叫作面向接口编程,也可以称为多态。 }
特殊:对接口中的函数进行默认实现
interface Study { fun readBooks() fun doHomework() { println("do homework default implementation.") } }
如果接口中的一个函数拥有了函数体,这个函数体中的内容就是它的默认实现。现在当一个类去实现 Study 接口时,只会强制要求实现 readBooks() 函数,而 doHomework() 函数则可以自由选择实现或者不实现,不实现时就会自动使用默认的实现逻辑。
Kotlin中public修饰符是默认项,而在Java中default才是默认项
Kotlin抛弃了Java中的default可见性(同一包路径下的类可见),引入了一种新的可见性概念,只对同一模块中的类可见,使用的是internal修饰符。比如我们开发了一个模块给别人使用,但是有一些函数只允许在模块内部调用,不想暴露给外部,就可以将这些函数声明成internal。
数据类:
当在一个类前面声明了 data 关键字时,就表明你希望这个类是一个数据类。Kotlin 会根据主构造函数中的参数帮你将 equals()、hashCode()、toString() 等固定且无实际逻辑意义的方法自动生成,从而大大减少了开发的工作量。
data class Cellphone(val brand: String, val price: Double)
实际应用:
fun main() { val cellphone1 = Cellphone("Samsung", 1299.99) val cellphone2 = Cellphone("Samsung", 1299.99) println(cellphone1) println("cellphone1 equals cellphone2 " + (cellphone1 == cellphone2)) // 判断这两个对象是否相等,返回 true }
单例类:
Java 中:
public class Singleton { private static Singleton instance; private Singleton() {} public synchronized static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } public void singletonTest() { System.out.println("singletonTest is called."); } }
在 getInstance() 方法中,我们判断如果当前缓存的 Singleton 实例为 null,就创建一个新的实例,否则直接返回缓存的实例即可,这就是单例模式的工作机制。
实际应用:
Singleton singleton = Singleton.getInstance(); singleton.singletonTest();
Kotlin 中:
(New→Kotlin File/Class,在弹出的对话框中输入“Singleton”,创建类型选择“Object”)
object Singleton { fun singletonTest() { println("singletonTest is called.") } }
实际应用:
Singleton.singletonTest()
这种写法虽然看上去像是静态方法的调用,但其实Kotlin在背后自动帮我们创建了一个Singleton类的实例,并且保证全局只会存在一个Singleton实例。
7.1 集合的创建与遍历
List、Set、Map 在 Java 中都是接口,List 的主要实现类是 ArrayList 和 LinkedList,Set 的主要实现类是 HashSet,Map 的主要实现类是 HashMap
(一)List 集合
listOf()函数创建的是一个不可变的集合,只能读取,不能添加、修改、删除。
创建:
①
val list = ArrayList
②
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape")
mutableListOf()函数创建的是可变的集合。
fun main() { val list = mutableListOf("Apple", "Banana", "Orange", "Pear", "Grape") // 创建 list.add("Watermelon") // 增加 for (fruit in list) { // 遍历 println(fruit) } }
(三)Set 集合:用法几乎与 List 集合一模一样,只是将创建集合的方式换成了setOf() 和 mutableSetOf() 函数。Set 集合底层是使用 hash 映射机制来存放数据的,因此集合中的元素无法保证有序,这是和 List 集合最大的不同之处。
创建:
val set = setOf("Apple", "Banana", "Orange", "Pear", "Grape")
遍历:
for (fruit in set) { println(fruit) }
(四)Map集合:mapOf() 和 mutableMapOf() 函数
创建:
①
val map = HashMap
②
val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5)
// to 不是关键字,而是一个 infix 函数
读取:
val number = map["Apple"]
遍历:
fun main() { val map = mapOf("Apple" to 1, "Banana" to 2, "Orange" to 3, "Pear" to 4, "Grape" to 5) for ((fruit, number) in map) { println("fruit is " + fruit + ", number is " + number) } }
7.2 集合的函数式 API
Lambda表达式的语法结构:
{参数名1: 参数类型, 参数名2: 参数类型 -> 函数体}
参数列表的结尾使用一个->符号,表示参数列表的结束以及函数体的开始。
函数体中可以编写任意行代码(虽然不建议编写太长的代码),并且最后一行代码会自动作为Lambda表达式的返回值。
举例:在一个水果集合里面找到单词最长的那个水果
第一步:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon") var maxLengthFruit = "" for (fruit in list) { if (fruit.length > maxLengthFruit.length) { maxLengthFruit = fruit } } println("max length fruit is " + maxLengthFruit)
第二步:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon") val lambda = { fruit: String -> fruit.length } val maxLengthFruit = list.maxBy(lambda)
// maxBy 函数的工作原理是根据我们传入的条件来遍历集合,从而找到该条件下的最大值。比如说想要找到单词最长的水果,那么条件自然就应该是单词的长度了。
// maxBy 接收的是一个 Lambda 类型的参数,并且会在遍历集合时,将每次遍历的值作为参数传递给 Lambda 表达式
第二、三行可简化为:
val maxLengthFruit = list.maxBy({ fruit: String -> fruit.length })
Kotlin 规定,当 Lambda 参数是函数的最后一个参数时,可以将 Lambda 表达式移到函数括号的外面,继续简化:
val maxLengthFruit = list.maxBy() { fruit: String -> fruit.length }
Kotlin 规定,当 Lambda 参数是函数的唯一一个参数的话,还可以将函数的括号省略,继续简化:
val maxLengthFruit = list.maxBy { fruit: String -> fruit.length }
Kotlin 有出色的类型推导机制,Lambda 表达式中的参数列表在大多数情况下不必声明参数类型,继续简化:
val maxLengthFruit = list.maxBy { fruit -> fruit.length }
当Lambda表达式的参数列表中只有一个参数时,也不必声明参数名,可以使用it关键字来代替,继续简化:
val maxLengthFruit = list.maxBy { it.length }
第三步:
val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon") val maxLengthFruit = list.maxBy { it.length } println("max length fruit is " + maxLengthFruit)
举例2:所有的水果名都变成大写模式
fun main() { val list = listOf("Apple", "Banana", "Orange", "Pear", "Grape", "Watermelon") val newList = list.map { it.toUpperCase() } for (fruit in newList) { println(fruit) } }
// map函数的功能非常强大,它可以按照我们的需求对集合中的元素进行任意的映射转换。你还可以将水果名全部转换成小写,或者是只取单词的首字母,甚至是转换成单词长度这样一个数字集合,只要在Lambda表示式中编写你需要的逻辑即可。
举例3:只想保留5个字母以内的水果(借助 filter 函数实现)
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) } }
举例3:any和all函数,其中any函数用于判断集合中是否至少存在一个元素满足指定条件,all函数用于判断集合中是否所有元素都满足指定条件。
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) }
// any函数就表示集合中是否存在5个字母以内的单词,而all函数就表示集合中是否所有单词都在5个字母以内。打印结果是 true 和 false
集合中还有许多其他函数式API,可查阅文档掌握。
7.3 Java函数式API的使用
如果我们在Kotlin代码中调用了一个Java方法,并且该方法接收一个Java单抽象方法接口参数,就可以使用函数式API。
Java单抽象方法接口指的是接口中只有一个待实现方法,如果接口中有多个待实现方法,则无法使用函数式API。
8.1 常规写法
用 Java 写:
public void doStudy(Study study) { study.readBooks(); study.doHomework(); }
这段代码安全吗?不一定,因为这要取决于调用方传入的参数是什么,如果我们向doStudy()方法传入了一个null参数,那么毫无疑问这里就会发生空指针异常。因此,更加稳妥的做法是在调用参数的方法之前先进行一个判空处理,如下所示:
public void doStudy(Study study) { if (study != null) { study.readBooks(); study.doHomework(); } }
这样就能保证不管传入的参数是什么,这段代码始终都是安全的。
用 Kotlin 写:
fun doStudy(study: Study) { study.readBooks() study.doHomework() }
Kotlin默认所有的参数和变量都不可为空,所以这里传入的Study参数也一定不会为空,我们可以放心地调用它的任何函数。
如果我们就是需要某个参数或者变量为空,就需要使用可为空的类型系统:在类名的后面加上一个问号。比如,Int 表示不可为空的整型,而 Int? 就表示可为空的整型;String 表示不可为空的字符串,而 String? 就表示可为空的字符串。
fun doStudy(study: Study?) { if (study != null) { study.readBooks() study.doHomework() } }
8.2 判空辅助工具
1、 ?.
fun doStudy(study: Study?) { study?.readBooks() study?.doHomework() }
// 当对象不为空时,正常调用相应的方法;当对象为空时,则什么都不做。
2、?:
举例:获得一段文本的长度
fun getTextLength(text: String?): Int { if (text != null) { return text.length } return 0 }
简化为:
fun getTextLength(text: String?) = text?.length ?: 0
3、!! (非空断言工具)慎用
这是一种有风险的写法,意在告诉 Kotlin,我非常确信这里的对象不会为空,所以不用你来帮我做空指针检查了,如果出现问题,你可以直接抛出空指针异常,后果由我自己承担。
4、 let 函数
fun doStudy(study: Study?) { study?.readBooks() study?.doHomework() }
可以少打几个 ?. 简化为:
fun doStudy(study: Study?) { study?.let { stu -> stu.readBooks() stu.doHomework() } }
当Lambda表达式的参数列表中只有一个参数时,可以不用声明参数名,直接使用it关键字来代替即可:
fun doStudy(study: Study?) { study?.let { it.readBooks() it.doHomework() } }
9.1 字符串内嵌表达式
"hello, ${obj.name}. nice to meet you!"
当表达式中仅有一个变量的时候,还可以将两边的大括号省略
"hello, $name. nice to meet you!"
举例:
val brand = "Samsung" val price = 1299.99 println("Cellphone(brand=" + brand + ", price=" + price + ")")
简化为:
val brand = "Samsung" val price = 1299.99 println("Cellphone(brand=$brand, price=$price)")
9.2 给函数设定参数默认值
第一种情况:
fun printParams(num: Int, str: String = "hello") { // 第二个参数有默认值,应用是可传可不传 println("num is $num , str is $str") } fun main() { printParams(123) }
第二种情况:
fun printParams(num: Int = 100, str: String) { println("num is $num , str is $str") }
fun main() { printParams(str = "world") // Kotlin 可以通过键值对的方式来传参,顺序无所谓
// printParams(str = "world", num = 123) // 也可以这么写 }
之前的例子
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) { }