Kotlin教程

概述

Kotlin的歷史

Kotlin由世界上IDE作得最好的公司JetBrains開發,2010年面向大衆推出,是一門年輕的、現代化的編程語言。Kotlin這個名字來自於JetBrains公司附近的一個島嶼,叫科特林島。估計這幫人沒事就去島上游游泳,釣釣魚,泡泡妹紙,順便寫寫代碼;慢慢就愛上了這個島,用了它的名字。html

JetBrains的IDE作的那麼好,固然最懂開發者的尿性,它發明的語言就是以解決實際開發過程當中的痛點和難點爲目標的。Kotlin可讓你面向多個平臺編寫程序,你能夠用它寫服務端,前端,各系統的原生應用,Android應用。前端

Kotlin在很長一段時間內沒有什麼聲音,直到2017年穀歌在I/O大會上宣佈推薦Kotlin做爲Android開發語言。一石激起千層浪,長江後浪推前浪,Java死在沙灘上。全世界的浪,哦不,開發者開始關注Kotlin,愈來愈多的公司和我的開始嘗試使用Kotlin開發Android應用。java

在2019年的I/O大會上,谷歌再次宣佈Kotlin爲Android開發的首選語言,而且Android官方的類庫代碼將逐漸切換爲Kotlin實現。如今是學習Kotlin的最佳時刻,趕忙滴,再晚就上不了車了!程序員

Kotlin的優點

從目前來看,Kotlin主要用來開發Android應用,而且已經成爲事實上Android開發的首選語言,無論你用不用,學不學,都沒法改變這個局面。根據我的經驗,用Kotlin替代Java編寫基於Spring技術棧的Web應用也很是的爽。一句話,用過都說好,一切能用Java編寫的程序,Kotlin都能作得更好!面試

我是個樂觀派,我認爲Kotlin替代Java只是時間的問題;在Android開發領域已經成爲現實,在Web開發領域,還須要更多人去實踐和推廣。sql

若是你是Android原生應用開發者,那Kotlin必定是最好的選擇;若是你是Java Web開發者,不妨也嘗試一下,說不定就喜歡上了呢!npm

image

對於Android開發,Kotlin擁有如下幾個實實在在的好處:編程

  1. 語法極具表現力和可讀性,這很是有助於咱們構建大型的,可擴展的項目。我用過JavaScript,Go,Python,在使用的體驗和舒服程度上,Kotlin無出其右
  2. 徹底兼容Java,咱們能夠無縫使用現有的Java代碼和類庫
  3. 學習曲線很是平緩,在我學過的全部語言中,Kotlin是最容易上手的
  4. Kotlin能大大減小代碼量,正常狀況下能輕鬆減小30%;更少的代碼意味着更低的Bug率

本教程的優點

Kotlin官方網站已經有教程,爲何重寫一套?設計模式

Kotlin的官方教程重在詳盡的講述全部的語法和特性,有這樣幾個問題:api

  1. 官方教程沒有強調主次輕重之分,咱們學東西的目的就是用最少的時間掌握對咱們有用的知識點;本教程會側重講解開發中最實用的東西,而用不到的東西儘可能少說甚至忽略
  2. 官方教程在語言上平淡枯燥,不夠生動有趣;本教程由資深老司機編寫,帶你領略速度的激情
  3. 官方教程在用例上比較簡潔,不夠深刻到實際的應用場景;本教程力求每一個示例都來自於真實的開發場景

image

準備好了嗎?趕忙上車吧😄!

Hello Kotlin

Hello Kotlin

按照國際慣例,在開始一門語言的學習以前,先來一個Hello World!Kotlin版本的Hello World長這樣:

fun main() {
    var s = "Hello Kotlin"
    println(s)
}

複製代碼

這個Hello World足以展現出Kotlin的簡潔和可讀性了:

  1. fun來聲明函數,短小精悍
  2. main函數不須要參數,這是應該的;能夠想一想咱們何時用過main函數的參數
  3. Kotlin代碼行尾沒有分號,如今都9102年了,難道編譯器還不能自動加分號嗎?
  4. var聲明變量,並且不須要指定變量類型,Kotlin會智能推斷出類型;若是要聲明常量能夠用val

從上面4點能夠看出Kotlin代碼恰到好處,一點也不拖泥帶水。我能用100個4個字的詞誇它,你信不信?

IDE選擇

就像做爲一個劍客,必需要夠賤,哦不,必需要有一把好劍同樣。要想學好一門語言,一款趁心如意的開發工具必不可少。好的IDE會讓你事半功倍,興趣盎然,鬥志昂揚。Kotlin是JetBrains開發的,IDE固然要用JetBrains開發的宇宙最強的開發工具 - IDEA

TIP

IDEA分爲社區版和專業版,社區版免費但功能有限,不過學Kotlin徹底夠用,專業版收費還挺貴。能夠求助於萬能的淘寶,買到便宜又實惠的激活碼。公司有錢的話能夠向公司申請購買正版軟件,支持正版,遠離盜版!

image

點擊這裏進入IDEA官網進行下載,下載的IDEA包含了Kotlin的全部東西,好比Kotlin編譯器和語法提示插件,有了IDEA就能愉快的學習Kotlin了!

建立Kotlin工程

注意

本教程使用的IDEA版本爲2018.3.5,請儘可能不要比個人版本老。若是版本太老,出了幺蛾子,要本身負責任,畢竟都是成年人了。

接下來,我將使用一組圖片描述如何使用IDEA建立Kotlin工程,一圖勝千言。

image

image

image

image

image

建立好的工程界面應該是這樣的:

image

src目錄爲源代碼目錄,咱們在這個目錄下面右鍵便可建立出kotlin file

友情提示

當工程第一次打開時,會嘗試下載Gradle。若是下載失敗,請到Gradle官網自行下載並解壓,而後在第4步中選擇Use local gradle distribution,選擇剛剛解壓的Gradle目錄便可。

接下來,正式開始Kotlin的學習之旅吧。

變量與基本類型

變量聲明

和Java不同,Kotlin使用var聲明變量,使用val聲明不可被更改的變量,變量和類型之間使用:分割。好比:

var name: String = "lxj"
var age: Int = 10
val city = "武漢"
city = "北京" //編譯報錯

複製代碼

Kotlin有強大的類型推斷系統,可以根據變量的值推斷出變量的類型,因此類型每每能夠省略不寫:

var name = "lxj"
var age = 10

複製代碼

數字型

Kotlin的數字類型和Java很是像,提供了以下幾種類型來表示數字:

Type Bit width
Byte 8
Short 16
Int 32
Long 64
Float 32
Double 64

咱們能夠用字面量來定義這些數據類型:

val money = 1_000_000L //極具可讀性
val mode = 0x0F //16進制
val b = 0b00000001 //byte
val weight = 30.6f

複製代碼

Kotlin還提供了這些數據類型之間相互轉換的方法:

println(1.toByte())
println(1L.toInt())
println(1f.toInt())

複製代碼

字符和布爾

Kotlin的字符和布爾,與Java同樣。

var c = 'A'
var isLogin = false

複製代碼

數組

數組在Kotlin中用Array表示,通常咱們這樣建立數組:

val arr = arrayOf(1, 2, 3)
arr[0] //獲取第0個元素
arr.size //數組的長度
arrayOf("a", "b").forEach { //遍歷數組並打印每一個元素
    println(it)
}

複製代碼

Kotlin的Array比Java的Array強大太多,支持不少高階函數,功能幾乎和集合同樣;高階函數的部分在後面的集合章節有更詳細的講述。

字符串

字符串類型是String,用雙引號""表示:

val s = "abc"
s[0]
s.length
s.forEach { println(it) }

複製代碼

Kotlin的字符串是現代化的字符串,支持原始字符串(raw string),用三個引號包起來:

println(""" 牀前明月光,疑是地上霜; 舉頭望明月,低頭思故鄉。 """.trimIndent())  //字符串的內容會原樣輸出

複製代碼

同時Kotlin還支持字符串插值,能夠將變量的值插入到字符串中:

var name = "李曉俊"
var age = 20
println("你們好,我叫$name,我今年${age}歲了。")

複製代碼

區間

區間(Range)嚴格來講不屬於基本類型,但重開一篇又感受殺雞焉用牛刀,因此就放在這了。

區間是用來表示範圍的數據類型,好比從1到5,從AB。它寫起來很是簡單,要表示從1到5的範圍,能夠這樣寫:

var range = 1..5
var range2 = 'A'..'E'

複製代碼

它還有函數形式的寫法是1.rangeTo(5),它們是徹底等同的,但1..5形式看起來簡潔,使用得比較多。

區間實現了Iterable接口,因此是可迭代可遍歷的,可使用for..in或者forEach來遍歷它:

for (i in 1..5){
    println(i)
}
(1..5).forEach {
    println(it)
}

複製代碼

默認狀況下區間是閉區間,也就說1..5是包含1-5的全部值,若是不想包含末尾值,可使用until關鍵字實現:

for (i in 1 until 5){
    println(i) //將不會打印出5
}

複製代碼

區間遍歷時,值是一步一步向上增加的,若是但願每次走2步,可使用step關鍵字實現:

for (i in 1..5 step 2){
    println(i) //將會打印出1,3,5
}

複製代碼

默認的區間是遞增遍歷,若是你須要一個遞減遍歷的區間,可使用downTo作到:

for (i in 5 downTo 1 step 2){
    println(i) //將會打印出5,3,1
}

複製代碼

要判斷一個數字是否在一個區間以內,須要使用in操做符,好比:

println(3 in 1..5) //true
複製代碼

