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添加三方库,集合和高阶函数,原生字符串等知识。

咱们目前只爬取了网站首页的数据,若是你对爬虫感兴趣,思考一下,如何能爬取整个网站的数据呢?

相关文章
相关标签/搜索