到本章為止,kotlin基本的知識點都記錄完畢瞭空。還有關(guān)于一些泛型和反射的知識點后續(xù)會更新上來揪阿,知識點和內(nèi)容來自《Kotlin in Action》這本書。Kotlin陸陸續(xù)續(xù)學(xué)習(xí)了大概半年多吧匙铡,中間接觸了一段時間Flutter和JavaScript图甜。2019年希望自己能學(xué)一下這方面的東西碍粥。與君共勉鳖眼,一起加油。
公司今年新啟動的一個內(nèi)部員工使用的項目就是用Kotlin開發(fā)的嚼摩,為了說服老大和那些管理層钦讳,寫了很多封郵件加很多次單獨的談話。最后才同意我的建議枕面。這里我還是有點私心的愿卒,紙上談兵是很難獲的到收貨和真理的。因為App是不對外開放的潮秘,我也有信心和耐心克服碰見困難琼开。其實并不是拿公司的項目來練手。在學(xué)習(xí)的過程中我就自己寫過一些Demo枕荞。說一下自己在開發(fā)App時使用Kotlin之后的想法吧柜候。Kotlin對可空類型的顯示的支持真的幫助我減少了很多NullPointError搞动,一些安全調(diào)用運(yùn)算符用起來效率高、可讀性高渣刷。lambda表達(dá)式更是隨處可見鹦肿。Kotlin函數(shù)庫讓你覺得處理一些問題真的很簡單和方便。函數(shù)式的編程風(fēng)格讓我用感覺自己才是一個“程序員”辅柴÷崂#總之,我還是很推薦大家去掌握這門語言的碌嘀。
Kotlin基礎(chǔ)
2.1.1 Hello,World!
fun main(args: ArrayList<String>) {
println("Hellow,World!")
}
- 關(guān)鍵字fun用來聲明一個函數(shù)
- 參數(shù)類型是寫在參數(shù)名稱的后面
- 函數(shù)可以寫在類的最外面涣旨,不需要放在類中
- 數(shù)組就是類,Kotlin沒有聲明數(shù)組類型的特殊語法
- println()代替了system.out.println()
- 與許多現(xiàn)代語言一樣股冗,可以省略語句后面的分好
2.1.2 函數(shù)
fun max(a: Int, b: Int): Int {
return if (a > b) a else b
}
注意在Kotlin中开泽,if是有結(jié)果值的表達(dá)式。它和java中的三目運(yùn)算符相似:(a > b) a else b
語句和表達(dá)式
在Kotlin中魁瞪,if是表達(dá)式穆律,而不是語句。語句和表達(dá)的區(qū)別在于导俘,表達(dá)式有值峦耘,并且能作為另一個表達(dá)式的一部分使用;而語句總是包圍著它的代碼塊中的頂層元素旅薄,并且沒有自己的返回值辅髓。在Java中,所有的控制結(jié)構(gòu)都是語句少梁。而在kotlin中洛口,除了循環(huán)(for、while凯沪、do/while)以外大多數(shù)控制結(jié)構(gòu)都是表達(dá)式
表達(dá)式函數(shù)體
fun max2(a: Int, b: Int) = if (a>b) a else b
2.1.3 變量
Java中聲明變量是以類型開始第焰,這種方式在kotlin中是行不通的。因為許多變量類型是可以省略的妨马。所以在kotlin中聲明變量是以關(guān)鍵字開頭挺举,然后是名稱,最后可以加上類型烘跺。也可以省略:
val question = "The Ultimate Question of Life";
val ansewer = 20
如果變量沒有初始化值湘纵,則需要顯示的指示它的類型
val answer: Int
answer = 20;
可變變量和不可變量
- val(來自value) ——不可變量,使用val聲明的變量不能在初始化之后在復(fù)制滤淳,他對應(yīng)的是java中的final
- var(來自variable) —— 可變變量梧喷,這種變量的值可以改變
2.1.4 字符串模板
fun main2(args: ArrayList<String>) {
val name = if (args.size > 0) args[0] else "Kotlin"
println("name:$name")
}
在變量前面加上前綴“”字符,你需要對他轉(zhuǎn)義“\ $x”
2.2 類和屬性
class Person(
val name: String,//val關(guān)鍵字修飾,表示只讀屬性
var isMarried: Boolean//var關(guān)鍵字修飾铺敌,表示可寫屬性绊困,有g(shù)et和set屬性
)
2.3.5 合并類型檢查和轉(zhuǎn)換
//表達(dá)式類層次結(jié)構(gòu)
interface Expr {
class Num(val value: Int) : Expr//簡單的值對象類,只有一個屬性适刀。實現(xiàn)了Expr接口
class Sum(val left: Expr, val right: Expr) : Expr//Sum的實參可以是任意一個Expr:Num或者另一個Sum
}
聲明類的時候秤朗,使用:和后面跟著接口名稱來表示實現(xiàn)了這個接口
Expr接口有兩種實現(xiàn),所以為了計算出表達(dá)的結(jié)果值,需要嘗試兩種選項
- 如果表達(dá)式是一個值笔喉,則直接返回
- 如果是一次求和取视,則則先計算左右兩邊表達(dá)式的值,在求和 代碼如下
fun main(args: Array<String>) {
println(eval(Expr.Sum(Expr.Sum(Expr.Num(1), Expr.Num(2)),Expr.Num(4))))
}
fun eval(e: Expr): Int {
if (e is Expr.Num) {
val n = e as Expr.Num;//顯示的轉(zhuǎn)換類型Num是多余的
return n.value
}
if (e is Expr.Sum) {
return eval(e.left) + eval(e.right)
}
throw IllegalArgumentException("Unknow Expression")
}
interface Expr {
class Num(val value: Int) : Expr
class Sum(val left: Expr, val right: Expr) : Expr
}
2.3.6 重構(gòu)常挚,用“when”代替“if”
使用有返回值的if表達(dá)式
fun eval2(e: Expr): Int =
if (e is Num) {
e.value
} else if (e is Sum) {
eval2(e.left) + eval2(e.right)
} else {
throw IllegalArgumentException("Unknow Expression")
}
用when代替if層疊
fun eval3(e: Expr): Int =
when (e) {
is Num -> e.value
is Sum -> eval3(e.left) + eval3(e.right)
else -> throw IllegalArgumentException("Unknow Expression")
}
2.4 迭代事務(wù):while循環(huán)和for循環(huán)
2.4.1 while循環(huán)
kotlin中的while循環(huán)和do-while循環(huán)和java中的完全一致
while(condition){
}
do{
}while(condition)
2.4.2 迭代數(shù)字:區(qū)間和數(shù)列(類比java中的for循環(huán))
在kotlin中作谭,代替java中for循環(huán)。使用到了區(qū)間的概念
區(qū)間本質(zhì)上就是兩個數(shù)值之間的間隔奄毡,一個是起始值折欠,一個是結(jié)束值。使用..符號來表示區(qū)間
val oneToTen: IntRange = 0..10;
for (i in 0 until 100) {
}
2.4.3 迭代map
fun main(args: Array<String>) {
val binaryReps = HashMap<Char, String>()
for (c in 'A'..'F') {
val binary = Integer.toBinaryString(c.toInt())
binaryReps[c] = binary
}
for ((letter, binery) in binaryReps)
println("$letter= $binery")
//迭代list
for ((index, element) in list.withIndex())
index//下標(biāo)
element//值
}
binaryReps[c] = binary 在這里等價于 binaryReps.put(c,binary)
2.4.3 in 關(guān)鍵字
- 用于檢查字符區(qū)間成員
- 用于檢查對象(實現(xiàn)Java.lang.Comparable接口的任意類)
- 檢查集合
//檢查字符區(qū)間
fun isLetter(c: Char) = c in 'a'..'z'
fun isNotDigit(c: Char) = c !in '0'..'9' //不在這個0到9區(qū)間
fun main(args: Array<String>) {
println(isLetter('c'))
println(isNotDigit('c'))
}
println("kotlin" in "java".."scala") // 是否在"java"和"scala"字符串區(qū)間 ,相當(dāng)于"java"<="kotlin"&& "kotlin" <= "scala"
println("kotlin" in setOf<String>("java","scala")) //kotlin 是否在集合中
2.6 小結(jié)
- fun關(guān)鍵字用來聲明函數(shù)吼过,val和var用來聲明不可變量和可變變量
- 字符串模板用來幫你避免煩瑣的字符串拼接锐秦,在變量名稱前面加上
{} 包圍一個表達(dá)式
- 值對象類在Kotlin中以簡潔的方式表示
- 熟悉if現(xiàn)在是帶返回值的表達(dá)式
- when表達(dá)式類似java中的switch但功能更強(qiáng)大
- 在檢查過變量是某種類型后(is)不必顯示的轉(zhuǎn)換他的類型
- for、while和do-while循環(huán)和java中類似盗忱。for循環(huán)現(xiàn)在更加方便酱床,特別是當(dāng)你在迭代map,又或是迭代集合需要下標(biāo)的時候
- 1..5會創(chuàng)建一個區(qū)間趟佃,可以使用in或者!in判斷在區(qū)間內(nèi)
- kotlin異常處理和java類似扇谣,除了Kotlin不要求你聲明的函數(shù)拋出異常
函數(shù)的定義和調(diào)用
3.2 讓函數(shù)更好的調(diào)用
java中每一個集合都有默認(rèn)的toString()實現(xiàn),但是它的格式化輸出是固定的闲昭。例如:
val list = listOf(1, 2, 3)
println(list)
>>> [1,2,3]
如果你需要用";"分隔并且用括號包圍罐寨,而不是采用默認(rèn)的輸出格式。需要怎么做序矩。代碼如下
}
fun <T> joinToString(collection: Collection<T>, speartor: String, prefix: String, postfix: String): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(speartor)
result.append(element)
}
result.append(postfix)
return result.toString()
}
fun main(args: Array<String>) {
val list = listOf(1, 2, 3)
println(joinToString(list,";","(",")"))
>>> (1;2;3)//輸出結(jié)果
3.2.1 命名參數(shù)
上面代碼中 println(joinToString(list,";","(",")")) 沒有什么可讀性鸯绿,如果不去查看函數(shù)聲明。我們根本不知道參數(shù)的用途贮泞。在kotlin中可以做的更優(yōu)雅楞慈,按照下面方式調(diào)用
joinToString(list,speartor = ";",prefix = "(",postfix = ")")
// 當(dāng)調(diào)用一個Kotlin函數(shù)時幔烛,可以顯示的表明一些參數(shù)的名稱啃擦,當(dāng)指明了一個參數(shù)的名稱時,為避免混淆饿悬。后面的參數(shù)名稱都需要指明
3.2.2 默認(rèn)參數(shù)值
//聲明帶默認(rèn)參數(shù)值的joinToString()
fun <T> joinToString(collection: Collection<T>,
speartor: String = ",",
prefix: String = "",
postfix: String = ""): String {
}
現(xiàn)在可以用所有的參數(shù)來調(diào)用這個函數(shù)
fun main(args: Array<String>) {
val list = listOf(1, 2, 3)
println(joinToString(list))
println(joinToString(list,""))
println(joinToString(list,";",prefix = "(",postfix = ""))
}
>>> (1,2,3)
>>> (123)
>>> (1;2;3
當(dāng)你從java中調(diào)用kotlin函數(shù)的時候令蛉,必須顯示的制定所有參數(shù)值,如果需要從java代碼中頻繁的調(diào)用,而且希望他能對java的調(diào)用者更加便捷珠叔,可以使用@JvmOverloads注解蝎宇,這個指示編譯器生成重載函數(shù)
對于上述joinToString()添加注解
@JvmOverloads
fun <T> joinToString(collection: Collection<T>, speartor: String = ",", prefix: String = "(", postfix: String = ")"): String {}
在java中調(diào)用時,有四種構(gòu)造方法供你使用
3.2.2 消除靜態(tài)工具類:頂層函數(shù)和屬性
@file:JvmName("StringFunctions")
package com.haife.app.kotlin.kotlindemo.strings
@JvmOverloads
fun <T> joinToString(collection: Collection<T>, speartor: String = ",", prefix: String = "(", postfix: String = ")"): String {
val result = StringBuilder(prefix)
for ((index, element) in collection.withIndex()) {
if (index > 0) result.append(speartor)
result.append(element)
}
result.append(postfix)
return result.toString()
fun main(args:Array<String>){
val list = listOf(1, 2, 3)
println(joinToString(list))
println(joinToString(list,";",prefix = "(",postfix = ""))
}
}
- 這個工具方法不用屬于任何類祷安,直接放在代碼文件的頂層
- @file:JvmName("StringFunctions") -> 注解制定類名姥芥,在java中可以 StringFunctions.joinToString(list);調(diào)用函數(shù)
3.3 拓展函數(shù)和屬性(給別人的類添加方法)
例如計算一個字符串最后一個字符
fun String.lastChar(): Char = this.get(this.length - 1)
>>> println("Kotlin".lastChar())
- 接收者類型:你需要拓展的類或者接口的名稱(String類)
- 接受者對象:用來調(diào)用這個拓展函數(shù)的對象(this)
3.3.1 導(dǎo)入和拓展函數(shù)
對于你定義的拓展函數(shù),它不會自動的整個項目范圍內(nèi)生效汇鞭,如果需要使用凉唐。需要進(jìn)行導(dǎo)入。Kotlin允許用和導(dǎo)入類一樣的語法來導(dǎo)入單個函數(shù)
import com.haife.app.kotlin.kotlindemo.strings.lastChar
println("Kotlin".lastChar())
//可以使用*號
import com.haife.app.kotlin.kotlindemo.strings.*
可以使用關(guān)鍵字as來修改導(dǎo)入的類或者函數(shù)名稱
import com.haife.app.kotlin.kotlindemo.strings.lastChar as last
println("Kotlin".last())
當(dāng)你在不同的包霍骄,有一些重名的函數(shù)事台囱,在導(dǎo)入時用as重命名就顯得尤為必要,這樣就可以在同一個文件中去使用它們读整。對于一般類和函數(shù)可以使用全名來指出這個類或者函數(shù)簿训,但對于拓展函數(shù) 關(guān)鍵字as是你解決沖突的唯一方式
3.3.3 作為拓展函數(shù)的工具函數(shù)(改寫上面分隔字符串代碼)
fun <T>Collection<T>.joinToString2(speartor: String = ",", prefix: String = "(", postfix: String = ")"):String{
val result = StringBuilder(prefix)
for ((index,element)in this.withIndex()){
if (index>0) result.append(speartor)
result.append(element)
}
result.append(postfix)
return result.toString()
}
3.3.5 拓展屬性
val String.lastChar: Char get() = get(this.length - 1)
"Kotlin".lastChar
3.4.2 讓函數(shù)支持任意數(shù)量的參數(shù)
varage:
3.5.1 分隔字符串
val str = "12.345-6.A";
println(str.split(".","-"))
>>>[12, 345, 6, A]
3.5.2 正則表達(dá)式和三重引號字符串
來看一個例子的兩種不同的實現(xiàn),一個使用拓展函數(shù)處理字符串米间,另一個使用正則表達(dá)式解析下完文件的整路徑獲取的目錄强品、文件名、文件拓展名
/User/yole/kotlin-book/chapter.adoc
先使用String的拓展函數(shù)substringBeforeLast屈糊、substringAfterLast講一個路徑分隔為目錄择懂、文件名、文件拓展名
fun paresPath(path: String) {
val directory = path.substringBeforeLast("/")
val fullName = path.substringAfterLast("/")
val fileName = fullName.substringBeforeLast(".")
val extension = path.substringAfterLast(".")
println("Dir: $directory,Name: $fileName,Extension: $extension")
}
>>> println("/User/yole/kotlin-book/chapter.adoc")
Dir: /User/yole/kotlin-book,Name: chapter,Extension: adoc
使用正則表達(dá)式解析一個目錄路徑
fun paresPath2(path: String) {
val regex = """(.+)/(.+)\.(.+)""".toRegex()
val matchResult = regex.matchEntire(path)
if (matchResult != null) {
val (directory, name, ext) = matchResult.destructured
println("Dir: $directory,name: $name,ext: $ext")
}
}
在這個例子中另玖,正則表達(dá)式寫在三重引號的字符串中困曙,在這樣的字符串中那就不需要對任何字符進(jìn)行轉(zhuǎn)義
3.6 局部函數(shù)和拓展
Kotlin提供一種方案:可以再函數(shù)中嵌套一些需要提取的函數(shù)
下面的例子中setUser函數(shù)是將user信息保存到數(shù)據(jù)庫
class User(val id: Int, val name: String, val address: String)
fun saveUser(user: User) {
if (user.name.isEmpty()) {
throw IllegalArgumentException("Cant save user ${user.id}:empty Name")
}
if (user.address.isEmpty()) {
throw IllegalArgumentException("Cant save user ${user.name}:empty Address")
}
//保存數(shù)據(jù)到數(shù)據(jù)庫
}
>>> saveUser(User(1, "", ""))
"Exception in thread "main" java.lang.IllegalArgumentException: Cant save user 1:empty Name"
如果需要驗證的用戶字段多了,這樣是不好的∏ィ現(xiàn)在我們將驗證代碼放到局部函數(shù)中慷丽,可以避免重復(fù)
fun saveUser2(user: User) {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw java.lang.IllegalArgumentException("Cant't save ${user.id}: empty $fieldName")
}
}
validate(user.name,"Name")
validate(user.address,"Address")
}
我們還可以繼續(xù)改進(jìn),將驗證代碼放到User的拓展函數(shù)中
fun User.validateBeforeSave() {
fun validate(value: String, fieldName: String) {
if (value.isEmpty()) {
throw java.lang.IllegalArgumentException("Cant't save ${this.id}: empty $fieldName")
}
}
validate(this.name, "Name")
validate(this.address, "Address")
}
fun saveUser3(user: User){
user.validateBeforeSave()
//保存到數(shù)據(jù)庫
}
3.7 小結(jié)
- Kotlin沒有自己定義集合的類鳄哭,而是在java集合類的基礎(chǔ)上提供了更加豐富的Api
- kotlin可以給函數(shù)的參數(shù)定義默認(rèn)值要糊,這樣大大降低了重載函數(shù)的必要性,而且命名參數(shù)讓多參數(shù)函數(shù)的調(diào)用更加易讀
- kotlin允許更靈活的代碼結(jié)構(gòu):函數(shù)和屬性都可以在文件中直接聲明妆丘,不僅僅是在類中作為成員
- Kotlin可以使用擴(kuò)展函數(shù)和屬性來擴(kuò)展任何類的Api锄俄,包括在外部庫中定義的類,而不需要修改源代碼勺拣,也沒有運(yùn)行時的開銷
- 中綴調(diào)用提供了處理單個參數(shù)的奶赠、類似調(diào)用運(yùn)算符方法的簡要語法
- Kotlin為普通字符串和正則表達(dá)式都提供了大量的方便字符串處理的函數(shù)
- 三重引號的字符串提供了一種簡潔的方法,解決了java中需要進(jìn)行大量的轉(zhuǎn)義和字符串鏈接的問題
- 局部函數(shù)幫助你保持代碼的簡潔药有,避免重復(fù)
4章 類毅戈、對象和接口
4.1 定義類繼承結(jié)構(gòu)
4.1.1 kotlin中的接口
Kotlin中的接口與java8中的相似:他們可以包含抽象方法的定義和非抽象方法的實現(xiàn)苹丸,但它們不能不能包含任何的狀態(tài)
使用interface來聲明一個Kotlin接口
interface Clickable {
fun click()
}
實現(xiàn)一個簡單接口
class Button : Clickable {
override fun click() {
println("I was click")
}
}
Kotlin在類名后面使用 : 來代替java中的extend和implements,和java一樣只能繼承一個類苇经,實現(xiàn)多個接口
與java中的@Override類似赘理,override修飾符標(biāo)注了被重寫的父類或者接口的方法和屬性。與java不同的是扇单,在Kotlin中使用 override 修飾符是強(qiáng)制要求的商模,這會避免先寫出實現(xiàn)方法再添加抽象方法造成的意外重寫:你的代碼將不能被編譯,除非你顯示的將這個方法標(biāo)注為 override 或者重命名它
接口的方法可以有一個默認(rèn)實現(xiàn)蜘澜,代碼如下
interface Clickable {
fun click()
fun showOff() = println("I'm clickable")
}
如果你實現(xiàn)了這個接口阻桅,需要為click提供一個實現(xiàn),可以重新定義showOff的行為兼都,如果你對接口默認(rèn)實現(xiàn)的感到滿意可行嫂沉,也可以直接忽視它
定義另一個實現(xiàn)同樣的方法的接口
interface Focusable {
fun setFocusable(b: Boolean) = println("I ${if (b) "Got" else "Lost"} focus.")
fun showOff() = println("I'm Focusable")
}
如果你在類中同時實現(xiàn)了Clickable和Focusable接口會發(fā)生什么?他們每一個都包含了帶默認(rèn)實現(xiàn)的showOff方法扮碧;那么類中會使用哪一個實現(xiàn)呢趟章?答案是任何一個都不會使用。取而代之的是慎王,如過你沒有顯示的實現(xiàn)showOff蚓土,你會得到編譯錯誤。Kotlin會強(qiáng)勢要求你提供自己的實現(xiàn)
class Button : Clickable, Focusable {
override fun showOff() {
super<Clickable>.showOff()
super<Focusable>.showOff()
}
override fun click() {
println("I was click")
}
}
Button實現(xiàn)了兩個接口赖淤,通過調(diào)用繼承的兩個父類型中的實現(xiàn)來實現(xiàn)showOff();Java中的寫法Clickable.super.showOff();在Kotlin中基類的名字要放在尖括號: super<Clickable>.showOff()
如果你只需要調(diào)用一個繼承的實現(xiàn)也可以這樣寫:
override fun showOff() = super<Clickable>.showOff()
如果你需要在java中實現(xiàn)包含方法體的接口(接口有默認(rèn)實現(xiàn))蜀漆,必須為所有方法,包括在Kotlin中有方法體的方法定義你自己的實現(xiàn)
重寫在基類中定義的成員
4.1.2 open咱旱、final和abstract修飾符:默認(rèn)為final
聲明一個帶一個open方法的open類(這個類是open的确丢,其他類可以繼承它)
open class RichButton : Clickable {
fun disable(){
//->這個函數(shù)默認(rèn)是final的,不能在子類中重寫它
}
open fun animate() {
//->這個函數(shù)是open的吐限,可以再子類中重寫它
}
override fun click() {
//->這個函數(shù)重寫了一個open函數(shù)并且她本身同樣是open的
}
}
注意鲜侥,如果你重寫了一個基類或者接口的成員,那么默認(rèn)這個成員也是open的诸典,如果你想改變這一行為描函,阻止你的類的子類重寫你的實現(xiàn),可以顯示的將重寫的成員標(biāo)注為final
禁止被重寫
open class RichButton : Clickable {
final override fun click() {
}
}
在這里final要指明是因為如果沒有final的override的默認(rèn)意味著open的狐粱,那么就可以被子類重寫
修飾符 | 相關(guān)成員 | 評注 |
---|---|---|
final | 不能被重寫 | 類中的成員默認(rèn)使用 |
open | 可以被重寫 | 需要明確的說明 |
abstract | 必須被重寫 | 只能在抽象類中使用舀寓;抽象成員不能有實例 |
override | 重寫父類或接口的成員 | 如果沒有用final聲明,重寫的成員默認(rèn)是開放的 |
4.1.3 可見性修飾符
修飾符 | 類成員 | 頂層聲明 |
---|---|---|
public(默認(rèn)) | 所有地方可見 | 所有地方可見 |
internal | 模塊中可見 | 模塊中可見 |
protected | 子類中可見 | —— |
private | 類中可見 | 文件中可見 |
4.1.4 內(nèi)部類和嵌套類(默認(rèn)是嵌套類)
- 嵌套類不持有外部類的引用肌蜻,內(nèi)部類持有
- 聲明成為內(nèi)部類需要inner修飾符
- Kotlin中默認(rèn)為嵌套類互墓,不持有外部類的引用
4.1.5 密封類:定義受限的類繼承結(jié)構(gòu)
sealed class Expr{
class Num(val value: Int) : Exp()
class Sum(val left: Exp, val right: Exp) : Exp()
}
sealed關(guān)鍵字聲明一個類為密封類,密封類不允許類外存在子類宋欺。
4.2 聲明一個帶非默認(rèn)構(gòu)造方法的類
Kotlin區(qū)分了主構(gòu)造方法(通常是主要而且簡潔的初始化類的方法轰豆,并且在類的外部聲明)和從構(gòu)造方法(在類的內(nèi)部聲明)
4.2.1 初始化類:主構(gòu)造方法和初始化語句塊
//帶一個參數(shù)的主構(gòu)造方法
class User constructor(nickname: String) {
val nickname: String
init {
this.nickname = nickname
}
}
- constructor關(guān)鍵字用來開啟一個主構(gòu)造方法或從構(gòu)造方法的聲明
- init關(guān)鍵字用來引入一個初始化代碼語句塊胰伍,這種語句塊包含在類被創(chuàng)建時執(zhí)行的代碼齿诞,并會與主構(gòu)造方法一起使用酸休,如果你愿意可以再使用多個初始化語句塊
構(gòu)造方法的參數(shù)也可以像函數(shù)一樣聲明一個默認(rèn)值
class User(val nickname: String, val isSubscribed: Boolean = false)
val user = User("Bob",false)
val user2 = User("Bob",isSubscribed = false)
val user3 = User("Bob")
注意 如果所有的構(gòu)造方法都有默認(rèn)值,編譯器會產(chǎn)生一個不帶任意參數(shù)的構(gòu)造方法來使用所有額默認(rèn)值
如果你的類具有一個父類祷杈,那么主構(gòu)造方法同樣需要初始化父類斑司。可以通過在基類列表的父類引用中提供父類構(gòu)造方法參數(shù)來做到這一點
open class User(val nickname: String = "Bob", val isSubscribed: Boolean = false)
class Twitteruser(nickname: String) : User(nickname, isSubscribed = true)
如果一個基類不具有任何構(gòu)造方法但汞,那么在子類在繼承時必須顯示的調(diào)用父類的構(gòu)造方法
open class Button
class RadioButton:Button()
注意與接口的區(qū)別接口是沒有構(gòu)造方法的宿刮。
4.4.2 構(gòu)造方法:用不同的方法來初始化父類
open class View {
constructor(ctx: Context)
constructor(ctx: Context, attr: Attributes)
}
這個類沒有聲明主構(gòu)造方法,私蕾,但是聲明了兩個從構(gòu)造方法僵缺。
如果你想拓展這個類,可以聲明同樣的構(gòu)造方法:
class MyButton : View {
//調(diào)用父類的構(gòu)造方法
constructor(ctx: Context) : super(ctx)
constructor(ctx: Context, attr: Attributes) : super(ctx, attr)
}
就像在java中一樣踩叭,使用this()關(guān)鍵字磕潮,使得一個構(gòu)造方法委托給同一個類中的另一個構(gòu)造方法,為參數(shù)傳入默認(rèn)值容贝。
class MyButton : View {
constructor(ctx: Context) : this(ctx,MY_STYLE)
constructor(ctx: Context, attr: Attributes) : super(ctx, attr)
}
4.2.3 實現(xiàn)在接口中聲明的屬性
//TODO:實現(xiàn)在接口中聲明的屬性
interface User {
val nickName: String
}
class PrivateUser(override val nickName: String) : User
class SubscribingUser(val email: String) : User {
override val nickName: String
get() = email.substringBefore('@')//自定義getter
}
class FaceBookUser(val accountId: String) : User {
override val nickName: String = getFaceBookName(accountId)//屬性初始化
}
4.3 編譯器生成的方法自脯,數(shù)據(jù)類和類委托
4.3.1 通用對象方法
- toString()
- equals()
- hashCode()
class Client(val name: String, val postalCode: Int) {
override fun toString(): String {
return "Client(name='$name', postalCode=$postalCode)"
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Client
if (name != other.name) return false
if (postalCode != other.postalCode) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + postalCode
return result
}
}
4.3.2 數(shù)據(jù)類
使用data修飾符修飾類,必要的方法會自動生成好
數(shù)據(jù)類和不可變型:copy()方法
class Client(val name: String, val postalCode: Int) {
fun copy(name: String = this.name, postalCode: Int = this.postalCode) {
}
}
>>> val client3 = client.copy(postalCode = 124)
類委托:使用by關(guān)鍵字
class DelegatingCollection<T> : Collection<T> {
private val innerList = arrayListOf<T>()
override fun contains(element: T): Boolean = innerList.contains(element)
override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
override val size: Int
get() = innerList.size
override fun isEmpty(): Boolean = innerList.isEmpty();
override fun iterator(): Iterator<T> = innerList.iterator();
}
可以使用by關(guān)鍵字將接口的實現(xiàn)委托到另一個對象
//類委托
class DelegatingCollection<T>(innerList: Collection<T> = ArrayList<T>()) : Collection<T> by innerList{
}
class CountSet<T>(val innerSet: MutableCollection<T> = HashSet<T>())
: MutableCollection<T> by innerSet {//將MutableCollection的實現(xiàn)委托給innerList
var objectAdded = 0
override fun add(element: T): Boolean {//不同的委托 提供一個不同的實現(xiàn)
objectAdded++
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {//不同的委托 提供一個不同的實現(xiàn)
objectAdded += elements.size
return innerSet.addAll(elements)
}
}
4.4 object關(guān)鍵字:將聲明一個類與創(chuàng)建一個實例結(jié)合起來
objec在kotlin中有多種情況斤富,但都遵循同樣的核心理念:這個關(guān)鍵字定義一個類并同事創(chuàng)建一個實例
使用場景:
- 對象聲明是定義單例的一種方式
- 伴生對象可以持有工廠方法和其他與這個類相關(guān)膏潮,但在調(diào)用時并不依實例的方法。他們的成員可以通過類名來訪問
-對象表達(dá)式用來替代java的匿名內(nèi)部類
4.4.1 對象聲明:創(chuàng)建單例
與類一樣满力,一個對象的聲明可以包含屬性焕参、方法、初始化語句塊等聲明油额,唯一不允許的就是構(gòu)造方法(主構(gòu)造方法和從構(gòu)造方法)
對象聲明同樣可以集成自類和接口龟糕,這通常在你使用的框架需要去實現(xiàn)一個接口,但是接口的實現(xiàn)并不包含任何狀態(tài)的的時候使用悔耘,下面是比較兩個對象誰更大的實現(xiàn)
object CaseInsensitiveFileComparator : Comparator<File> {
override fun compare(o1: File, o2: File): Int {
return o1.path.compareTo(o2.path, ignoreCase = true);
}
}
>>> println(CaseInsensitiveFileComparator.compare(File("/File"), File("/file")))
>>>0
4.4.2 伴生對象:工廠方法和靜態(tài)成員的地盤
companion關(guān)鍵字
kotlin中不能擁有靜態(tài)成員讲岁;java中的static并不是kotlin中的一部分,作為替代衬以,kotlin依賴包級別(大多數(shù)情況下替代java的靜態(tài)方法)函數(shù)和對象聲明(在其他情況下替代java的靜態(tài)方法缓艳,同事還包括靜態(tài)字段)。大多情況下還是推薦頂層函數(shù)看峻,但是頂層函數(shù)不能訪問累的private成員阶淘,為解決這個問題,如果你需要在沒有類實例的條件下調(diào)用但是需要訪問類內(nèi)部的函數(shù)互妓∠希可以將其寫成那個類中的對象聲明的成員坤塞,這種函數(shù)的一個例子就是工廠方法
class Test2Kotlin private constructor(val nickname2: String) {//將主構(gòu)造方法標(biāo)記為private
companion object {
fun newSubcribingUser(email: String) = Test2Kotlin(email.substringBefore('@'))
//通過工廠方法讓Facebook賬號來創(chuàng)建一個新的用戶
fun newFaceBookUser(accountId: Int) = Test2Kotlin(getFaceBookName(accountId))
private fun getFaceBookName(faceBookAccountId: Int): String {
return faceBookAccountId.toString()
}
}
工廠方法能夠返回聲明這個方法的類的子類,你還可以在不需要的時候避免創(chuàng)建子類
4.4.3 作為普通對象使用的伴生對象
伴生對象是一個聲明在類中的普通對象澈蚌,他可以有名字摹芙、實現(xiàn)一個接口或者擁有拓展函數(shù)、屬性等
聲明一個帶有名字的伴生對象
class Person(val name: String) {
companion object Loader {
fun fromeJson(jsonText: String): Person {
val gson = Gson()
return gson.fromJson(jsonText, Person::class.java)
}
}
}
>>> Person.fromeJson("{name:Bob}")
>>> Person.Loader.fromeJson("{name:haife}")
如果你省略了伴生對象的名稱宛瞄,那么默認(rèn)的名字為Companion浮禾,這個例子會在伴生對象的拓展時會用到
在伴生對象中實現(xiàn)接口
interface JSONFactory<T> {
fun fromJson(jsonText: String): T
}
class Person2(val name: String) {
//實現(xiàn)接口的伴生對象
companion object : JSONFactory<Person2> {
override fun fromJson(jsonText: String): Person2 {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
}
}
fun <T> loadFromJSON(factory: JSONFactory<T>): T {
return factory.fromJson("{name:Haife}")
}
5章 lambda編程
5.1 lambda表達(dá)式和成員引用
lambda的本質(zhì)就是傳遞給函數(shù)的一小段代碼
5.1.3 lambda表達(dá)式語法
{ x: Int, y: Int -> x + y }
val people = listOf(Person(name = "Alice", age = 29), Person(name = "Bob", age = 31))
people.maxBy({ p: Person -> p.age })//1
people.maxBy() { p: Person -> p.age }//2
people.maxBy { it.age }//3
- 如果lambda表達(dá)式是函數(shù)最后一個實參,可以將他放在括號外面 如上代碼2處
- 如果lambda表達(dá)式是函數(shù)唯一的實參份汗,可以省略括號 如上代碼3處
在lambda中使用函數(shù)參數(shù)
fun printMessageWithPrefix(messages: Collection<String>, prefix: String) {
messages.forEach { print("$prefix $it") }
}
在lambda中改變局部變量
fun printProblemCounts(response: Collection<String>) {
var clientErrors = 0
var serverErrors = 0
response.forEach {
if (it.startsWith("4")) {
clientErrors++
} else if (it.startsWith("5")) {
serverErrors++
}
}
}
如果lambda被用作事件處理器或者用作在其他異步執(zhí)行的情況盈电,對局部變量的修改只會在lambda執(zhí)行的時候發(fā)生
5.1.5 成員引用
Kotllin與Java8一樣,如果把函數(shù)轉(zhuǎn)換成一個值杯活,你就可以傳遞它匆帚。使用 :: 運(yùn)算符來轉(zhuǎn)化
val getAge = Person::age(age可以是屬性也可以是函數(shù))
val getAge2 = { p: Person -> p.age }
這兩行代碼是同樣的作用,注意:不管你引用的是函數(shù)還是屬性旁钧,都不要再成員引用的后面加括號吸重。
5.2 集合的函數(shù)式Api
kotlin中有許多提供操作集合的一些函數(shù)庫,以及如何使用
5.2.1基礎(chǔ)操作:filter和map函數(shù)
filter和map函數(shù)形成了集合操作的基礎(chǔ)均践,很多集合操作都是借助他們來完成的
filter函數(shù)遍歷集合并選出應(yīng)用給定的lambda表達(dá)式后會返回true的那些元素
val list = listOf(1, 2, 3, 4)
println(list.filter { it % 2 == 0 })
>>> [2,4]
map集合對函數(shù)中的每一個元素應(yīng)用給定的函數(shù)并把結(jié)果收集成一個新的集合
val list = listOf(1, 2, 3, 4)
println(list.map { it * it })
>>> [1, 4, 9, 16]
再試用lambda表達(dá)式代碼看起來簡單晤锹,但有時候影藏了底層操作的復(fù)雜性。要時刻記住自己的代碼在干什么彤委。
還可以對map集合應(yīng)用過濾和變換的函數(shù)
val number = mapOf(0 to "zero", 1 to "one")
println(number.filterValues { it == "zero" })
>>>{0=zero}
鍵和值分別由各自的函數(shù)處理鞭铆。filterkeys 和 mapKeys 來過濾和變化map的鍵,filterValue 和 mapValue過濾和變換對應(yīng)的值
5.2.2 “all” "any" "count" 和find:對集合應(yīng)用的判斷式
但你需要檢查集合中所有元素是否都符合某種條件(或者他的變種焦影,是否存在符合的元素)车遂。在Kotlin中,它們是通過all和any函數(shù)來表達(dá)的斯辰。count函數(shù)檢查有多少個元素滿足判斷式舶担,而find函數(shù)是返回第一個滿足判斷式的值
5.2.3 groupBuy:把列表轉(zhuǎn)化成分組的map
val people = listOf(Person(name = "Alice", age = 29), Person(name = "Bob", age = 31), Person(name = "Hai", age = 31))
val groupBy: Map<Int, List<Person>> = people.groupBy { it.age }
println(groupBy)
>>> {29=[Person(name=Alice, age=29)], 31=[Person(name=Bob, age=31), Person(name=Hai, age=31)]}
這個操作結(jié)果是一個map,是元素分組依據(jù)的鍵(這里是age)和元素分組(persons)之間的映射
5.2.4 flatMap和flatten:處理嵌套集合中的元素
val people = listOf(Person(name = "Alice", age = 29), Person(name = "Bob", age = 31), Person(name = "Hai", age = 31))
println(people.flatMap { it.name.toList() }.toSet())
>>> [A, l, i, c, e, B, o, b, H, a]
//沒有調(diào)用toSet()函數(shù)(移除重復(fù)元素)
>>> [A, l, i, c, e, B, o, b, H, a, i]
flatMap處理兩件事:首先根據(jù)作為實參給定的函數(shù) 對集合中的每個元素做變換(映射)彬呻,然后把多個列表合并成一個列表
注意:當(dāng)你卡殼在元素的集合不得不合并成一個的時候衣陶,你可能回想起flatMap函數(shù)。但是當(dāng)你不需要做任何變換的時候闸氮,只需要平鋪成一個集合剪况。可以使用flatten函數(shù):listOfLists.flatten
5.3 惰性集合操作:序列
Kotlin中惰性結(jié)合操作的入口就是Sequence接口蒲跨。這個接口表示的就是一個可以逐個列舉元素的元素序列译断。Sequence只提供了一個方法,iterator或悲,用來從序列中獲取值孙咪。
val people = listOf(Person(name = "Alice", age = 29), Person(name = "Bob", age = 31), Person(name = "Hai", age = 31))
println(people.asSequence().map { it.age * it.age }.filter { it>900 }.toList())
5.3.1 執(zhí)行序列操作:中間和末端操作
序列操作分為兩類:中間和末端堪唐。一次中間操作返回的是一個序列。如上面代碼的map和filter操作翎蹈,這個新序列知道如何變幻序列的元素淮菠。而一次末端操作是返回一個結(jié)果⊙畹埃可以是集合兜材、數(shù)字理澎、或者從其他集合變換序列中獲取的任意對象
中間操作始終是惰性的逞力,如果你沒有對應(yīng)末端操作。對應(yīng)的中端操作會被延期執(zhí)行糠爬,中斷操作只有在獲取結(jié)果的時候才會被調(diào)用
另外值得注意的是寇荧,對操作序列來說:所有的操作是按順序應(yīng)用在每一個元素上:處理完第一個元素(先映射在過濾 即 先一個元素先執(zhí)行map在執(zhí)行filter,而不是等到所有的元素執(zhí)行玩map在執(zhí)行filter)然后完成第二個元素的處理执隧。這意味有些元素不會被處理發(fā)生變化 比如當(dāng)find替換上面的map函數(shù)來過濾時揩抡。
5.3.2 創(chuàng)建序列
上面是通過list集合:調(diào)用asSequence()。另一種方式就是通過generateSequence函數(shù)
生成并使用自然數(shù)序列
val naturalNumbers = generateSequence(0) { it + 1 }
val numberTo100 = naturalNumbers.takeWhile { it<100 }
println(numberTo100.sum())
5.4.2 SAM構(gòu)造方法:顯示的吧lambda轉(zhuǎn)化成函數(shù)式接口
SAM構(gòu)造方法是編譯器生成的函數(shù)镀琉,讓你執(zhí)行從lambda到函數(shù)式接口的顯示轉(zhuǎn)化峦嗤,例如有一個方法返回的是一個函數(shù)式接口,你不能直接返回一個lambda屋摔,要用SAM構(gòu)造方法將其包裝起來
fun createAllDoneRunnable(): Runnable {
return Runnable { print("All Done") }
}
>>> createAllDoneRunnable.run()
5.5 帶接受者的lambda:with()和apply()
在lambda函數(shù)體內(nèi)可以調(diào)用一個不同對象的方法烁设,而且無需借助任何額外限定符;這種能力在java中是找不到钓试。這樣的lambda叫做帶接受者的lambda
5.5.1 "with"函數(shù)
fun alphabet():String{
val stringBuilder = StringBuilder()
return with(stringBuilder){//指定接受者的值装黑,你會調(diào)用他的方法
for (letter in 'A'..'Z'){
this.append(letter)//通過顯示的this來調(diào)用接受者值的方法
}
this.append("\n I Know The alphabet" )//可以省略this
this.toString()//從lambda返回值
}
}
>>>輸出: ABCDEFGHIJKLMNOPQRSTUVWXYZ
I Know The alphabet
with結(jié)構(gòu)看起來像是一種特殊的語法結(jié)構(gòu),但他實際上是一個接受兩個參數(shù)的函數(shù)弓熏,一個是stringbuilder恋谭;另一個是一個lambda表達(dá)式
with函數(shù)把第一個參數(shù)轉(zhuǎn)化作為第二個參數(shù)傳給它lambda的接收者。
with返回值是執(zhí)行l(wèi)ambda代碼的結(jié)果挽鞠,該結(jié)果就是lambda中最后一個表達(dá)式(的值)疚颊。但有時候你想返回的是接受者對象,而不是執(zhí)行l(wèi)ambda的結(jié)果信认。那就需要用到apply()函數(shù)
5.5.2 apply 函數(shù)
apply函數(shù)幾乎和with函數(shù)一模一樣材义,唯一的區(qū)別是apply始終會返回作為實參傳遞給他的對象(接受者對象)
fun creatViewTithCustomAttributes(context: Context) {
TextView(context).apply {
this.text = "Sample Text"
this.textSize = 18.0F
this.setPadding(10,0,0,0)
}
}
5.6小結(jié)
- lambda允許你把代碼塊當(dāng)做參數(shù)傳遞給函數(shù)
- Kotlin可以吧lambda放在括號外面?zhèn)鬟f給函數(shù),而且可以使用it引用單個的lambda參數(shù)
- lambda中的代碼可以訪問和修改包含這個lambda調(diào)用的函數(shù)中的變量
- 通過在函數(shù)名稱前加上::狮杨,可以創(chuàng)建方法逸寓、構(gòu)造方法以及屬性的引用剧腻,并應(yīng)用這些引用代替lambda傳遞給函數(shù)
- 使用像filter、map关串、all、any拴曲、count、find等函數(shù)時,大多數(shù)公共的集合不需要手動迭代就可以完成翩迈。
- 序列允許你合并集合上的多次操作,而不需要創(chuàng)建新的集合來保存中間結(jié)果
- 可以把lambda作為實參傳遞給接受java函數(shù)式接口(帶單抽象方法的接口盔夜,也叫SAM接口负饲,列入runnable)作為形參的方法
- 帶接受者的lambda是一種特殊的lambda,可以在這種lambda中訪問一個特殊接收對象的的方法
- with標(biāo)準(zhǔn)庫函數(shù)允許你調(diào)用同一個兌現(xiàn)的多個方法喂链,而需要反復(fù)寫出這個對象的引用返十。apply函數(shù)讓你使用構(gòu)建者風(fēng)格的API創(chuàng)建和初始化任何對象
Kotlin的類型系統(tǒng)
6.1 可空性
可空性是Kotlin類型系統(tǒng)幫助你避免NullPointerException錯誤的特性
6.1.1 可空類型
Java和kotlin類型系統(tǒng)之間最重要的一條也可能是最重要的一條的區(qū)別是,Kotlin對可空類型的顯示的支持,這是一種指出你的程序中哪些變量和屬性允許為null的方式椭微,如果一個變量可以為空洞坑,那么對這個變量的方法的調(diào)用就是不安全的。Kotlin中不允許你這樣的調(diào)用
java
pubic int strLen(String s){
return s.length();
}
>>> strLen(null) //在Java中這樣是可以執(zhí)行的蝇率,但是會拋出NullPointerException
kotlin
fun strLen(s:String) = s.length
>>> strLen(null)//這樣調(diào)用在編譯器就會被標(biāo)記錯誤
如果你允許調(diào)用這個方法的時候傳給它所有可能的實參迟杂,包括那些可以為null的實參,那需要顯示的在類型名稱后面添加"?"來標(biāo)記它
fun strLen2(s:String?) = s?.length?:0
strLen2(null)
重申一下本慕,沒有"?"的類型表示這種類型的變量不能存儲null的引用排拷。這說明所有的常見類型默認(rèn)都是非空的,除非顯示的把他標(biāo)記為可空锅尘。不能把一個可空類型的變量賦值給一個非空類型的變量监氢;也不能傳遞給非空類型參數(shù)的函數(shù)
6.1.3 安全調(diào)用運(yùn)算符:"?."
?. 運(yùn)算法允許你把一次null檢查和一次方法操作合并成一個操作
s?.length 等價于 if(s!=null) s.length else null
注意這個調(diào)用的操作結(jié)果也是可空的
安全調(diào)用還可以處理屬性;也可以鏈接多個安全調(diào)用
6.1.4 Elvis運(yùn)算符:"?:"
Kotlin有方便的運(yùn)算符來替代null的默認(rèn)值鉴象,被稱作Elvis運(yùn)算符
Elvis運(yùn)算符接受兩個運(yùn)算數(shù)忙菠,如果第一個運(yùn)算數(shù)不為空,運(yùn)算結(jié)果就是第一個運(yùn)算數(shù)纺弊。反之則返回第二個運(yùn)算數(shù)
fun foo(s: String?) {
val str: String = s ?: ""
}
這個例子中個""空字符串代替了null
6.1.5 安全轉(zhuǎn)換:"as?"
as?運(yùn)算符嘗試把值轉(zhuǎn)化成指定的類型牛欢,如果值不是合適的類型就返回null
//常見的用法和Elvis運(yùn)算符結(jié)合使用
class Person2( val firstName: String, val lastName: String) {
override fun equals(other: Any?): Boolean {
val otherPerson:Person2 = other as? Person2 ?: return false
return otherPerson.firstName == firstName && otherPerson.lastName == lastName
}
}
>>> val p1 = Person2("Dmitry","Jemerov")
val p2 = Person2("Dmitry","Jemerov")
println(p1==p2)
println(p1.equals(42))
6.1.6 非空斷言:"!!"
非空斷言是Kotlin提供給你最簡單的處理可空類型值的工具,可以把任何值轉(zhuǎn)化成非空類型淆游。如果你對一個null值做非空斷言傍睹,程序則會拋出NullPointExpection
6.1.7 "let"函數(shù)
let函數(shù)讓處理可空表達(dá)式變得更容易。和安全調(diào)用運(yùn)算符一起犹菱,它允許你對表達(dá)式求值拾稳,檢查結(jié)果是否為null,并吧結(jié)果保存為一個變量
fun sendEmail(email: String?) {
println("SendEmail:$email")
}
val email: String? = "773938795@qq.com"
email?.let { sendEmail(it) }
let里面的it是非空的腊脱,如果email為空访得,則不會執(zhí)行l(wèi)ambda
6.1.8 延遲初始化屬性
聲明一個不需要初始化器的非空類型屬性
private var lateinit mService:MyService
注意 延遲初始化屬性的都是var,因為她需要在夠著方法外修改值,而val會被編譯成在構(gòu)造方法中初始化的final字段悍抑。
6.1.9 可空類型的拓展函數(shù)
為可空類型定義拓展函數(shù)是一種更強(qiáng)大的處理null值的方式鳄炉,可以允許接收者null的(拓展函數(shù))調(diào)用,并在該函數(shù)中處理null搜骡,而不是在確保變量不為null之后在調(diào)用它的方法拂盯。只有拓展函數(shù)才能處理這一點,普通成員方法的調(diào)用是通過對象的實例來進(jìn)行分發(fā)的记靡,因此當(dāng)實例為null時谈竿,成員方法永遠(yuǎn)不能被執(zhí)行
Kotlin標(biāo)準(zhǔn)庫中提供了String類的兩個拓展函數(shù)就說明了這一點isEmpty()和isBank(),第一個函數(shù)判斷字符串是否為空字符串第二個判斷它是否是空的或者它只包含空白字符
> 不需要安全調(diào)用就可以訪問可空類型的拓展
fun verifyUserInput(input: String?) {
if (input.isNullOrBlank()) {//這里不用安全調(diào)用 ?.
print("Please fill in the required fields")
}
}
>>> verifyUserInput("")
>>> verifyUserInput(null)
public fun String?.isNullOrBlank(): Boolean {
return this == null || this.isBlank()
}
當(dāng)你為一個可空類型定義拓展函數(shù)事 意味者你可以對可空的值調(diào)用這個函數(shù),并且函數(shù)體中的this可能為null摸吠,所以你必須顯示的檢查空凸,在java中this永遠(yuǎn)是非空的,因為她的引用是你當(dāng)前類所在的實例蜕便。而在Kotlin中 這并不成立
注意 當(dāng)你定義自己的拓展函數(shù)時贩幻,需要考慮該拓展是否需要為可空類型定義。默認(rèn)情況下,應(yīng)該把它定義成非空類型的拓展函數(shù)。如果發(fā)現(xiàn)大部分情況下需要在可空類型上使用這個函數(shù)顾瞪,你可以稍后在做修改。
6.1.10 類型參數(shù)的可空性
Kotlin中所有的泛型類和泛型參數(shù)默認(rèn)都是可空的。任何類型,包括可空類型在內(nèi),都可以替換類型參數(shù)坯屿。在這種情況下吠昭,使用類型參數(shù)作為類型的聲明都允許為nul
蘑拯,盡管參數(shù)類型T并沒有用問好(?)結(jié)尾
3.1.11 可空性和Java
Kotlin與java的互操作性是非常有用的孔轴,但是在Java得了類型系統(tǒng)中是不支持可控性的悍引,那么在當(dāng)你你混合使用Kotlin和Java時會發(fā)生什么玉凯?是否會失去所有的安全性?或者每個值都必須檢查是否為null吗浩?
Java中的@Nullable String被Kotlin當(dāng)做String赶熟?@NotNull String被當(dāng)做String
如果這些注解不存在會發(fā)生什么眶诈,這種情況下溯警,Java類型會變成Kotlin中的平臺類型
平臺類型
平臺類型就是的本質(zhì)就是Kotlin不知道可空性信息,既可以把它當(dāng)做可控類型處理曹宴,也可以把它當(dāng)做非空類型處理,這意味者你需要像Java中一樣宴偿,對你在這個類型上的操作負(fù)全部責(zé)任翻伺,如果你錯誤的理解了這個值急灭,使用的時候就會運(yùn)到NullPointException
Type(java) = (Type?) or (Type) (Koltin)
在Kotlin中不能聲明一個平臺類型的變量,這些類型只能來自java代碼
繼承
當(dāng)在Kotlin中重寫Java方法時扫尖,可以把參數(shù)和返回類型定義成可空的白对,也可以選擇非空的
注意 在實現(xiàn)Java類或者接口的方法時一定搞清楚它的可空性,因為方法的實現(xiàn)可以在非Kotlin代碼中被調(diào)用换怖。Kotlin會為你聲明的每一個非空參數(shù)生成非空斷言甩恼,如果java傳給null 斷言會被觸發(fā)。
6.3 集合與數(shù)組
6.3.1 可空性和集合
創(chuàng)建一個可空值的集合
fun readNumbers(reader: BufferedReader): List<Int?> {
val result = ArrayList<Int?>()
for (line in reader.lineSequence()) {
try {
val number = line.toInt()
result.add(number)
} catch (e: NumberFormatException) {
result.add(null)
}
}
return result
}
List<Int?> 是一個可持有Int?類型值的列表(可以持有int或者null)沉颂。從Kotlin1.1開始可以寫成String.toIntOrNull來簡化上面代碼条摸。
注意,變量自己類型的可空性和用作類型參數(shù)的類型的可空性是有區(qū)別的
List<Int?> 這種情況下铸屉,列表本省始終不為空钉蒲,但列表中的每個值可能為空
List<Int>? 可能包含空引用而不是列表實例,單列表中的每個實例都是非空的
在另一種上下文中彻坛,你可能需要聲明一個變量持有可控的列表顷啼,并且包含可空的數(shù)字,Kotlin中的寫法是List<Int?>?
使用可控集合
fun addValidNumbers(numbers: List<Int?>) {
var sumOfValidNumbers = 0
var invalidNumbers = 0
for (number in numbers) {
println("number: $number")
if (number != null) {
sumOfValidNumbers += number
} else {
invalidNumbers++
}
}
println("Sum Of validNumbers $sumOfValidNumbers")
println("invalidNumbers $invalidNumbers")
}
>>> 43
>>> 1
遍歷一個包含可空值的集合并過濾null值是一種很常見的操作昌屉,因此Kotlin提供了一個標(biāo)準(zhǔn)庫函數(shù)filterNotNull來完成它钙蒙,這里大大簡化上述的代碼
6.3.2 只讀集合和可變集合
MutableCollection集成了Collection并添加了修改集合內(nèi)容的方法
graph LR
MutableCollection-->Collection
MutableCollection | Collection |
---|---|
add() | size() |
remove() | iterator |
clear() | contains |
使用集合接口時需要牢記的一點是只讀集合不一定是不可變的,如果你使用的變量擁有一個只讀接口類型间驮,它可能只是同一個集合的眾多引用中一個躬厌,任何其他的引用都可能擁有一個可變類型接口
兩個不同的引用,一個只讀竞帽、一個引用 指向同一個集合對象
graph LR
list:List-->a/b/c
mutableList:MutableList-->a/b/c(集合)
如果你調(diào)用了這樣的方法扛施,這會導(dǎo)致concurrentModification錯誤和其他一些問題。因此必須了解只讀集合并不總是線程安全的
6.3.3 Kotlin集合和Java
集合創(chuàng)建函數(shù)
集合類型 | 只讀 | 可變 |
---|---|---|
List | listOf | mutableListOf抢呆、arraylistOf |
Set | setOf | mutableSetOf煮嫌、hashSetOf、linkSetOf |
Map | mapOf | mutableMapOf抱虐、hashMapOf、linkMapOf |
6.3.5 對象和基本數(shù)據(jù)類型的數(shù)組
fun main(args: Array<String>) {
for (i in args.indices) {
print("Argument $i is : ${args[i]}")
}
}
要在Kotlin中定義數(shù)組饥脑,有以下方法供你選擇
- arrayOf函數(shù)創(chuàng)建一個數(shù)組恳邀,它包含的元素是指定為該函數(shù)的實參
- arrayOfNulls創(chuàng)建一個給定大小的數(shù)組,包含的是null元素灶轰。當(dāng)然它只能用來創(chuàng)建包含元素類型可空的數(shù)組
- Array 構(gòu)造方法接受數(shù)組的大小和一個Lambda表達(dá)式
創(chuàng)建字符數(shù)組
val letters: Array<String> = Array(26) { i -> ('a' + i).toString() }
println(letters.joinToString("")
為了表示基本數(shù)據(jù)類型的數(shù)組谣沸,Kotlin提供了若干獨立的類,每一種基本數(shù)據(jù)類型都對應(yīng)一個笋颤,例如Int類型對應(yīng)IntArray char對應(yīng)CharArray boolean對應(yīng)BooleanArray
6.4 小結(jié)
- Kotlin對可空類型的支持乳附,可以幫助我們在編譯期内地,檢測出潛在NullPointException錯誤
- Kotlin提供了像安全調(diào)用(?.) Elvis運(yùn)算符(?:)、非空斷言(!!)以及l(fā)et函數(shù)這樣的函數(shù)來簡潔處理可空類型
- as?安全轉(zhuǎn)化運(yùn)算符提供了一種簡單的方式來轉(zhuǎn)化成一個類型赋除,以及處理當(dāng)它擁有不同類型的情況
- Java中的類型在Kotlin中被稱之為平臺類型阱缓,允許開發(fā)者們將他當(dāng)做可空或則非空來處理
- Any是所有其他類型的超類型,類似于Java中的Object举农,而Unit類比于Void
- 不會正常終止的函數(shù)使用Nothing類型來作為返回類型
- Kotlin使用標(biāo)準(zhǔn)Java集合類荆针,并通過區(qū)分只讀和可變集合來增強(qiáng)它們
- 當(dāng)你在Kotlin中繼承或者實現(xiàn)Java類或者Java接口時,你需要仔細(xì)考慮參數(shù)的可空性和可變性
7章.運(yùn)算符重載及其他約定
本章內(nèi)容包括
1 運(yùn)算符重載
2 約定:支持各種運(yùn)算的特殊命名函數(shù)
3 委托屬性
7.1 重載算術(shù)運(yùn)算符
7.1.1 重載二元算術(shù)運(yùn)算符
/**
* 定義一個plus運(yùn)算符
*/
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
注意 如何使用operator關(guān)鍵字聲明plus函數(shù)颁糟,用于重載運(yùn)算符的所有函數(shù)都必須用此關(guān)鍵字聲明航背,在使用了operator關(guān)鍵字聲明了plus函數(shù)后,你就可以使用“+”號來求和棱貌,就會調(diào)用plus函數(shù)
除了把這個運(yùn)算符聲明成成員函數(shù)以外玖媚,還可以聲明為拓展函數(shù)
//TODO 定義一個plus運(yùn)算符 拓展函數(shù)寫法
operator fun Point.plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
可重載的二元算術(shù)運(yùn)算符
表達(dá)式 | 函數(shù)名 |
---|---|
a * b | times |
a / b | div |
a % b | mod |
a + b | plus |
a - b | minus |
7.1.2 重載復(fù)合賦值運(yùn)算符
通常情況下,在你定義plus運(yùn)算符的時候婚脱,Kotlin不止支持+號 也支持+= -=今魔,像這樣的運(yùn)算符我么稱之為復(fù)合賦值運(yùn)算符
在一些情況下 +=運(yùn)算可以修改使用它的變量所引用的對象,但不會重新分配引用起惕,將一個元素添加到可變集合就是一個很好的例子
val number = ArrayList<Int>()
number += 42
println(number[0])
>>> 42
如果你定義了一個名字為plusAssign的函數(shù)涡贱,Kotlin會在你調(diào)用+=的時候用到它,其他二元運(yùn)算符也有命名相似的函數(shù)惹想。入timesAssign问词、minusAssign
Kotlin標(biāo)準(zhǔn)庫為可變集合定義了plusAssign函數(shù)
public inline operator fun <T> MutableCollection<in T>.plusAssign(element: T) {
this.add(element)
}
當(dāng)你在代碼中調(diào)用+=時,理論上plus和plusAssign都有可能被調(diào)用嘀粱,如果這種情況下激挪,兩個函數(shù)都有定義且適用,編譯器會報錯锋叨。一種方案就是替換運(yùn)算符為普通函數(shù)調(diào)用垄分,另一種就是用val替代var。這樣plusAssign就不再適用娃磺,但一般來說薄湿,最好是在設(shè)計新類的時候不要同時定義這兩個函數(shù)
Kotlin支持集合的這兩種方法,+和-總是返回一個新的集合偷卧。+=和-=運(yùn)算符用于可變集合時豺瘤,總是在修改它們。而它們在用作只讀集合的時候會返回一個修改過的副本听诸,這意味在只有在只讀集合被聲明稱var的時候坐求,才能使用+=和-=
7.3 集合與區(qū)間的約定
7.3.1 通過下標(biāo)來訪問元素: "get"與"set"
我們都知道在Kotlin中可以通過類似Java訪問數(shù)組的方式來訪問map中的元素或修改它
val value = map[key]
mutable[key] = value
那我們也可以為自定義類添加類似的方法
實現(xiàn)get約定
operator fun Point.get(index: Int) {
when(index){
0->x
1->y
else
-> throw IndexOutOfBoundsException()
}
}
val point = Point(3,4)
point[1]
7.3.2 實現(xiàn)in約定
寫法與get函數(shù)類似,對應(yīng)的函數(shù)名稱為“contains” 晌梨。a in c -> c contains a
in 右邊的對象會調(diào)用contains函數(shù)桥嗤,in左邊的對象會被作為函數(shù)參數(shù)
7.3.3 實現(xiàn)rangTo約定
創(chuàng)建區(qū)間操作 start..end -> start.rangTo(end)
rangTo函數(shù)返回了一個區(qū)間须妻,你可以為自己的類定義這個運(yùn)算符。但當(dāng)你的類實現(xiàn)了Comparable接口泛领,那就不需要了荒吏。你可以通過Kotlin標(biāo)準(zhǔn)庫創(chuàng)建一個任意可比較的區(qū)間
在for循環(huán)中使用“iterator”的預(yù)定
在Kotlin中,我們知道在for循環(huán)中也可以使用in運(yùn)算符师逸,和用作區(qū)間檢查一樣司倚。但是在這種情況下它的含義是不同的:它是被用來執(zhí)行迭代。這意味著for(x in list){} 將會被轉(zhuǎn)化成list.iterator()的調(diào)用篓像。然后就像和Java一樣动知,在上面重復(fù)調(diào)用hasNext()和next()
請注意,在Kotlin中员辩,這也屬于一種約定盒粮,那么就意味著iterator()函數(shù)也可以被定義成拓展函數(shù),這就解釋為什么可以遍歷一個常規(guī)java字符串:標(biāo)準(zhǔn)庫已經(jīng)為CharSequence定義了一個iterator方法,而她是String的父類
public operator fun CharSequence.iterator(): CharIterator = object : CharIterator()
7.4 解構(gòu)聲明和組件函數(shù)
解構(gòu)聲明 這個功能允許你展開單個復(fù)合值奠滑,并使用它來初始化多個單獨的變量
val point = Point(3, 4)
val (x, y) = point
print(x)
print(y)
對于數(shù)據(jù)類(data class) 編譯器會為每一個在主構(gòu)造方法中聲明的屬性定義一個componentN函數(shù)丹皱,如果是非數(shù)據(jù)類,你可以按照下面代碼的方式手動添加
class Point(val x: Int, val y: Int) {
operator fun component1() = x
operator fun component2() = y
}
請注意宋税,標(biāo)準(zhǔn)庫只允許你用此語法來訪問一個對象的前五個元素
7.4.1 解構(gòu)聲明和循環(huán)
用解構(gòu)聲明來遍歷map集合
fun printEntries(map: Map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
7.5 重用屬性訪問的邏輯:委托屬性
委托是一種設(shè)計模式摊崭,操作的對象不用自己執(zhí)行,而是把工作委托給另一個輔助對象
7.5.1 委托屬性的基本操作
7.5.2 使用委托屬性:惰性初始化和"by lazy()"
惰性初始化是一種常見的模式杰赛,只有在第一次訪問該屬性的時候呢簸,才根據(jù)需求創(chuàng)建對象的一部分
7.6 小結(jié)
Kotlin允許使用對應(yīng)名稱的函數(shù)名來沖在一些標(biāo)準(zhǔn)的數(shù)學(xué)運(yùn)算,但是不能定義自己的運(yùn)算符
比較運(yùn)算符映射的是 "equals()"和 "compareTo()"
通過自定義get乏屯、set和contains函數(shù)根时,可以讓你自己的類與Kotlin的集合一樣通過[]、和in運(yùn)算符
可以通過約定來創(chuàng)建區(qū)間辰晕,以及迭代集合和數(shù)組
解構(gòu)聲明和可以用來展開單個對象用來初始化多個變量蛤迎,這可以方便的重函數(shù)總返回多個值,他們可以自動處理數(shù)據(jù)類含友,或者自己定義的componentN來支持
委托屬性可以用來重用邏輯替裆、這些邏輯用來如何存儲、初始化窘问、訪問和修改屬性值扎唾。
lazy()函數(shù)提供了一種實現(xiàn)惰性初始化的簡單方法
8章.高階函數(shù):lambda作為形參和返回值
本章內(nèi)容
1 函數(shù)類型
2 高階函數(shù)及其在組織代碼中的應(yīng)用
3 內(nèi)聯(lián)函數(shù)
4 非局部返回和標(biāo)簽
5 匿名函數(shù)
8.1 高階函數(shù)
高階函數(shù)就是將另一個函作為形參和返回值的函數(shù),在Kotlin中函數(shù)可以用函數(shù)引用和lambda來表示南缓。因此,任何以函數(shù)或者lambda表達(dá)式作為函數(shù)參數(shù)的函數(shù)荧呐,都可以稱之為高階函數(shù)
8.1.1 函數(shù)類型
為了聲明一個以lambda作為實參的函數(shù)汉形,你需要知道如何聲明對應(yīng)形參的類型纸镊。我們來看會個簡單的例子
val sum = { x:Int,y:Int -> x+y}
val action = { println(42) }
在這個例子中,編譯器推導(dǎo)出sum和action這兩個變量具有函數(shù)類型概疆。
變量的顯示類型聲明
//TODO:有兩個Int型參數(shù)和Int型返回值的函數(shù)
val sum: (Int ,Int) -> Int = { x,y-> x+y}
//TODO:沒有參數(shù)和返回值的函數(shù)
val action:()-> Unit = { println(42) }
在聲明一個普通函數(shù)時逗威,Unit是可以省略的,但是一個函數(shù)類型聲明總是需要一個顯示的返回類型岔冀,這種情況下Unit是不能省略的
就像其他與法一樣凯旭,函數(shù)類型的返回值也可以標(biāo)記為可空類型
var canReturnNull :(Int,Int) -> Int? = { null }
也可以定義一個函數(shù)類型可空的變量,為了表示是本來那個本身可空使套,而不是返回值可空罐呼,需要用括號將整個函數(shù)類型包裹
var canReturnNull : ((Int,Int) -> Int)? = { null }
8.1.2 調(diào)用作為參數(shù)的函數(shù)
知道了怎么申明一個告誡函數(shù),現(xiàn)在我們來討論如何去實現(xiàn)它侦高,下面是一個filter函數(shù)聲明
fun String.filter(predicate: (Char)->Boolean):String
- String : 接受這類型
- predicate:參數(shù)類型
- ((Char) - > Boolean):函數(shù)類型參數(shù)
- (Char):作為參數(shù)傳遞的函數(shù)的參數(shù)類型
- Boolean:作為參數(shù)傳遞的函數(shù)的返回類型
filter 函數(shù)以一個判斷試作為參數(shù)嫉柴。判斷式的類型是一個函數(shù),以字符作為參數(shù)并返回boolean類型的值奉呛。如果要讓傳遞給判斷式的字符出現(xiàn)在最終返回的字符串中计螺,判斷式需要返回ture,否則為false瞧壮,以下是這個方法的實現(xiàn)
fun String.filter(predicate: (Char) -> Boolean): String {
var stringButter = StringBuffer()
for (index in 0 until this.length) {
var element = get(index)
if (predicate(element)) stringButter.append(element)
}
return stringButter.toString()
}
8.1.4 函數(shù)類型的默認(rèn)參數(shù)值和null值
給函數(shù)類型的參數(shù)指定默認(rèn)值
fun <T> Collection<T>.jointToString(separator: String = ", ", prefix: String = "[", postfix: String = "]", transfrom: (T) -> String = { it.toString() }): String {
val result = StringBuilder(prefix)
println(result)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transfrom(element))
}
result.append(postfix)
return result.toString()
}
使用函可空的參數(shù)
fun <T> Collection<T>.jointToString2(separator: String = ", ", prefix: String = "[", postfix: String = "]", transfrom: ((T) -> String)? = null): String {
val result = StringBuilder(prefix)
println(result)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transfrom?.invoke(element) ?: element.toString())
}
result.append(postfix)
return result.toString()
}
當(dāng)參數(shù)類型可空的時候登馒,你不能直接調(diào)用作為參數(shù)傳遞進(jìn)來的函數(shù),這樣編譯器會報錯咆槽,需要顯示的檢查null:
fun foo (callback: (() -> Unit)?){
if(callback!=null){
}
}
或者有一個更簡單的版本陈轿,kotlin中函數(shù)類型是一個包含了invoke的接口方法的具體實現(xiàn),作為一個方法罗晕,invoke可以通過安全調(diào)用語法被調(diào)用callback?.invoke()
8.1.5 返回函數(shù)的函數(shù)
8.1.6 通過lambda去除重復(fù)代碼
8.2 內(nèi)聯(lián)函數(shù):消除Lambda帶來的運(yùn)行開銷
因為lambda表達(dá)式在運(yùn)行時會被編譯成匿名類济欢,也就是表示每次調(diào)用一次lambda就會創(chuàng)建一個額外的類
當(dāng)一個函數(shù)被聲明成inline時,他的函數(shù)體是內(nèi)聯(lián)的小渊,函數(shù)體會被直接替換到函數(shù)被調(diào)用的地方
8.3 高階函數(shù)中的控制流
8.3.1 lambda中返回語句:從一個封閉的函數(shù)返回
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return
}
}
println("Alice is not Found")
}
注意,如果你在lambda中調(diào)用return關(guān)鍵字酬屉,它會重你調(diào)用lambda的函數(shù)中返回半等,而不是從lambda中返回。這樣的語句叫非局部返回呐萨,因為他不是從一個比包含return的代碼塊更大的代碼塊中返回的
@kotlin.internal.HidesMembers
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
需要注意的是杀饵,只有在以lambda作為參數(shù)的函數(shù)是內(nèi)聯(lián)函數(shù)時才能從外層的函數(shù)中返回,所以上面的forEach是安全的
8.3.2 從lambda中返回:使用標(biāo)簽返回
也可以在lambda中使用局部返回谬擦。lambda中的局部返回和for循環(huán)的break表達(dá)式類似切距,它會終止lambda的執(zhí)行,并接著從調(diào)用lambda中向下執(zhí)行
用一個標(biāo)簽實現(xiàn)局部返回
fun lookForAlice(people: List<Person>) {
people.forEach label@{
if (it.name == "Alice") {
println("Found!")
return@label
}
}
println("Alice is not Found")
}
另一種方式是可以使用函數(shù)名作為標(biāo)簽
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return@forEach
}
}
println("Alice is not Found")
}
如果你顯示的指定了lambda表達(dá)式標(biāo)簽惨远,再用函數(shù)名返回是無效的谜悟,一個lambda表達(dá)式的標(biāo)簽數(shù)量最多一個
8.3.3 匿名函數(shù):默認(rèn)使用局部返回
如果一個lambda中包含多個返回語句會變得非常笨重话肖,解決方案就是用匿名函數(shù)來替換
在匿名函數(shù)中使用return
fun lookForAlice(people: List<Person>) {
people.forEach(fun(person) {
if (person.name == "Alice") return// "return指向最近的函數(shù):fun(person)匿名函數(shù)"
println("${person.name} is not alice")
})
}
在匿名函數(shù)中,不帶標(biāo)簽的return表達(dá)式會從匿名函數(shù)返回葡幸,而不是包含匿名函數(shù)的函數(shù)返回最筒。這條規(guī)則很簡單:return從最近的使用fun關(guān)鍵字聲明的函數(shù)中返回
小結(jié)
- 函數(shù)類型可以讓你聲明一個持有函數(shù)引用的變量、參數(shù)或者函數(shù)返回值
- 高階函數(shù)以其他函數(shù)作為參數(shù)或者返回值蔚叨〈仓可以使用函數(shù)類型作為參數(shù)值或者返回值來創(chuàng)建這樣的函數(shù)
- 內(nèi)聯(lián)函數(shù)被編譯后,他的字節(jié)碼連同傳遞給他的lambda字節(jié)碼都會被插入到調(diào)用函數(shù)的代碼中
- 高階函數(shù)促進(jìn)了一個組件內(nèi)不同部分的代碼重用
- 內(nèi)聯(lián)函數(shù)可以讓你使用非局部返回—在lambda中包含函數(shù)返回的返回表達(dá)式