控制流程

If表達式

Kotlin沒有三元運算符,由於它的if/else不只是條件判斷語句,也是一個表達式,有返回值,徹底能夠替代三元運算符。

var age = 30
var name = if (age > 30) "中年" else "青年"

複製代碼

if/else的分支能夠是代碼塊,最後的表達式做爲該塊的值:

var name = if (age > 30) {
    println("我是中年啦,體力不支了")
    "中年"
} else {
    println("我仍是很年輕,精力很充沛哦")
    "青年"
}

複製代碼

When表達式

Kotlin沒有switch,也不須要,由於when表達式足夠強大了。

var cup = 'A'
var say = when(cup){
    'A' -> "通常般啦"
    'B' -> "還不錯哦"
    'C' -> "哇!哇!"
    'D' -> "個人天哪!"
    else -> "有點眼暈!"
}

複製代碼

when的分支條件能夠是任意表達式,而不僅是常量。

var weight = 110
when(weight){
    // in能夠判斷一個值是否在一個區間以內
    in 100..110 -> println("正常")
    in 120..140 -> println("微胖")
}

複製代碼

For循環

for 循環能夠對任何提供迭代器(iterator)的對象進行遍歷,好比數組和集合。對一個區間進行遍歷:

for (i in 1..3) {
    println(i)
}
//向下遞減遍歷,每次減2
for (i in 10 downTo 0 step 2){
    println(i)
}

複製代碼

若是要遍歷一個數組,能夠這麼作:

var arr = arrayOf("A", "B", "C")
for (i in arr.indices){
    println(arr[i])
}

複製代碼

在Kotlin中咱們通常只用for循環遍歷區間,而不去遍歷數組和集合;由於數組和集合有更強大的forEach方法:

arr.forEach { println(it) }

複製代碼

While循環

Kotlin仍然支持while循環和do..while循環。

var i = 5
while(i > 0){
    println(i)
    i--
}
do {
    //retry() //重試請求
} while (i > 0)

複製代碼

中斷與返回

一個典型的例子是在嵌套for循環中,若是想中斷外層循環,能夠這麼作:

out@ for (i in 1..100) {
    for (j in 1..100) {
        if (j>10) break@out
    }
}

複製代碼

再看一個容易讓人迷惑的例子,在一個方法中的for循環內部進行返回,默認返回的是方法:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return // 返回的是foo()的調用
        print(it)
    }
    println("這個打印根本到不了。")
}

複製代碼

這個設計是Kotlin有意爲之的,也是合理的,由於這種邏輯場景下咱們大多數都但願直接返回函數調用。若是真的想返回forEach循環能夠這麼作:

fun foo() {
    listOf(1, 2, 3, 4, 5).forEach {
        if (it == 3) return@forEach // 返回的是foo()的調用
        print(it)
    }
    println("這個打印能夠執行到。")
}
複製代碼

函數

函數聲明

Kotlin使用fun來聲明函數,其中返回值用:表示,參數和類型之間也用:表示。

下面來聲明一個函數,接收一個Int參數,並返回一個Int結果:

//聲明一個方法,接收一個Int類型的參數,返回一個Int類型的值
fun makeMoney(initial: Int) : Int{
    println("make money 996!")
    return initial * 10
}
var money = makeMoney(10)//調用函數

複製代碼

若是一個方法沒有返回值,能夠用Unit表示,等同於Java的void,好比這樣寫:

fun makeMoney(): Unit{
    println("work hard,make no money!")
}

複製代碼

在Kotlin中若是一個方法的返回值是Unit,則能夠省略不寫:

fun makeMoney2(){
    println("work hard,make no money!")
}

複製代碼

默認參數

默認參數是現代化編程語言必備的語法特點。你確定和我同樣,早就厭倦了Java的又臭又長的毫無心義的方法重載。假設咱們要打印學生的信息,若是大部分學生的城市都是武漢,年齡都是18歲,那就能夠用默認參數來定義:

fun printStudentInfo(name: String, age: Int = 18, city: String = "武漢"){
    println("姓名:$name 年齡:$age 城市:$city")
}
printStudentInfo(name = "李雷") //姓名:李雷 年齡:18 城市:武漢
printStudentInfo(name = "韓梅梅", age = 16) //姓名:韓梅梅 年齡:16 城市:武漢

複製代碼

在調用多個參數的函數時,強烈建議像上面那樣使用命名參數傳遞,這樣更具可讀性,並且不須要關心參數傳遞的順序。好比:

printStudentInfo(age = 16, name = "韓梅梅")

複製代碼

單表達式函數

若是函數有返回值而且只有單個表達式,能夠省略大括號,加個=號,像這樣簡寫:

fun square(p: Int) = p * p //無需寫返回值類型,Kotlin會自動推斷

複製代碼

可變參數

Kotlin固然支持可變參數,使用vararg來聲明可變參數。

//可變參數ts,是做爲數組類型傳入函數
fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for (t in ts) // ts is an Array
        result.add(t)
    return result
}
val list = asList(1, 2, 3)

複製代碼

函數式編程

Kotlin支持相似於JavaScript那樣的函數式編程,函數能夠賦值給一個變量,也能夠做爲參數傳遞。

//使用square變量記錄匿名函數
var square = fun (p: Int): Int{
    return p * p
}
println(square(10))

//接收函數做爲參數,onAnimationEnd是函數類型
fun doAnimation(duration: Long, onAnimationEnd: (time: Long)->Unit){
    //執行動畫,動畫執行結束後調用onAnimationEnd
    println("執行動畫")
    onAnimationEnd(duration)
}
doAnimation(2000, { time ->
    println("animation end,time:$time")
})

複製代碼

若是最後一個參數是函數的話,Kotlin有一種更簡潔的寫法,能夠將函數代碼塊直接拿到大括號外面寫:

doAnimation(2000) { time ->
     println("animation end,time:$time")
}

複製代碼

像這種在大括號外面的函數,省略了參數和返回值類型,使用箭頭連接方法體,寫法極其簡潔,又叫作Lambda表達式。

擴展函數

Kotlin的擴展函數是一個很是有特點而且實用的語法,可讓咱們省去不少的工具類。它能夠在不繼承的狀況下,增長一個類的功能,聲明的語法是fun 類名.方法名()

好比:咱們能夠給String增長一個isPhone方法來判斷本身是不是手機號:

fun String.isPhone(): Boolean{
    return length==11 //簡單實現
}
"13899990000".isPhone() // true

複製代碼

再好比給一個Int增長一個方法isEven來判斷本身是不是偶數:

fun Int.isEven(): Boolean {
    return this % 2 == 0
}
1.isEven() // false

複製代碼

有了擴展函數,咱們幾乎不用再寫工具類,它比工具類調用起來會更簡單,而且更天然,就好像是這個對象自己提供的方法同樣。咱們能夠封裝本身的擴展函數庫,使用起來爽翻天。

中綴表達式

中綴表達式是一個特殊的函數,只不過調用的時候省略了.和小括號,多了個空格。它讓Kotlin的函數更富有藝術感和魔力,使用infix來聲明。

來看個例子,咱們給String增長一箇中綴方法愛(),這個方法接收一個String參數:

infix fun String.愛(p: String){
    println("這是一箇中綴方法:${this}$p")
}
//調用中綴方法
"我""你" //這是一箇中綴方法:我愛你

複製代碼

是一個String對象,調用方法,傳入這個參數。

若是將上面的方法增長一個String返回值:

infix fun String.愛(p: String): String{
    println("這是一箇中綴方法:${this}$p")
}
//咱們能夠一直愛到底...
"我""爸爸""媽媽""奶奶"

複製代碼

可見中綴表達式能夠解放你的想象力,讓方法調用看起來跟玩同樣。你能夠創造出不少有意思的東西。不過中綴表達式有一些限制:

  1. 它必須是一個類的成員函數或者擴展函數
  2. 只能接收一個參數
  3. 參數不能是可變參數,而且不能有默認值

Kotlin的中綴表達式可讓咱們像說大白話同樣進行編程(聲明式編程),這種又叫作DSL。Kotlin標準庫中的kotlinx.html大量使用了這種語法,咱們能夠這樣寫html:

html {
    body {
        div {

        }
    }
}

複製代碼

htmlbodydiv都是一箇中綴方法,你能夠試着實現一個簡單的方法。

局部函數

Kotlin支持局部函數,即在一個函數內部再建立一個函數:

fun add(p: Int){
    fun abs(s: Int): Int{
        return if(s<0) -s else s
    }
    var absP = abs(p)
}

複製代碼

局部函數內聲明的變量和數據都是局部做用域,出了局部函數就沒法使用。

尾遞歸函數

有時候咱們會寫一些遞歸調用,在函數的最後一行進行遞歸的調用叫作尾遞歸。若是遞歸的次數過多,會形成棧溢出,由於每一次遞歸都會建立一個棧。Kotlin支持使用tailrec關鍵字來對尾遞歸進行自動優化,保證不會出現棧溢出。

咱們只須要用tailrec修飾一個方法便可得到這種好處,無需額外寫任何代碼:

tailrec fun findGoodNumber(n: Int): Int{
    return if(n==100) n else findGoodNumber(n+1)
}

複製代碼

像上面的函數,使用了tailrec修飾,Kotlin會進行編譯優化,生成一個基於循環的實現。大概相似下面這樣:

fun findGoodNumber(n: Int): Int{
    var temp = n
    while (temp!=100){
        temp ++
    }
    return temp
}

複製代碼

Inline函數

我不打算直接解釋什麼叫內聯函數,先看個例子。假設咱們有一個User對象,須要對它進行一些列賦值以後,去調用它的say方法:

var user = User() // 建立User對象,不須要new關鍵字
user.age = 30
user.name = "李曉俊"
user.city = "武漢"
user.say()

複製代碼

上面的代碼看起來稍顯囉嗦,不夠簡潔。使用apply內聯函數改寫爲:

//使用apply內聯函數進行改寫
User().apply {
    age = 30
    name = "李曉俊"
    city = "武漢"
    say()
}

複製代碼

是否是更加簡潔明瞭了?

內聯函數通常用來簡化對某個對象進行一系列調用的場景。Kotlin提供了大量的內聯函數:applyalsorunwith等,總結起來它們的做用大都是可讓咱們對某個對象進行一頓操做而後返回這個對象或者Unit。而且內聯函數不是真的函數調用,會被編譯器編譯爲直接調用的代碼,並不會有堆棧開銷。

Kotlin的內聯函數在項目中會被大量應用,用得最多的是withapply

類和繼承

和Java同樣,kotlin也使用class聲明類,咱們聲明一個Student類並給它增長一些屬性:

class Student {
    var age = 0
    var name = ""
}
//建立對象並修改屬性
val stu = Student()
stu.name = "lxj"

複製代碼

咱們給Student類增長了非私有屬性,Kotlin會自動生成屬性的setter和getter,經過IDEA提供的工具Show Kotlin Bytecode查看生成的字節碼便可得知。

構造函數

既然是類必定少不了構造函數,想經過構造函數給一個類的對象傳參能夠這樣寫:

class Student (age: Int, name: String) {
    var age = 0
    var name = ""
}

複製代碼

在類名後面加個小括號就是構造函數了,可是類名後面的構造函數不能包含代碼,那麼如何將傳入的agename賦值給本身的屬性呢?

Kotlin提供了init代碼塊用來初始化一些字段和邏輯,init代碼塊是在建立對象時調用,咱們能夠在這裏對屬性進行賦值:

class Student (age: Int, name: String){
    var age = 0
    var name = ""
    //在init代碼塊中來初始化字段
    init {
        this.age = age
        this.name = name
    }
}

複製代碼

可是這樣的寫法略顯囉嗦,Kotlin做爲一種現代化的編程語言,確定有更簡潔的寫法。其實一個類的屬性定義和構造傳參一般能夠簡寫爲這樣:

class Student (var age: Int, var name: String) //加個var關鍵字便可,若是你不想屬性被更改就用val

複製代碼

上面的寫法要求咱們建立對象時必須傳遞2個參數,若是但願傳參是可選的,那麼能夠給屬性設置默認值:

class Student (var age: Int = 0, var name: String = "")
val stu = Student() //不用傳參也能夠
val stu1 = Student(name = "lxj")
val stu2 = Student(age = 20, name = "lxj")

複製代碼

TIP

像上面那樣,若是給構造函數的全部參數都設置了默認值,Kotlin會額外生成一個無參構造,全部的字段將使用默認值。這對於有些須要經過類的無參構造來建立實例的框架很是有用。

通常來講,一個類是能夠有多個構造函數的,那麼Kotlin的類如何編寫多個構造函數呢?

像上面那樣直接在類名後面寫的構造被成爲主構造函數,它的完整語法其實要加個constructor關鍵字:

class Student constructor(var age: Int = 0, var name: String = "")

複製代碼

不過Kotlin規定,若是主構造函數沒有註解或者可見性修飾符(private/public)來修飾,constructor能夠省略。

一個類除了能夠有主構造函數,也能夠有多個次構造函數。使用constructor關鍵字給Student類添加次構造函數:

class Student {
    var age = 0
    var name = ""
    //次構造函數不能經過var的方式去聲明屬性
    constructor(name: String){
        this.name = name
    }
    constructor(age: Int){
        this.age = age
    }
}

複製代碼

若是一個類同時擁有主構造和次構造,那麼次構造函數必需要調用主構造:

class Student (var age: Int, var name: String){
    constructor(name: String) : this(0, name) {
        //do something
    }
}

複製代碼

繼承

默認狀況下,Kotlin的類是不能被繼承的。若是但願一個類能夠被其餘類繼承,須要用open來修飾,繼承用冒號:表示:

open class People
class Student : People()

複製代碼

若是子類和父類都有主構造,則子類必須調用父類的構造進行初始化:

open class People (var name: String)
class Student(name: String) : People(name)

複製代碼

若是子類沒有主構造,那麼次構造必須使用super關鍵字來初始化父類:

open class People (var name: String)
class Student : People{
    constructor(name: String): super(name)
}

複製代碼

既然是繼承,那麼就可能遇到屬性覆蓋和方法覆蓋。

若是想對父類的屬性覆蓋,首先父類的屬性要用open修飾,而後子類的屬性要用override修飾:

open class People (open var name: String)
class Student : People{
    constructor(name: String): super(name)
    override var name: String = "lxj"
}

複製代碼

若是想對父類的方法覆蓋,那麼道理是同樣的:

open class People (open var name: String){
    open fun say(){
        println("i am a people.")
    }
}
class Student : People{
    constructor(name: String): super(name)
    override fun say() {
        println("i am a student.")
    }
}

複製代碼

抽象類

使用abstract來聲明一個抽象類,抽象類的抽象方法無需添加open便可被覆蓋:

abstract class People{
    abstract fun say()
}
class Student : People() {
    override fun say() {
        println("i am a student.")
    }
}

複製代碼

Getters 與 Setters

對於一個類的非私有屬性,Kotlin都會生成默認的settergetter。當咱們對一個對象的屬性進行獲取和賦值,就會調用默認的settergetter

class Student {
    var name: String = ""
}
val stu = Student()
stu.name = "lxj" //會調用name屬性的setter方法
println(stu.name) //會調用name屬性的getter方法

複製代碼

咱們能夠這樣自定義一個字段的settergetter

class Student {
    var name: String = ""
        //field是個特殊標識符,專門用在setter和getter中,表示當前字段
        get() = if(field.isEmpty()) "LXJ" else field
        set(value) {
            field = value.toLowerCase() //將名字變成小寫
        }
}

複製代碼

延遲初始化

你可能注意到我在定義類的屬性時,常常給屬性設置默認值:

class Student {
    var name: String = "" //若是不賦值,會編譯報錯
}

複製代碼

這是由於Kotlin要求顯式地對屬性進行賦值,但不少時候咱們不想一上來就給默認值,但願感情到了待會兒再初始化這個屬性。那麼可使用lateinit來聲明這個屬性:

class Student {
    lateinit var name: String //告訴編譯器待會兒初始化這個變量
}

複製代碼

可是lateinit聲明的變量有個不爽的地方,就是當你用到這個變量的時候,若是這個變量尚未被初始化,你將會收穫一個異常:

kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized

複製代碼

也許你但願的是,當你用到這個變量時,若是變量尚未被初始化,那應該獲得一個null,而不該該報異常。

這個想法很美好,可是和Kotlin的類型系統相互衝突了。Kotlin中增長了可空類型和非空類型的定義,像上面那樣咱們聲明一個name屬性爲String類型,是在告訴編譯器name是非空類型,因此若是沒有初始化,Kotlin不會給你一個null,而是直接GG

至於可空類型如何定義,你如今只須要簡單的知道String?就是可空類型,後面咱們會專門討論可空類型的使用。

爲了不你在初始化一個變量以前就使用它而致使GG,Kotlin給每一個變量增長了一個屬性isInitialized來判斷這個變量是否初始化過:

fun printName(){
    if(this::name.isInitialized){ //若是初始化過再使用,可避免GG
        println(name)
    }
}

複製代碼

數據類

Java中有一個著名的名詞叫JavaBean,就是一個用來描述數據的類。咱們定義一個類Person用來描述人的信息,這個類就是一個JavaBean,Kotlin叫數據類。

先來定義一個普通的類:

class People(var name: String, var age: Int)

複製代碼

上面的寫法Kotlin會生成字段的settergetter;數據類還要求這個類有hashCode()equals()toString()方法,只需添加一個data關鍵字就變成數據類了:

data class People(var name: String, var age: Int)

複製代碼

就是這麼簡潔,經過Show Kotlin Bytecode工具能夠查看生成的字節碼。

來一個Java版本的對比一下,就能感覺到Kotlin的data class有多強大:

public class People {
    private String name;
    private int age;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        People people = (People) o;
        return age == people.age &&
                name.equals(people.name);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
    @Override
    public String toString() {
        return "People{" +
                "name='" + name + '\'' + ", age=" + age + '}'; } } 複製代碼

嵌套類和內部類

嵌套類就是一個嵌套在類中的類,但並不能訪問外部類的任何成員,在實際開發中用的不多:

class Student {
    var name: String = ""

    class AddressInfo{
        var city: String = ""
        var province: String = ""
        fun printAddress(){
            //
        }
    }
}
//調用嵌套類對象的方法
Student.AddressInfo().printAddress()

複製代碼

內部類使用inner class聲明,能夠訪問外部類的成員:

class Student {
    var name: String = ""

    inner class AddressInfo{
        var city: String = ""
        var province: String = ""
        fun printAddress(){
            println(name)
        }
    }
}
//調用內部類對象的方法
Student().AddressInfo().printAddress()

複製代碼

內部類在Android中用的比較多,好比在Activity中建立Adapter類,此時Adapter能夠直接訪問Activity的數據,很是方便。

枚舉類

Kotlin的枚舉和Java很像。直接看例子:

enum class Position{
    Left, Top, Right, Bottom
}
val position = Position.Left

複製代碼

