经过几周的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
)
我们可以观察到几个不同:
- 首先变量类型被放到的后面,我觉得这个设计主要是因为Kotlin是有类型推到的,希望部分代码在声明的时候就不要写变量类型了。
- 通过关键字var(val)声明变量是否可以修改。其中var表示变量可修改,val表示这个变量不可以修改。
- 默认值的赋值,在这里实际上实现了多个构造方法。
在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上都会非常有竞争力。
参考的文章和书籍: