Kotlin分享小记

经过几周的Kotlin实践,我整理出了一些Kotlin的语法和特性在团队内部来分享,晚上把PPT的内容整理了一下,放到这里。

我们大家都知道Java在诞生之初,提出了著名的一句话:

Write once, run anywhere。

为了实现这句话,Java的代码会被编译成class文件,然后在JVM上去运行,这也为后来为很多在JVM上设计的语言埋下的伏笔,Kotlin也是众多JVM语言中的其中一个。

Kotlin是著名的IDE开发商JetBrains在2011年的时候推出的,当时很多人笑称这门语言是Scala--, 意思是比Scala还差点,但是Kotlin的野心并不小,据说取名为Kotlin,也是特意找了一个Java的J之后的一个字母K,来起的名字Kotlin,而且连今年刚刚发布的Spring5也宣布全面支持Kotlin

让我们看看Kotlin到底是一门怎么样的语言。我们先来观察一个普通的Java Bean平时是怎么写的:

public class Person {
    private String name ;
    private Integer age;

    // setter and getter
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public Integer getAge() {
        return age;
    }
    public void setAge(Integer age) {
        this.age = age;
    }
}

换成是Kotlin之后:

class Person(
        var name: String = “张三”,
        var age: Int = 25
)

我们可以观察到几个不同:

  1. 首先变量类型被放到的后面,我觉得这个设计主要是因为Kotlin是有类型推到的,希望部分代码在声明的时候就不要写变量类型了。
  2. 通过关键字var(val)声明变量是否可以修改。其中var表示变量可修改,val表示这个变量不可以修改。
  3. 默认值的赋值,在这里实际上实现了多个构造方法。

在Kotlin中使用这个类的时候,我们能看到在构造函数中也是允许通过属性名为其进行赋值的。