自定義枚舉的值:

enum class Position(var posi: String){
    Left("left"), Top("top"), Right("right"), Bottom("bottom")
}
複製代碼

接口

聲明與實現

Kotlin的接口和Java8的接口很像,能夠聲明抽象方法和非抽象方法。

interface Animal{
    fun run()
    fun eat(){
        println("吃東西")
    }
}

複製代碼

編寫一個類實現接口:

class Dog : Animal{
    override fun run() {
        println("i can run")
    }
}

複製代碼

接口繼承

接口之間能夠進行繼承,語法和類的繼承同樣:

interface Animal{
    fun run()
    fun eat(){
        println("吃東西")
    }
}
interface LandAnimal : Animal{
    fun dig(){
        println("我會挖洞")
    }
}
class Dog : LandAnimal{
    override fun run() {
        println("i can run")
    }
}
Dog().dig()

複製代碼

多繼承

在大型項目中,咱們的類可能會繼承多個接口,而多個接口可能會存在重複的方法。好比:

interface A {
    fun eat()
    fun run(){
        println("A run")
    }
}

interface B {
    fun eat(){
        println("B eat")
    }
    fun run(){
        println("B run")
    }
}

複製代碼

A和B接口有着重複的方法,A實現了run方法,B實現了eatrun方法。假設類C同時繼承了A和B,那麼它須要實現這兩個方法。好比:

class C : A, B{
    override fun run() {
        println("c run")
    }
    override fun eat() {
        println("c eat")
    }
}

複製代碼

若是類C在run方法中想調用父類對run的實現,應該怎麼作呢?

你很快會猜到應該這樣寫:

class C : A, B{
    override fun run() {
        super.run() //會編譯出錯
        println("c run")
    }
    override fun eat() {
        println("c eat")
    }
}

複製代碼

可是事與願違,直接調用super.run()會編譯報錯,緣由是A和B都實現了run,編譯器搞不懂你的super.run()是要調用誰。因此須要明確指定咱們要調用誰的實現,好比想調用A的實現,代碼以下:

override fun run() {
    super<A>.run() //編譯經過
    println("c run")
}

複製代碼

若是C想在eat方法中調用父類的實現,則直接調用super.eat()便可,由於2個父類中只有B實現了eat方法,編譯器能肯定調用的是誰。好比:

override fun eat() {
    super.eat() //編譯經過
    println("c eat")
}
複製代碼

泛型

聲明泛型

泛型能夠大大提升程序的動態性和靈活性。在Kotlin中聲明泛型和Java相似:

class MyNumber<T>(var n: T)
//傳入參數,若是類型能夠推斷出來,則能夠省略
MyNumber<Int>(1)
MyNumber(1) //Int能夠不寫
MyNumber<Float>(1.2f) //Float也能夠不寫

複製代碼

泛型賦值

來看一個例子,這個例子說明了父類泛型並不能直接接收子類泛型:

var n1 = MyNumber<Int>(1)
var n2: MyNumber<Any> = n1 //編譯報錯,儘管Any是Int的父類,Any至關於Java的Object

複製代碼

上面的例子在Java中也是沒法編譯經過的,在Java中須要這樣作:

ANumber<? extends Object> n2 = n1;

複製代碼

Kotlin提供了out關鍵字,out T表示能夠接收T以及T的子類:

var n1 = MyNumber<Int>(1)
var n2: MyNumber<out Any> = n1 //編譯經過

複製代碼

再來看一個方法:

fun fill(dest: ArrayList<String>, value: String){
    dest.add(value)
}
fill(arrayListOf<String>(), "22") 
fill(arrayListOf<CharSequence>(), "22") //編譯出錯,儘管String是CharSequence的實現類

複製代碼

上面的方法將一個String裝入ArrayList<String>,但有時候咱們但願fill方法也能接收泛型是String父類的集合,此時可使用in String,表示接收String以及它的父類:

fun fill(dest: ArrayList<in String>, value: String){
    dest.add(value)
}
fill(arrayListOf<CharSequence>(), "22") //編譯經過

複製代碼

in關鍵字對應了Java中的ArrayList<? super String>

泛型通配符

在Java中若是咱們但願一個泛型能夠接收全部類型,通常可使用通配符?

ANumber<?> n2 = new ANumber<Integer>(1);
n2 = new ANumber<Float>(1.2f);

複製代碼

在Kotlin中用*表示通配符:

var n2: MyNumber<*> = MyNumber<Int>(1)
n2 = MyNumber(1000L)

複製代碼

泛型函數

除了類上面能夠聲明泛型,函數也能夠聲明泛型:

fun <T, R> foo(t: T, r: R){
}
//調用函數
foo<Int, String>(1, "2")
複製代碼

強大的object

object表達式

不少時候咱們想對一個類進行輕微改動(好比重寫或實現某個方法),但不想去聲明一個子類。在Java裏面通常會使用匿名內部類,在Kotlin中使用object關鍵字來聲明匿名類的對象:

Collections.sort(listOf(1), object : Comparator<Int>{
    override fun compare(o1: Int, o2: Int): Int {
        return o1 - o2
    }
}) 

複製代碼

有時候咱們只須要一個臨時對象,封裝一些臨時數據,而不想爲這個對象單獨去定義一個類。object也能夠作到:

var obj = object  {
    var x: Int = 0
    var y: Int = 0
}
obj.x = 12
obj.y = 33

複製代碼

在Java中,匿名內部類訪問了局部變量會要求這個變量必須是final的,若是後面又須要對這個變量進行更改的話會很是不方便。在Kotlin中則沒有這個限制:

fun calculateClickCount(view: View){
    var clickCount = 0 
    view.setOnClickListener(object : OnClickListener{
        override fun onClick(v: View){
            clickCount ++ //能夠直接訪問和修改局部變量
        }
    })
}

複製代碼

單例聲明

單例模式是一種很是有用的設計模式。在Java中實現單例並非很簡單,有時候還要考慮併發問題。成千上萬富有智慧的Java程序員創造了多種定義單例的方式,甚至還起了個高大上的名字懶漢式餓漢式;在Java面試題中單例的實現方法出現的頻率也很是高。

先看Java中一種典型的餓漢式定義單例的方式:

class HttpClient{
    private HttpClient(){}
    private static HttpClient instance = new HttpClient();
    public static HttpClient getInstance(){
        return instance;
    }
}

複製代碼

好的編程語言會盡量的幫程序員作事情,解放程序員的心智負擔。在Kotlin中定義單例只須要使用object關鍵字聲明便可,無需額外作任何事情:

object HttpClient{
    fun executeRequest(){
        //執行請求   
    }
}
//調用單例對象的方法,雖然看起來像靜態調用,但其實是對象調用
HttpClient.executeRequest()

複製代碼

object聲明單例不但簡潔,並且線程安全,這一切由Kotlin的編譯器技術來保證。若是你感興趣底層是如何實現的,能夠經過Show Kotlin Bytecode查看,會發現原來Kotlin幫咱們幹了Java版本的實現。

伴生對象

Kotlin中可使用companion object聲明一種特殊的內部類,並且內部類的類名能夠省略,這個內部類的對象被稱爲伴生對象:

class HttpClient {
    //注意:伴生類並不能訪問外部類的成員和方法
    companion object {
        fun create(){
        }
    }
}
//調用伴生對象的方法
HttpClient.create()

複製代碼

伴生對象調用方法看起來像單例調用和靜態調用,但並非;仍是內部類的實例對象調用。那這有什麼卵用呢?

用處就是實現真正的靜態調用。

Kotlin中並無提供直接能進行靜態調用的方法,對伴生類的成員和方法添加@JvmStatic註解,就能夠實現真正的靜態調用:

class HttpClient {
    //注意:伴生類並不能訪問外部類的成員和方法
    companion object {
        @JvmStatic var DefaultMethod = "Get"
        @JvmStatic fun create(){
        }
    }
}
//真正的靜態變量
HttpClient.DefaultMethod
//真正的靜態方法調用
HttpClient.create()

複製代碼

Kotlin爲何沒有提供更簡潔的靜態調用呢?

它確定能夠作到,既然沒有提供,我我的猜測是不提倡靜態類的編寫。由於它提供的單例調用和伴生對象調用在便利性上面和靜態調用是同樣的,調用者使用起來足夠方便,沒有必要要求必定是靜態的內存分配。

集合與高階函數

建立集合

Kotlin和大多數編程語言同樣,有三種集合:List,Set,Map,但Kotlin的集合區分可變集合和不可變集合。

建立可變集合:

var arrayList = arrayListOf(1.2f, 2f)
var list = mutableListOf("a", "b")
list.add("c")
var set = mutableSetOf(1, 2, 2, 3)
set.add(1)
println(set) //[1, 2, 3]
var map = mutableMapOf<String, String>(
    "a" to "b",
    "c" to "d"
)

複製代碼

建立不可變集合:

//不可變集合,沒有add,remove,set之類的方法,不能修改元素
var list2 = listOf("a") 
list2[0]
var set2 = setOf("b")
var map2 = mapOf("a" to "b")

複製代碼

高階函數

Kotlin的集合提供了很是多強大的擴展函數,容許咱們對集合數據進行各類增刪改查,過濾和篩選。

  1. 遍歷

    list.forEach { println(it) }
    //帶索引遍歷
    list.forEachIndexed { index, s -> println("$index - $s") }
    
    複製代碼
  2. 查詢

    list.find { it.contentEquals("a") } //找到第一個包含a的元素
    list.findLast { it.contentEquals("a") } //找到最後一個包含a的元素
    list.first { it.startsWith("a") } //找出第一個知足條件的,找不到拋出異常
    list.last { it.startsWith("a") } //找出最後一個知足條件的,找不到拋出異常
    
    複製代碼
  3. 刪除

    list.removeAll { it.startsWith("a") } //刪除全部以a開頭的元素 
    
    複製代碼
  4. 過濾

    list.filter { it.contains("a") } //獲取全部知足條件的元素
    list.filterNot { it.contains("a") } //獲取全部不知足條件的元素
    
    複製代碼
  5. reduce操做

    list.reduce { acc, s ->  acc + s }
    //反向reduce
    list.reduceRight { s, acc ->  acc + s}
    
    複製代碼
  6. map操做

    list.map { println(it.toUpperCase()) }
    //flatMap
    list.flatMap { it.toUpperCase().toList() }
    
    複製代碼
  7. 其餘

    //打亂排序
    list.shuffle()
    //替換
    list.replaceAll { if(it=="a") "A" else it }
    list.any { it.contentEquals("a") } //只要有一個元素符合條件就返回true
    list.all { it.contentEquals("a") } //是否全部元素都符合條件
    
    複製代碼

更多的高階函數等待你去嘗試,篇幅有限,我只能寫到這了。

空安全

非空類型與可空類型

在不少編程語言中,若是咱們訪問了一個空引用,都會收穫一個相似於NullPointerException的空指針異常。Kotlin的類型系統區分可空類型和非空類型,來盡力避免空指針異常。

定義一個非空類型和可空類型:

var name: String = "" //定義非空類型name
name = null //非空類型賦值null,編譯出錯

var name2: String? //定義可空類型
name2 = null  //能夠賦值null

複製代碼

對於非空類型,咱們能夠放心訪問它的屬性和方法,保證不會出現空指針。

name.length
name.slice(0..2)

複製代碼

對於可空類型,直接訪問它的屬性和方法有可能收穫空指針,並且編譯器會直接報錯;但咱們仍是須要訪問。通常有兩種方式來避免空指針:空檢查和使用安全調用符?.

空檢查

空檢查很好理解,咱們在Java中也是這樣作的。

name2.length //直接訪問,編譯報錯
if(name2!=null){
    name.length
}

複製代碼

安全調用符

能夠這樣來訪問成員:

name2?.length //若是name2爲null,則返回null

複製代碼

能夠鏈式調用:

name2?.substring(3)?.length //只要有一個爲null,就返回null
name2?.substring(2) //若是name2爲null,則不會執行函數調用

複製代碼

Elvis 操做符

當咱們有一個可空類型時,常常會遇到這樣的邏輯:若是它不是空,就用它;不然使用另一個。

if/else寫就是這樣的:

val l = if(name2!=null) name2.length else -1

複製代碼

用Elvis操做符?:能夠簡寫爲:

val l = name2?.length ?: -1

複製代碼

它還能夠用在不少這種相似邏輯的場景:

fun findFocusChild(view: View){
    val focusChild = view.getFocusChild() ?: return
    val visibility = focusChild.getVisibility() ?: throw IllegalArgumentException("not visible.")
}

複製代碼

! ! 操做符

!!操做符也叫非空斷言運算符。由上面得知,當咱們訪問一個可空類型的成員或者函數時,可使用空檢查或者安全調用符。但若是你很是肯定這個變量必定不爲空時,也可使用!!來進行調用:

var name2: String? = null
//其餘賦值邏輯
println(name2!!.length) // 這樣也能夠避免編譯報錯

複製代碼

!!操做符的安全性徹底由你本身的邏輯保證,編譯器不會進行任何的非空判斷。這意味着,若是你的邏輯不夠嚴謹,也就是若是name2若是爲空,你仍然會收穫一個NPE。

而使用安全調用符則能夠保證不出現NPE,!!在實際開發中用的不多,除非你能保證它不爲空才能夠用。

安全的類型轉換

接收參數,處理參數,而後輸出結果,這是咱們軟件開發的基本流程。但有時候接收的參數類型並非很肯定,好比咱們原本想要對String進行操做,接收到的是Any參數,但咱們覺得接收的是一個String。代碼以下:

var param: Any?  = getParam()
val s = param as String //as是類型轉換標識符

複製代碼

若是param真的是一個String,則程序正常工做。但若是它是一個Int呢?又或者它爲空呢?這些狀況就會致使類型轉換失敗,收穫ClassCastException

咱們沒法保證接收的參數一切正常,但可使用as?來進行安全的類型轉換:

val s = param as? String 

複製代碼

param不是String或者爲空,變量s則爲null,而程序並不會出現異常。

代理

代理

什麼是代理?

代理就是你想去找老婆,可是你如今沒有找老婆的功能(好比不認識女生,沒有女生的聯繫方式),而媒婆有這個功能,那媒婆就是一個代理對象。當你要找老婆時,無需本身去實現找老婆的功能,直接調用媒婆的功能便可。

代理設計模式已經被普遍的應用在各個語言的程序當中,好比Java的Spring技術棧,Android的Retrofit網絡框架。代理模式能夠將調用主體和代理對象的職責分離,有助於項目的維護。

Kotlin中提供了by關鍵字,直接從語言層面支持的代理模式,無需咱們額外編寫任何代碼。Kotlin的代理分兩種:類代理和屬性代理。

類代理

類代理也能夠看作另外一種實現繼承的方式,由於它可讓一個類擁有另一個類的功能。

先來定義一個接口,接口表明着一種能力:

//碼農的功能
interface Coder {
    fun writeCode()
}

複製代碼

如今有個類想擁有Coder的能力:

class Student : Coder

複製代碼

而目前已經有別的類實現了Coder能力:

class A : Coder {
    override fun writeCode() {
        println("write code very happy!")
    }
}

複製代碼

此時,Student類就不必本身再實現一遍,能夠將A的對象做爲本身的代理對象,讓代理對象幫助咱們實現。使用by關鍵字就能夠作到:

class Student(c: Coder) : Coder by c
//調用方法,實際上調用了代理的方法
Student(A()).writeCode() //write code very happy!

複製代碼

固然若是你願意,也能夠選擇覆蓋代理對象的某個方法實現:

class Student(c: Coder) : Coder by c {
    override fun writeCode() {
        println("write code 996!")
    }
}
Student(A()).writeCode() //write code 996!

複製代碼

可是若是代理對象的方法引用了它本身的屬性,咱們在本身類中覆蓋這個屬性則是不會生效的:

interface Coder {
    val company: String
    fun writeCode()
}
class A : Coder {
    override val company = "華爲"
    override fun writeCode() {
        println("write code at $company!")
    }
}
class Student(c: Coder) : Coder by c {
    override val company = "阿里巴巴"
}
Student(A()).writeCode() //write code at 華爲!

複製代碼

其根本緣由是最終調用的是代理對象的方法,並非本身的方法,所以使用的變量仍然是代理對象本身的。

屬性代理

屬性代理可讓咱們使用另一個類對象來代理屬性的Setter和Getter。

來看一個User類,它有一個name屬性:

class User {
    var name: String 
}

複製代碼

假設咱們並不想去關心name屬性的Getter邏輯和Setter邏輯(好比範圍檢查之類的邏輯),而是但願讓別的代理類來作,此時就能夠編寫一個屬性代理類。

屬性代理類不須要實現任何接口,只須要提供getValue()setValue()方法便可,分別對應屬性的Getter和Setter。好比:

class NameDelegate {
    private var _value = "defaultValue"
    //當訪問屬性的getter時調用
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("get -> $thisRef '${property.name}' ")
        return _value
    }
    //當訪問屬性的setter時調用
    operator fun setValue(thisRef: Any?, property: KProperty<*>, value: String) {
        //若是不爲空就設置
        if(value.isNotEmpty()) _value = value
        println("set -> $value want set to '${property.name}' in $thisRef.")
    }
}

複製代碼

NameDelegate的做用很是簡單,只有傳入的值不是空字符串才進行賦值,不然取值時就返回默認值,默認值目前是寫死的,能夠經過構造參數傳入。

接下來使用這個屬性代理,並對User對象的屬性進行訪問:

class User {
    var name: String by NameDelegate()
}
var user = User()
user.name = "123" //輸出:set -> 123 want set to 'name' in User@3af49f1c.
user.name //輸出:get -> User@3af49f1c 'name' 

複製代碼

上面就是一個屬性代理的基本使用,看起來好像跟直接重寫屬性的Setter和Getter並無太大區別。那屬性代理有什麼好處呢?

答案是屬性代理將對屬性的訪問邏輯抽成一個獨立的類,便於複用。假設項目中有10個類的某個屬性訪問邏輯須要自定義時,用Setter和Getter須要在每一個類中寫一遍,而屬性代理只須要寫一次便可。

內置代理

標準庫已經封裝了幾種代理,說說其中2個比較經常使用的:lazy代理Observable代理

lazy代理專門用於屬性的延時初始化場景,好比有個集合不想一開始就初始化,等到咱們第一次使用它時再進行初始化,好處是能夠節省初始內存。lazy只能用在val變量上面,它接收一個函數,將函數的返回值做爲屬性的值。來看看如何使用:

class User {
    val age: Int by lazy {
        println("do something")
        10
    }
}
var user = User()
println(user.age) //只會打印一次 do something
println(user.age)

複製代碼

值的延遲計算默認是線程安全的,若是你肯定你是在單線程場景,能夠給lazy傳入一個參數來取消這個安全,得到一些性能上的提高:

class User {
    val age: Int by lazy(mode = LazyThreadSafetyMode.NONE) {
        println("do something")
        10
    }
}