val person = Person(“张三", 25)
val person = Person(
    name = "张三",
    age = 25
)
val person = Person(
    age = 25
)
val person = Person(
    name = "张三"
)

然后就是字符串模板,在Kotlin中字符串的变量允许在字符串中被使用,不需要像Java中那样,写很多+号或者用String.format方法,只需要在字符串中使用$,Kotlin会把对应的变量值放到字符串里面。

同时,Kotlin支持这种多行字符串,通过三个引号的方式可以声明多行字符串,这种用法和Python的多行字符串用法是一样的。

println("My name is ${person.name}, I'm ${person.age} years old. ")

println("""
        My name is ${person.name},
        I'm ${person.age} years old.
        I'm happy to write kotlin
""")

而且在Kotlin中,大部分语句都是表达式。比如if语句,when语句,try语句。所有语句的最后一行会被返回。

fun action(person: Person) : Person {
    return if ( person.age > 20 ) a else b
}

特别是when语句,在Java中switch语句限制可能会很多,但在Kotlin中,when语句几乎可以做任何分支判断,然后返回给语句。

fun getPoint(grade: Char) = when (grade) {
    'A' -> "GOOD"
    'B', 'C' -> {
       // do something
        "OK"
    }
    'D' -> "BAD"
    else -> "UN_KNOW"
}

fun getPoint2(grade: Int) = when {
    grade > 90 -> "GOOD"
    grade > 60 -> "OK"
    grade.hashCode() == 0x100 -> "STH"
    else -> "UN_KNOW"
}

fun getPoint2(grade: ParentClass) = when {
     is SubClassA -> "GOOD"
     is SubClassB -> "OK"
     is SubClassC -> "STH"
    else -> "UN_KNOW"
}

这样我们在使用try之类的语句的时候,不用在代码块声明一个变量然后在try和catch语句里分别为变量赋值一遍了。

// Java 
Object obj;
try {
    obj = //
}catch(Exception ex) {
    obj = //
}

// kotlin
Object obj = try {
     // statement
}catch(Exception ex) {
    // statement
}

在平时写业务代码的时候一个函数中,我们会有很多模板代码,有时候会懒得去再去写一个private的函数,也不知道写在哪里比较合适。在kotlin中,允许我们在模板内部声明一个局部函数 这个小功能,其实在平时写代码的时候会让我们方便很多。

fun saveUser2(user: User) {
    fun validate(value: String, fildName: String) {
        if (value.isEmpty()) {
            throw IllegalArgumentException("Can't save user ${user.id}: empty $fildName")
        }
    }
    validate(user.name, "Name")
    validate(user.address, "Address")
    validate(user.email, "Email")
    // save to db ...
}

我们都知道在Java中NullPointerException被认为是一个非常昂贵的设计,我们的程序大部分异常都是在避免这个NullPointerException的发生,所以我们经常需要这样的代码:

if(objects.nonNull(obj)) {
    // do something with obj
}

Kotlin设计一些语法糖来帮助我们避免NullPointerException。

// 这种一般的声明,本身在编译期间就不允许变量被声明为null。
val a: String = "aa"
// a是非空类型,下面的给a赋值为null将会编译不通过
// a = null ✘
a.length

// ?声明是可空类型,可以赋值为null
var b: String? = "bb"
b = null

// b是可空类型,直接访问可空类型将编译不通过,需要通过?.或者!!.来访问
// b.length
b?.length
b!!.length

// 通过 ?: 符号可以给空的值付一个默认值。有点像Java中的三元表达式。
val b = input?.length ?: -1;

input?.let {
    // do something with input
    return result
}

上面中使用到的let是Kotlin中每个对象都内置的作用域函数, Kotlin中内置几个高阶函数都非常棒。举个例子,原来在Java中我们操作集合将结果进行操作可能是这样的:

List<String> list = listData
                .stream()
                .filter( data -> Objects.equals(data.name ,"James") )
                .collect(toList());
// invoke function with list object 
function(list);

在Kotlin中借助这些高阶函数,可以容易地在一行代码就实现了:

listData
        .asSquence()
        .filter { it.name == "James" }
        .run { function(this) }
listData
    .asSquence()
    .filter { it.name == "James" }
    ?.let { function(it) }

具体的几个高阶函数如下:

函数名 block参数 函数返回值 其他
with 无,可以使用this 闭包返回 普通函数
run 闭包返回 普通函数
let it 闭包返回
apply 无,可以使用this this
run 无,可以使用this 闭包返回
also it 闭包返回
takeIf it this 或 null 闭包返回类型必须是Boolean
takeUnless it this 或 null 闭包返回类型必须是Boolean

至于如何选择这些函数,这篇文章我觉得总结地非常好:http://liangfei.me/2018/03/31/kotlin-mastering-standard-functions/。自从有了这些扩展,大部分场景中的代码都在一个链式中就可以被处理了。

在扩展方法这块,之前已经专门写了一篇文章来讲Kotlin中强大的扩展方法,这里就不再重复了。

接来下我们看看Kotlin对DSL,也就是领域特定语言的支持,先看一组代码,对象之间相互嵌套,我们在组装代码的时候会非常繁琐。

data class Person(val name: String, val age: Int)
data class House(val people: List<Person>)
data class Village(val houses: List<House>)

// 组装参数
val houses = mutableListOf<House>()
val people1 = mutableListOf<Person>()
people1.add(Person("Emily", 31))
people1.add(Person("Hannah", 27))
people1.add(Person("Alex", 21))
people1.add(Person("Daniel", 17))

val house1 = House(people1)
houses.add(house1)

val village = Village(houses)

通过Kotlin的写法可以一些简化,看起来稍微清晰一些了,但是仍然会有些疑惑,因为属性名被省去了,阅读起来还是感到有些困难。

val house1 = House(listOf(
        Person("Emily", 31),
        Person("Hannah", 27),
        Person("Alex", 21),
        Person("Daniel", 17)))

val village = Village(listOf(house1))

但是当我们把属性名加上去,又显得代码有点啰嗦了。

val village = Village(listOf(
            House(listOf(
                    Person(
                            name = "Emily",
                            age = 31
                    ),
                    Person(
                            name = "Hannah",
                            age = 27
                    ),
                    Person(
                            name = "Alex",
                            age = 21
                    ),
                    Person(
                            name = "Daniel",
                            age = 17
                    )
            ))
))

看看Kotlin可以如何来解决这类问题,这是一段定义DSL的代码。

@SimpleDsl1
class VillageBuilder {
    private val houses = mutableListOf<House>()
    operator fun House.unaryPlus() {
        houses += this
    }
    fun house(setup: HouseBuilder.() -> Unit = {}) {
        val houseBuilder = HouseBuilder()
        houseBuilder.setup()
        houses += houseBuilder.build()
    }
    fun build(): Village {
        return Village(houses)
    }
}
// Kotlin允许我们定义局部扩展方法,将传入的函数绑定到指定的类型上,就是之前的扩展方法。Kotlin称之为**接收者类型**
// 这里可能不好去理解,就是说这个village方法会接收到一个函数setup,然后这个setup函数在这个village方法会成为villageBuilder这个类的一个方法。
@SimpleDsl1
fun village(setup: VillageBuilder.() -> Unit): Village {
    val villageBuilder = VillageBuilder()
    villageBuilder.setup()
    return villageBuilder.build()
}

经过这样的一番折腾之后,我们的刚刚的类型组装就可以变成这样。

village {
    house {
        person {
            name = "Emily"
            age = 31
        }
        person(name = "Hannah") {
            age = 27
        }
        person("Alex", 21)
        person(age = 17, name = "Daniel")
    }
}

比原来简洁了很多,这种DSL风格特别合适在一些模板定义的场景中去使用,比如短信模板和附页模板这类场景。代码在阅读的时候会特别的清晰。

再看kotlin里一个官方的测试库代码,DSL也是测试中的一个很常见的使用工具,用DSL写出来的测试代码一般会和自然语言非常接近。阅读起来非常清晰。

"length should return size of string" {
    "hello".length shouldBe 5
}

"startsWith should test for a prefix" {
    "world" should startWith("wor")
}

最后再介绍一下Kotlin中的一个还在试验阶段的功能——协程。在说这个功能之前,我们可以回顾一下两个多线程中的概念:

  • 并行,几个线程同一时间时在不同CPU一起运行
  • 并发,几个线程在同一段时间在一个CPU上,交替运行。

在多线程中,无论是并行还是并发多是指的CPU资源的并行和并发,当然CPU资源是有限的。所以多线程技术还是有一定的弊端,即使是在多线程中依然会有线程被挂起的问题造成线程被浪费。当我们把这里的资源换成是线程本身会如何呢?我们在线程上构建很多的协程,实现并发,这样会可以进一步地提高使用CPU的使用效率。

代码本身我们就不多说了,跟多线程的用法很像,相信大家也是使用多线程上的好手了:

suspend fun doSomethingUsefulOne(): Int {
    delay(1000L) // pretend we are doing something useful here
    println("bingo doSomethingUsefulOne")
    return 13
}

suspend fun doSomethingUsefulTwo(): Int {
    delay(3000L) // pretend we are doing something useful here, too
    println("bingo doSomethingUsefulTwo")
    return 29
}

fun main(args: Array<String>) = runBlocking {
    val startTime = System.currentTimeMillis()
    val job = launch {
        val two = async { doSomethingUsefulTwo() }
        val one = async { doSomethingUsefulOne() }
        println("The answer is ${one.await() + two.await()}")
    }
    job.join()
    println("Completed in ${System.currentTimeMillis() - startTime} ms")
}

我们可以尝试一下启动10w个协程来运行一段代码执行的效率还是非常快的,但我们要执行10w个线程,恐怕系统是要直接挂掉。

fun main(args: Array<String>) {
    repeat(100_000) { // 启动十万个协程试试 
        launch { 
            suspendPrint() 
        } 
    } 
    Thread.sleep(1200) // 等待协程代码的结束 
} 
suspend fun suspendPrint() { 
    delay(1000) 
    println(".") 
}

而且协程带来的另外一个好处是既然代码是在一个线程上工作的,那么资源就不存在被竞争的问题了,这种情况下代码就不需要锁机制来保证并发情况下数据的安全性了。而且没有线程上下文的切换,CPU的工作效率也高了很多。在Java中可能是因为多线程的能力太强了,协程这种技术被忽略,而在一些动态语言里,比如Python里,协程是一种很常见的技术,我们如果能构建一个全局多线程,局部协程的程序,代码的运行效率能提高非常多。协程在kotlin中还是处于一个实验阶段,但预计在未来几个版本中很多会正式开放。

我觉得Kotlin之所以能够在这段时间热度一直都很好,在于他对老项目真的是太友好了,我们再看下kotlin如何在一个Java的maven项目中被使用,在pom.xml中加入几个跟kotlin相关的dependency和几个plugin就好了:

<dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
            <version>${kotlin.version}</version>
</dependency>
<dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-maven-noarg</artifactId>
            <version>${kotlin.version}</version>
</dependency>
<dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib-jdk8</artifactId>
            <version>${kotlin.version}</version>
</dependency>
            <plugin>
                <artifactId>kotlin-maven-plugin</artifactId>
                <groupId>org.jetbrains.kotlin</groupId>
                <version>${kotlin.version}</version>
                <executions>
                    <execution>
                        <id>compile</id>
                        <phase>compile</phase>
                        <goals>
                            <goal>compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <source>${project.basedir}/src/main/java</source>
                                <source>${project.basedir}/src/main/kotlin</source>
                            </sourceDirs>
                        </configuration>
                    </execution>
                    <execution>
                        <id>test-compile</id>
                        <phase>test-compile</phase>
                        <goals>
                            <goal>test-compile</goal>
                        </goals>
                        <configuration>
                            <sourceDirs>
                                <source>${project.basedir}/src/test/java</source>
                                <source>${project.basedir}/src/test/kotlin</source>
                            </sourceDirs>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                    </compilerPlugins>
                    <jvmTarget>${java.version}</jvmTarget>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>

为了代码的清晰,我们可以在src目录下新建一个kotlin文件夹,然后package之类的用法和Java完全是一样的。

 .
├── main
│   ├── java
│   ├── kotlin
│   └── resources
└── test
    ├── java
    ├── kotlin
    └── resources

由于Java的Write once, run anywhere的特性,其实Kotlin写的所有代码,都是可以在Java中被使用的,由于Android之前一直都是使用JDK1.6,所以kotlin目前支持最低的JDK版本是1.6,也就是说即使是在一个1.6环境里写的代码,我们还是能用kotlin来cover住,对老项目非常的友好。

当然Kotlin还有很多非常有意思的特性和技巧,我也只是使用了不是非常长的时间,前两天在Raddit上有一个话题说,Kotlin是不是比Java10更好,地下有一个回复被赞了很多次:

Kotlin will be better than Java 20

当然这是一句玩笑,但他的编译器确实是帮我们做了很多工作减少重复代码,而且与之前众多的JVM语言有些不同,Kotlin给自己的定位,感觉更加像是Java上的一个补充,大部分类库都没有自己去重复再造轮子,对Java原有的生态非常的友好,很大程度上降低了老项目引入Kotlin的成本,又添加了很多原来在动态语言里才能用上的特性,个人感觉这门语言在未来很长一段时间在JVM上都会非常有竞争力。

参考的文章和书籍:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 230,527评论 6 544
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,687评论 3 429
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 178,640评论 0 383
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,957评论 1 318
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,682评论 6 413
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 56,011评论 1 329
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 44,009评论 3 449
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 43,183评论 0 290
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,714评论 1 336
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,435评论 3 359
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,665评论 1 374
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 39,148评论 5 365
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,838评论 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 35,251评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,588评论 1 295
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,379评论 3 400
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,627评论 2 380

推荐阅读更多精彩内容