複製代碼

Observable代理通常用在咱們想在屬性值更改時執行一些邏輯的場景,它接收一個屬性初始值和屬性更改時的處理函數,每次屬性被賦值時都會執行這個函數。

來看看Observable代理的用法:

class User {
    var age: Int by Delegates.observable(10){
        property, oldValue, newValue ->
        println("${property.name}的值從${oldValue}修改成$newValue")
    }
}
var user = User()
user.age = 11 //age的值從10修改成11
user.age = 15 //age的值從11修改成15

複製代碼

在Android開發中,lazy代理用的會比較多。其實屬性代理功能很是強大,能夠用來實現MVVM架構,須要實現一個VM層將類的屬性和UI映射起來,監聽數據的屬性變化,當值被更改時去更新對應UI。

Android官方爲了方便你們開發,提供了Jetpack類庫,其中的LiveData框架是用Java實現的一個MVVM框架,若是用Kotlin代理來作會更簡單一些。

其餘語法

This 表達式

在類中,this表示當前類對象的引用。在多層嵌套類中,咱們可使用this@類名來明確指定要訪問的是哪一個類的對象:

class A { 
    inner class B { 
        //注意:foo是一個Int的擴展方法
        fun Int.foo() { // 隱式標籤 @foo
            val a = this@A // A 的 this
            val b = this@B // B 的 this
            val c = this // foo() 的接收者,一個Int對象
        }
    }
}

複製代碼

is 與 !is 操做符

使用is來判斷對象是不是某個類型;!is語氣則相反。

var s: Any = "ss"
println(s is String)
println(s !is Int)

複製代碼

異常

Kotlin的異常體系和Java相似,代碼以下:

throw Exception("boom!") //拋出異常
try {
    // 一些代碼
}
catch (e: SomeException) {
    // 處理程序
}
finally {
    // 可選的 finally 塊
}

複製代碼

所不一樣的是,Kotlin的try/catch是一個表達式,有返回值。它的返回值是try代碼塊中最後一個表達式的值,或者catch代碼塊中最後一個表達式的值。

val a: Int? = try { parseInt(input) } catch (e: NumberFormatException) { null }

複製代碼

Kotlin的異常還有一個好處就是:在有異常拋出的方法中,無需在方法上面顯式的再拋出異常。這在Java中是必須作的,有時候你調用了一個會拋出異常的方法,若是咱們不try/catch就必須顯式拋出。

Kotlin認爲這種異常規範對於小項目有用,但對於大項目會致使生產力下降和代碼質量降低。示例以下:

fun foo(s: String) { //方法上不在顯式拋出異常
    if(s.isEmpty()) throw IllegalArgumentException("s can not be empty!")
}
複製代碼

協程(Coroutine)

概念

什麼是協程呢?

簡單說,協程是比線程更輕量的,有狀態,可暫停可恢復的任務單元。

如何理解任務單元呢?

拿作飯來講,將作飯當作一個任務。爲了提升作飯的效率,咱們會把作飯分紅不少小的任務單元:洗菜,切菜,煮米飯,準備配料,炒菜。而後大家全體家庭成員共同上陣,你負責洗菜,爸爸負責煮米飯和準備配料,媽媽負責切菜和炒菜。這些任務有些是能夠並行的,好比洗菜和煮米飯;有些是串行的,好比洗菜和切菜。大家一塊兒工做,能大大提升作飯的效率。

對於操做系統而言,進程是運行每一個程序的任務單元。每一個應用程序都在本身的進程中運行,狀態和數據相互隔離,穩定運行;一個程序崩潰了不會影響其餘程序運行。這些程序是併發運行的。

對於進程而言,爲了提升程序的運行速度,咱們會將一些耗時的任務分離爲更小的任務單元,就是線程。多個線程併發工做,能大大加快總體任務的執行速度。

既然進程和線程都能經過併發執行提升運行效率,那協程有什麼優點呢?通常有2個:

  • 更小的內存開銷。進程和線程的內存開銷比較大;通常的電腦能夠開1000000個協程也沒太大問題,可是開10000個線程內存估計就爆掉了,而進程的內存開銷更大
  • 沒有上CPU下文切換帶來的性能開銷。線程和進程由CPU來調度執行,每一個CPU會執行多個線程,每當切換新線程時,須要先存儲當前線程的狀態,再加載新線程的狀態;在頻繁的調度下,切換線程消耗CPU的不少性能。而協程由應用程序控制狀態的切換,性能開銷要小不少

雖然多線程也能很好進行併發編程,但協程的併發會消耗更少的資源,有更高的調度性能。這對於服務器處理高併發的場景會帶來很大的優點。

協程是如何實現的

目前我所知道的支持協程的語言有Python, NodeJs,Go和Kotlin。簡單講,原理大都是OS Thread Pool配合狀態機來實現的。協程底層仍然是靠線程池調度,靠狀態機來維護狀態。具體實現上每一個語言都不盡相同,這些細節暫不深究。

拿上面作飯的例子來講,作飯被分割成了不少的task,這些task由大家全家人一塊兒調度。那大家全家人就至關於線程池,這些task就比如是不少個協程。爸爸可能調度多個協程,由於可能很快完成本身的,接着去作別的。爸爸也可能中途暫停煮飯協程,先執行切菜的協程,而後再回頭恢復煮飯的協程。

因爲協程可暫停和可恢復的特性,能直接消除異步回調,讓咱們用同步寫法編寫異步執行代碼。不少編程語言在處理異步任務結果時都採用Callback的方式,好比早期的JavaScript。當邏輯複雜的時候,很容易陷入回調地域,致使代碼可讀性差,可維護性低。來個Kotlin協程的代碼示例:

fun main() {
    GlobalScope.launch { 
        var url = "http://www.lixiaojun.xin"
        //等待異步請求返回,無需Callback
        var result = request(url).await()
        println("請求結果爲:$result")
    }
}

複製代碼

綜上所述,協程有如下幾個有點:

  • 更少的資源消耗和更高的調度性能
  • 用同步的方式寫異步代碼,可讀性更好
  • 協程比線程更容易使用,不須要關心過多的狀態,直接編寫邏輯便可

第一個協程

協程不屬於Kotlin標準庫,須要添加依賴才能使用。在build.gradle文件中添加協程的依賴:

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.0'
}

複製代碼

編寫一個協程程序,並在協程中延時1秒:

fun main() {
    // 在後臺啓動一個新的協程
    GlobalScope.launch {
        delay(1000L) // 掛起當前協程,但並不會阻塞程序,1秒後恢復執行
        println("World!") //延遲1秒後打印
    }
    println("Hello,") // 會當即打印,協程的delay並不會阻塞程序
    Thread.sleep(2000L) // 阻塞主線程 2 秒鐘來保證 JVM 存活,不然的話協程還未恢復執行,進程就退出了
}
//輸出
Hello,
World!

複製代碼

能夠看到開啓協程很簡單,咱們不用關心哪一個線程在調度協程,也不用關心協程的狀態,只須要專心編寫咱們的異步邏輯便可。

delay是一個suspend關鍵字修飾的掛起函數,會暫停當前協程的執行,但並不阻塞主線程往下進行;等時間到,便恢復執行。

主協程

因爲上面的協程沒法阻塞住當前線程,咱們使用Thread.sleep()來阻塞線程,使得協程有機會獲得執行。Kotlin提供了一個特殊的主協程能夠阻塞主線程:

fun main() = runBlocking { //開啓主協程
    GlobalScope.launch { //開啓子協程
        delay(1000L) // 掛起當前協程,但並不會阻塞程序,1秒後恢復執行
        println("World!") //延遲1秒後打印
    }
    println("Hello,") // 會當即打印,協程的delay並不會阻塞程序
}

複製代碼

runBlocking開啓的爲主協程,因爲GlobalScope.launch是在一個協程中開啓協程,所以咱們叫它子協程。

可是上面的World仍然不會獲得執行,由於主協程瞬間就執行完畢,並不會等待GlobalScope開啓的子協程執行完成才結束。主協程一旦結束,主線程就執行結束,整個程序就結束。

有兩種方式可讓主協程等待子線程執行完成才結束:一種是使用delay函數掛起主協程,另外一種是讓子協程join主協程中。

先看第一種,使用delay函數掛起主協程,掛起的時間要大於子協程掛起的時間:

fun main() = runBlocking { 
    GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    delay(2000) //掛起主協程,等待子協程執行完畢
}
//輸出
Hello,
World!

複製代碼

另一種,使用一個變量記住GlobalScope.launch開啓的協程的引用:

fun main() = runBlocking {
    val job = GlobalScope.launch {
        delay(1000L)
        println("World!")
    }
    println("Hello,")
    job.join() //等待子協程執行完才結束
}
//輸出
Hello,
World!

複製代碼

看起來,使用join方法更加優雅。

協程存活期

繼續上面的例子,咱們剛纔得出GlobalScope.launch開啓的子協程並不能阻塞主它的父協程。但仔細想一想這不合理。

假設邏輯再複雜一些,在剛纔的主協程中,咱們開啓5個子協程。那就必須手動持有5個子協程的引用,不然沒法保證讓每一個協程獲得執行。若是咱們忘記持有某個協程的引用,那麼這個協程的代碼就報廢了,由於沒法獲得執行。若是真的是這樣的話,那出錯的機率仍是很大的。難道父協程不能自動的等全部子協程執行完畢才結束嗎?實際上是能夠的。

爲何上面的例子不行呢?每一個協程是有存活期的,在一個協程中開啓的子協程的存活期通常不會超過其父協程的存活期。可是GlobalScope比較特殊,它開啓的是頂級協程。頂級協程的存活期由整個應用程序管理,並不受主協程限制,至關於直轄市。頂級協程雖然在主協程內部開啓,可是在存活期和做用域上和主協程平級,所以它沒法阻塞主協程,須要咱們手動的join或者delay主協程。

每一個協程對象都是CoroutineScope實例,CoroutineScope有個launch方法用來在本身的做用域內開啓一個受本身管轄的子協程。並且會自動的等全部子協程執行完畢才結束。將上面的例子稍作改動就能夠:

fun main() = runBlocking {
    //去掉了GlobalScope
    val job = launch { //在本身的做用域內開啓子協程
        delay(1000L)
        println("World!")
    }
    println("Hello,")
//    job.join() 無需join了
}
//輸出
Hello,
World!

複製代碼

Kotlin不建議咱們直接使用GlobalScope開啓頂級協程,一般應該直接使用launch方法在本身的做用域內開啓子協程,這樣不容易出錯。

取消與超時

協程一般用來執行耗時操做。 在Android開發中,咱們在一個界面開啓協程進行耗時請求。假如此時用戶關閉了界面,那麼協程的執行結果已經不須要了,所以協程應該是能夠被取消的。

協程提供了cancel()方法來取消:

fun main() = runBlocking {
    val job = launch {
        println("i am working...")
        delay(2000L)
        println("work done!") //將不會輸出
    }
    delay(1000)
    job.cancel() //取消協程
}

複製代碼

有時候耗時操做的時間是不肯定的,好比在Android發起一個網絡請求,咱們並不肯定它何時會返回,所以超時的處理是必要的。咱們假設若是請求超過10秒鐘未返回結果,用戶已經沒有耐心等待了,此時應該結束這個協程了。

使用withTimeout來開啓帶超時限制的協程:

withTimeout(5000){
    println("start request")
    delay(120000) //延時12秒,模擬巨慢的弱網環境
    println("get result!")
}

複製代碼

協程的超時會拋出TimeoutCancellationException異常。若是你不喜歡拋出異常的方式,可使用withTimeoutOrNull的方式開啓協程,若是協程超時則返回null,這樣就再也不有異常了。

val result = withTimeoutOrNull(5000){
    println("start request")
    delay(120000) //延時12秒,模擬巨慢的弱網環境
    println("get result!")
}
println(result) //null

複製代碼

suspend函數

使用suspend修飾的函數叫作掛起函數,delay就是一個掛起函數。因爲咱們不可能將全部異步邏輯都寫到協程中,必然要重構和抽取。好比:

val job = launch { 
    //執行網絡請求
    var result = doRequest() 
    println(result)
}
fun doRequest(): String{
    return "請求的結果"
}

複製代碼

假設全部的耗時請求都抽取到doRequest方法中,可是普通的方法並不能掛起協程,因此doRequest()沒法阻塞住println()。給函數添加suspend修飾符便可:

suspend fun doRequest(): String{
    delay(2000) //模擬請求耗時2秒
    return "請求的結果"
}

複製代碼

協程的併發執行

若是協程內有多個耗時操做,默認狀況下它們是順序執行的。Kotlin提供了一個measureTimeMillis函數用來測量一段代碼的執行時間:

suspend fun doRequest1(): Int{
    delay(2000)
    return 1
}
suspend fun doRequest2(): Int{
    delay(2000)
    return 2
}
val totalTime = measureTimeMillis {
    doRequest1()
    doRequest2()
}
println("totalTime: $totalTime") // totalTime: 4009

複製代碼

爲了提升執行效率,咱們但願兩個耗時操做是併發執行的。使用async就能夠作到:

val totalTime = measureTimeMillis {
    val result1 = async { doRequest1() }
    val result2 = async { doRequest2() }
    println("result: ${result1.await() + result2.await()}") //result: 3
}
println("totalTime: $totalTime") //totalTime: 2032

複製代碼

async開啓一個特殊的協程,可以與其餘協程併發工做。它返回一個Deferred對象,該對象能夠經過await()來等待異步執行的結果;同時Deferred對象也是一個Job對象,能夠cancel()掉。

上面的async代碼塊一旦執行,協程就開始工做了。有時候咱們但願知足某些條件下,協程在開始工做。那麼能夠這樣使用懶惰的async

val totalTime = measureTimeMillis {
    val result1 = async(start = CoroutineStart.LAZY) { doRequest1() } //只是建立協程對象,並未開始工做
    val result2 = async(start = CoroutineStart.LAZY) { doRequest2() } //只是建立協程對象,並未開始工做

    //知足條件了才執行
    result1.start() //協程開始執行
    result2.start() //協程開始執行
    println("result: ${result1.await() + result2.await()}")
}
println("totalTime: $totalTime")

複製代碼

異常處理

協程中的邏輯有可能遇到異常,若是咱們不處理,他們則默認向上傳播給調度線程,從而致使程序崩潰:

fun main() = runBlocking {
    launch {
        throw ArrayIndexOutOfBoundsException()
    }
    launch {
        throw IllegalArgumentException()
    }
    println("start...")
}

複製代碼

上面的程序在遇到第一個協程拋出的ArrayIndexOutOfBoundsException時就會終止執行。咱們除了在每一個協程代碼塊中進行try/catch以外,也能夠設置一個全局的異常處理器。

因爲協程最終由線程調度,全部未處理的異常最終都會拋給線程,所以給線程設置全局的異常處理器便可:

fun main() = runBlocking {
    Thread.setDefaultUncaughtExceptionHandler { t, e ->
        println("catch exception: $e")
    }
    GlobalScope.launch {
        throw ArrayIndexOutOfBoundsException()
    }.join()
    launch {
        throw IllegalArgumentException()
    }
    println("start...")
}
//輸出
catch exception: java.lang.ArrayIndexOutOfBoundsException
start...
catch exception: java.lang.IllegalArgumentException

複製代碼

協程併發安全問題

當咱們使用多線程對同一個共享數據進行修改時,極可能遇到線程安全問題。協程本質上仍然由線程調度執行,因此協程併發執行時,也有和線程相似的安全問題。來看一段代碼:

fun main() = runBlocking {
    var n = 0
    val list = mutableListOf<Job>()
    repeat(100) {
        list.add(GlobalScope.launch {
            repeat(100) { n++ }
        })
    }
    list.forEach {
        it.join()
    }
    println("n: $n")
}

複製代碼

這段代碼重複添加100個協程對象,每一個協程執行100次++,共執行10000次++操做。運行結果極可能不是10000,能夠屢次運行看看:

n: 9495

複製代碼

TIP

若是你的機器只有不超過2個CPU,你將老是看到10000。由於此時線程池只有一個線程來調度協程,不會出現併發安全問題。

在線程遇到安全問題時咱們通常有2種處理方案:一種是加鎖,另一種是使用線程安全的數據結構。

加鎖每每會下降效率,所以咱們推薦採用第二種方案。JDK提供了大量線程安全的數據結構,咱們使用AtomicInteger 來改寫代碼:

var n = AtomicInteger()
val list = mutableListOf<Job>()
repeat(100) {
    list.add(GlobalScope.launch {
        repeat(100) { n.incrementAndGet() }
    })
}
list.forEach {
    it.join()
}
println("n: $n")

複製代碼

不管運行多少次,你將老是獲得10000。

Kotlin官方文檔爲協程併發安全提供了多種解決方案,其中使用線程安全的數據結構是效率最高的方案,這些數據結構由JDK常年迭代進行超細粒度的優化,直接使用便可。

在Android開發中,協程通常用來代替線程執行耗時任務,更有用的是它能夠用同步的方式編寫異步代碼,可以將複雜的異步邏輯變的極具可讀性。具體使用時協程配合強大的高階函數,已經成爲事實上的線程調度的最佳方案,RxJava已經沒有存在的必要。

標準庫

標準庫內容

Kotlin的標準庫大體包含這樣幾個部分:

  • 數據類型和集合
  • JS和Native平臺相關SDK
  • JDK擴展方法,JVM平臺已經很是成熟,因此主要是對JDK進行擴展
  • 其餘語言特點

本課程主要面向Android開發者,AndroidSDK基於JDK,因此主要學習下JDK擴展方法中比較重要的部分。

IO擴展

標準版給File類增長了不少實用的擴展,IO操做在實際開發中佔至關大的比重。

  • append系列

    val file = File("F:\\kotlin_space\\KotlinCoroutine\\src\\main\\kotlin\\a.txt")
    file.appendText(""" 牀前明月光,疑是地上霜; 舉頭望明月,低頭思故鄉。 """.trimIndent())
    file.appendBytes("哈哈".toByteArray())
    
    複製代碼
  • buffer系列

    //讀取每行內容並打印
    file.bufferedReader().lineSequence().forEach {
        println(it)
    }
    //向文件寫入
    file.bufferedWriter().apply {
        write("呵呵")
        write("嘻嘻")
        flush()
    }
    
    複製代碼
  • copy系列

    //拷貝文件
    file.copyTo(File("F:\\kotlin_space\\KotlinCoroutine\\src\\main\\kotlin\\a-copy.txt"))
    //遞歸拷貝目錄
    File("F:\\kotlin_space\\KotlinCoroutine\\src\\main\\kotlin")
                .copyRecursively(File("F:\\kotlin_space\\KotlinCoroutine\\src\\main\\kotlin-copy"))
    
    複製代碼
  • 刪除系列

    //刪除整個目錄
    File("F:\\kotlin_space\\KotlinCoroutine\\src\\main\\kotlin-copy").deleteRecursively()
    
    複製代碼
  • 讀取系列

    println(file.readBytes()) //讀取字節
    file.readLines().forEach { println(it) } //直接讀取全部行並打印
    println(file.readText())//以文本方式讀取整個內容
    
    複製代碼
  • 寫入系列

    file.writeBytes("牀前明月光".toByteArray()) //寫入字節
    file.writeText("疑是地上霜") //寫入文本
    
    複製代碼
  • 其餘

    println(file.extension) //文件擴展名
    println(file.nameWithoutExtension)//文件名
    file.forEachLine { println(it) } //直接遍歷每一行
    file.forEachBlock { buffer, bytesRead -> println(bytesRead) } //讀取字節塊
    
    複製代碼

    String擴展

    String處理在開發中也是不可或缺。

    val s = "abcde"
    println(s.indices) //獲取字符下標的Range對象
    s.all { it!='e' } //全部字符都知足給定條件纔是true
    s.any { it=='a' } //只要有一個字符知足條件就是true
    println(s.capitalize()) //首字母大寫
    println(s.decapitalize()) //首字母小寫
    println(s.repeat(3)) //重複幾回
    "[a-zA-Z]+".toRegex().find(s) //轉正則
    //還有各類查找,替換,過濾,map,reduce等函數,就不一一展現了...
    
    複製代碼

    Sequence類型

    Sequence翻譯過來叫序列,是一種延遲計算的集合,它有着和List,Set,Map同樣的高階函數。來看看如何使用序列:

    val list = mutableListOf("a", "bc", "bcda", "feec", "aeb")
    list.asSequence().forEach { println(it) }
    
    複製代碼

    List,Set,Map,String都有asSequence()方法直接轉爲一個序列,看起來和集合沒兩樣。咱們用list和序列分別執行相同的邏輯,並計算他們的耗時:

    //序列的版本
    println(measureTimeMillis { list.asSequence().first { it.contains("f")} }) //18
    //list的版本
    println(measureTimeMillis { list.first { it.contains("f")} }) //0
    
    複製代碼

    結果發現list比序列快多了!穩住,別急!

    咱們將數據量增大,並將邏輯複雜化來看看:

    val list2 = mutableListOf<String>().apply {
            repeat(1000000){ //將數據量增長到100萬
                add("abcdefg"[Random.Default.nextInt(7)].toString())
            }
        }
    println(measureTimeMillis { list2.asSequence().map { "${it}-aaa" }.filter { it.startsWith("a") } }) //19
    println(measureTimeMillis { list2.map { "${it}-aaa" }.filter { it.startsWith("a") } }) //136
    
    複製代碼

    能夠看到序列的性能比list提升了86%🚀!

    因此,若是你的場景知足如下兩個條件,應該優先使用序列,不然都使用集合:

    1. 數據量超級大,百萬級別
    2. 對數據集進行頻繁的操做

    然而Android開發屬於客戶端開發,基本不太可能遇到這麼大的數據量。通常是後臺對大數據集進行處理好,返給咱們,客戶端頂通常都會分頁加載,一次加載20條。因此,Sequence在Android開發中基本沒有用武之地😥。

Gradle

Gradle簡介

每種編程語言都有本身的包管理器,好比Python用的是pip,Dart用的是pub,NodeJs用的是npm。包管理器最顯而易見的功能就是管理項目的依賴庫,通俗的講,就是讓你方便的用別人的類庫,你也能夠分享本身的類庫給別人用。

但Gradle的功能其實遠不止包管理器,它還能夠對代碼進行混淆,壓縮,動態構建;嚴格意義上講,它應該屬於項目構建工具。

JavaWeb技術棧的同窗喜歡用Maven,但Gradle在構建速度和擴展性上都比Maven好,能夠說是JVM平臺項目的首選構建工具;作Android開發也是用這個構建工具。

Gradle不須要額外安裝和下載,當你初次建立Kotlin工程時,IDEA會自動下載Gradle。

build.gradle文件

Gradle是經過build.gradle來配置項目的,這個文件在你建立工程時會自動生成,它的內容大體以下,註釋都寫在裏面:

//構建項目首先會執行的部分
buildscript {
    ext.kotlin_version = '1.3.0'
    repositories {
        mavenCentral()
    }
    //添加Kotlin插件到classpath
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
apply plugin: "kotlin" //使用Kotlin插件
apply plugin: "java"   //使用java插件
group 'com.lxj'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8

repositories {
    mavenCentral() //指定下載類庫的倉庫
}
//指定要依賴的三方庫類庫
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"

    testCompile group: 'junit', name: 'junit', version: '4.12'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
}

複製代碼

若是咱們要依賴一個新的三方庫,直接將類庫加到dependencies下面便可。以網頁解析庫jsoup爲例:

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}"

    testCompile group: 'junit', name: 'junit', version: '4.12'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.0'
    compile 'org.jsoup:jsoup:1.11.3' //jsoup
}

複製代碼

而後刷新Gradle便可,以下圖示:

image

Gradle的知識點很是多,要講詳細必須重開一套教程,這篇教程的重點在Kotlin內容的學習,Gradle的知識先簡單瞭解便可。

爬蟲項目實戰

爬蟲介紹

爬蟲就是抓取某個或某些Url地址的數據,可供本身使用;若是數據有價值,也能夠商用。

就像要把大象裝冰箱同樣,爬蟲通常也有三個步驟:

  1. 抓取Url數據
  2. 解析數據
  3. 使用數據,具體怎麼使用看你的需求

要爬取目標網站是:quotes.toscrape.com/,該網站是一個國外的網站,專門展現名人名言。簡單一點,咱們只爬取首頁的數據。

首頁有十條數據,咱們須要爬取每條名言的做者,內容和標籤。

抓取數據

抓取數據須要用到一個三方類庫,就是上個小節提到的jsoup,它具備http請求和網頁數據解析的雙重功能。先將它添加到依賴庫,而後建立crawler.kt文件來編寫爬蟲。

編寫一個getHtml方法來抓取數據,抓取數據是耗時操做,咱們使用協程實現:

suspend fun main() {
    val url = "http://quotes.toscrape.com/"
    //1.抓取數據
    val document = getHtml(url).await()
}

fun getHtml(url: String): Deferred<Document?> {
    return GlobalScope.async {
        Jsoup.connect(url).get()
    }
}

複製代碼

解析數據

解析數據本質是解析html的結構結構,找到對應的標籤,取出文本數據,這裏須要你有一些基本的html知識。爲了更好的分析目標元素的Dom結構,能夠利用Chrome的開發者工具。

編寫一個方法parseHtml來解析數據:

fun parseHtml(document: Document) {
    val elements = document.select("div.quote")
    elements.forEach {
        val content = it.selectFirst("span.text").html()
        val author = it.selectFirst("span>small.author").html()
        val tagEls = it.select("div.tags>a")
        tagEls.forEach { tag -> println(tag.html()) }
    }
}

複製代碼

數據雖然解析出來了,可是這些數據是散亂的,不方便傳輸,處理以及下一步的使用。咱們須要編寫一個類來封裝這些信息:

data class Quote(
        var content: String,
        var author: String,
        var tags: List<String>
){
    fun toJson() = """ { "content": $content, "author": $author, "tags": [${tags.joinToString(separator = ", ")}] } """.trimIndent()
}

複製代碼

改寫parseHtml方法以下:

fun parseHtml(document: Document): List<Quote> {
    val elements = document.select("div.quote")
    val list = mutableListOf<Quote>()
    elements.forEach {
        val content = it.selectFirst("span.text").html()
        val author = it.selectFirst("span>small.author").html()
        val tagEls = it.select("div.tags>a")
        val tags = mutableListOf<String>()
        tagEls.forEach { tag -> tags.add(tag.html()) }
        list.add(Quote(content = content, author = author, tags = tags))
    }
    return list
}

複製代碼

最終的main方法以下:

suspend fun main() {
    val url = "http://quotes.toscrape.com/"
    //1.抓取數據
    val document = getHtml(url).await()
    //2.解析數據
    if (document != null) {
        parseHtml(document)
    }
}

複製代碼

使用數據

在企業級項目中咱們在使用數據以前可能會將數據進行持久化存儲,好比保存到Mysql。具體怎麼使用,每一個公司的需求都不同,能夠用圖表展現,數據量大的話能夠用大數據框架進行處理。咱們這裏只是簡單的打印Json,編寫一個方法printData

fun printData(quotes: List<Quote>){
    quotes.forEach { println(it.toJson()) }
}

複製代碼

最終的main方法以下:

suspend fun main() {
    val url = "http://quotes.toscrape.com/"
    //1.抓取數據
    val document = getHtml(url).await()
    //2.解析數據
    if (document != null) {
        val quotes = parseHtml(document)
        //3.打印數據
        printData(quotes)
    }
}

複製代碼

運行項目,將會打印出Json結構的數據:

{
    "author": Albert Einstein,
    "content": 「The world as we have created it is a process of our thinking. It cannot be changed without changing our thinking.」,
    "tags": [change, deep-thoughts, thinking, world]
}
{
    "author": J.K. Rowling,
    "content": 「It is our choices, Harry, that show what we truly are, far more than our abilities.」,
    "tags": [abilities, choices]
}
...

複製代碼

經過這個小小的爬蟲項目,咱們綜合練習了數據類,Kotlin協程,使用Gradle添加三方庫,集合和高階函數,原生字符串等知識。

咱們目前只爬取了網站首頁的數據,若是你對爬蟲感興趣,思考一下,如何能爬取整個網站的數據呢?

相關文章
相關標籤/搜索