用Kotlin一段时间了,主要是用来做本地数据分析,需要大量的字符串和集合处理,用原生Java着实麻烦了点。Python虽然也可以用,奈何公司自研的分布式数据库只支持Java访问,只能找一个JVM语言来用了。
Groovy和Scala都尝试过:
Groovy对Java兼容的实在是太细致入微了(我曾经把Java代码原样贴到Groovy脚本里,直接运行无压力),以至于我根本没有动力按照Groovy的最佳实践来进行开发。而且,不论是自己的实践还是第三方测评都已经证明了Groovy的性能相较于于原生Java差一倍左右。
Scala则是另一个极端,它提供了极其复杂的设计,上手的速度让人难以接受。此外,其编译的速度同样让人难受;再此外,无论是Eclipse还是Idea对Scala的支持都不是很美好(至少在我尝试Scala的时候是这样的)。
Kotlin则不同,JetBrains公司自己设计的语言,Idea对其支持是全面而深入的(根本不会出现代码提示的时候卡顿的情况,Scala呵呵哒)。当Google宣布Kotlin成为Android官方开发语言的时候,另一种叫做巨头站队的确定性加入到这门语言中。Kotlin对Java语法的支持是很节制的,甚至在Idea层面上,从Java拷贝的代码到Kotlin文件中,会做好自动转化而不是原样输出。
出于规避Oracle大流氓的角度考虑,切换其他的JVM语言是需要认真考虑的事情。虽然直接切换到其他平台,例如Go,也是可以考虑的,然而这将导致巨大的重写成本以及基础库的切换成本,我司自研数据库的访问就是一个例子。
以上。
在我使用Kotlin的这段时间里,从需求场景出发,最大的体会是它相当顺手的语法特征以及几乎没有门槛的切换,相对于原生Java来说,对我是相当可口的语法糖。本文不是严谨的一步步的语法教程,也不是详细的对比介绍,仅仅是使用过程中对Kotlin方便好用感触较深的点做相对全面的记录。基
础语法可以参考 kotlincn.net。
第一眼差别
顶层属性 和 顶层函数
其实顶层函数和顶层属性的意义是差不多的,很像脚本的写法,不需要包裹在一个Class或者Object中,而单独在脚本中出现。
静态函数是不包含状态的,也就是所谓的纯函数,它的输入仅仅来自于它的参数列表,那么调用的时候需要通过一个对象或者类中是非常多余的,因此在Kotlin中认为一个函数或方法有时候并不是属于任何一个类,它可以独立存在。
顶层属性和顶层函数的作用于是包,即同一个包中可以不用Import而直接调用。而跨包的调用则需要Import。
原理上,顶层文件会反编译成一个容器类。(类名一般默认就是顶层文件名+”Kt”后缀,注意容器类名可以自定义,通过@JvmName),顶层函数会反编译成一个static静态函数
Main函数可以作为顶层函数
直接作为顶层函数,可以没有类定义而直接运行。偏向于脚本的形式。
val(常量)、var(变量)、lateinit、by lazy{}
Kotlin中val之定义并赋值之后就无法变更的量,相当于Java中的final变量。而var则是可变量。在函数式编程中,是推荐尽量使用减少var,可变量在使用过程中很容易出现线程不安全的情况。具体参见 “理解Scala的函数式风格:从var到val的转变”。Scala和Kotlin在这一点上是一致的。
kotlin中默认是空安全的,任何属性的声明都必须有初始化值,如果支持可空”?”,才能把属性声明为null。那么一个非空的变量如果需要在后面赋值,则需要用lateinit。lateinit var只能用来修饰类属性,不能用来修饰局部变量,并且只能用来修饰对象,不能用来修饰基本类型(因为基本类型的属性在类加载后的准备阶段都会被初始化为默认值)。lateinit var的作用也比较简单,就是让编译期在检查时不要因为属性变量未被初始化而报错。lateinit可以在任何位置初始化并且可以初始化多次。
而 val 的延迟初始化则是要用 by lazy { 代码块 }。by lazy只能作用于val关键字标注的属性。当属性用到的时候才会初始化”lazy{}”里面的内容,而且再次调用属性的时候,只会得到结果,而不会再次执行lazy{}的运行过程。
类型推断
kotlin类型名首字母是大写的 变量名是小写的 以示区分。形式定义为:var 变量名 : 类型名 = 值。对于基本数据类型,可以直接省略类型名,而由系统进行类型推断。
Import
如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义:
import foo.Bar // Bar 可访问
import bar.Bar as bBar // bBar 代表“bar.Bar”
可读的数字
val creditCardNumber = 1234_5678_9012_3456L
常见功能简化:
println() 等同于 System.out.println()。
不需要“new”关键字 val rectangle = Rectangle(5.0, 2.0)
强制类型转换:”123“.toInt() 看上去更加顺畅
‘==‘和‘===‘的区别,双等号比较kotlin团队的处理就是通过Intrinsics.areEqual(b, c)来比较两个对象的值是否相等,三个等号就是通过java中的‘==‘比较两个对象的地址是否相等。
类型判断:obj is String
属性默认为final,除非申明为 open(跟val一样的原因,不推荐使用可变量)
没有Object类型,只有Any
Kotlin中所有的类都有一个共同的基类Any,如果类没有申明继承其他类的话,默认继承的就是Any。它只有三个方法,equals,hashCode和toString。而去掉了Object关于同步相关的函数(wait, notify等)。
?和!!
"?"加在变量名后,系统在任何情况不会报它的空指针异常,用于显式标记变量可以为空。"!!"加在变量名后,如果对象为null,那么系统一定会报异常,用于显式标记变量绝对不能为空。这两个符号能让开发者明确对变量空值的感知。任何一个变量,如果不加上?,相当于默认加上了!!。
roomList?.size 返回的是 null 或者是 roomList的大小。
字符串相关
多行字符串:
你可以通过 trimMargin() 函数去除前导空格:
val text = """
|Tell me and I forget.
|Teach me and I remember.
|Involve me and I learn.
|(Benjamin Franklin)
""".trimMargin()
默认 | 用作边界前缀,但你可以选择其他字符并作为参数传入,比如 trimMargin(">")。
字符串模板
val s = "abc"
println("$s.length is ${s.length}")
循环和判断
If not null 缩写
val files = File("Test").listFiles() println(files?.size) | val b: Boolean? = …… if (b == true) { …… } else { // `b` 是 false 或者 null } |
If not null and else 缩写
val files = File("Test").listFiles()
println(files?.size ?: "empty")
if null 执行一个语句
val values = ……
val email = values["email"] ?: throw IllegalStateException("Email is missing!”)
val email = values[“key”]!!
在可能会空的集合中取第一元素
val emails = …… // 可能会是空集合
val mainEmail = emails.firstOrNull() ?: ""
映射可空值(如果非空的话)
val value = ……
val mapped = value?.let { transformValue(it) } ?: defaultValueIfValueIsNull
When(升级版swit)
// 直接返回表达式结果
gameCount[thisUserGameCount] = when (gameCount.containsKey(thisUserGameCount)) {
true -> gameCount[thisUserGameCount]!! + 1
false -> 1
}
// 我们可以用任意表达式(而不只是常量)作为分支条件
when (obj) {
1 -> "One"
"Hello" -> "Greeting"
is Long -> "Long"
!is String -> "Not a string"
else -> throw IllegalArgumentException("Invalid color param value")
}
For 和 区间
for (i in 1..100) { …… } // 闭区间:包含 100
for (i in 1 until 100) { …… } // 半开区间:不包含 100
for (x in 2..10 step 2) { …… }
for (x in 10 downTo 1) { …… }
if (x in 1..10) { …… }
for (index in items.indices) {
println("item at $index is ${items[index]}")
}
for ((k, v) in map) {
println("$k -> $v")
}
x !in a..b
直接返回代码块结果
返回 when 表达式 fun transform(color: String): Int { return when (color) { "Red" -> 0 "Green" -> 1 "Blue" -> 2 else -> throw IllegalArgumentException("Invalid") } } | 返回 “try/catch”表达式 fun test() { val result = try { count() } catch (e: ArithmeticException) { throw IllegalStateException(e) } // 使用 result } | “if”表达式 val result = if (param == 1) { "one" } else if (param == 2) { "two" } |
Collection相关
创建
arrayOf(1, 2, 3) 创建了 array [1, 2, 3]。
库函数 arrayOfNulls() 可以用于创建一个指定大小的、所有元素都为空的数组。
接受数组大小以及一个函数参数的 Array 构造函数,用作参数的函数能够返回给定索引的每个元素初始值:
val asc = Array(5, { i -> (i * i).toString() }) // 创建一个 Array<String> 初始化为 ["0", "1", "4", "9", "16"]
可变和不可变集合
listOf 创建不可变LIst,跟Java不同,除了不能改变List的引用之外,也不能变更List的内容;
mutableListOf,可以变更内容的List。
setOf, mutableSetOf,mapOf, mutableMapOf:与上述内容类型
不型变(invariant)
这意味着 Kotlin 不让我们把 Array<String>赋值给 Array<Any>,以防止可能的运行时失败。(但是你可以使用 Array<out Any>, 参见类型投影)
常用函数(实用)
list.joinToString(
separator = " | ",
prefix = "(",
postfix = ")"
)
val sortedMapByValue = map.toSortedMap()
val sortedMap = map.toList().sortedBy { (key, value) -> key }.toMap()val sortedMapByValue = map.toSortedMap()
val sortedMap = map.toList().sortedBy { (key, value) -> key }.toMap()
取值
方括号取值,如 arr[0]。
流式处理(用法跟Java差别不大)
filter, map, reduce, all, any, count, find
函数相关
函数定义
单行定义:fun sum(a: Int, b: Int) = a + b
指定是否可能返回空:fun parseInt(str: String): Int? {……}
默认参数
可以省去不需要的参数传递,无需像Java那样为了传递默认参数而定义一堆多态的函数。
fun someFunction( param1: String, param1: Boolean = true, param2: Boolean = true, param3: String = “3" ) { // function body }
| 函数有默认值的时候,参数有默认值的话,我们可以不传这个参数
someFunction("aa”,false) //这样相当于someFunction("aa”,false, true,”3") | 如果要在多个默认参数之中,设置个别参数的值,就可以使用命名参数了。
someFunction("aa”, param2 = true) //这样相当于 someFunction("aa",true, false,”3") |
中缀函数
infix fun String.中缀(param: String): String {
return "This:" + this + “, Param” +param
}
val resultInfix = "北京" 中缀 "上海"
let、with、run、apply、also函数
// if not null 执行代码
val value = ……
value?.let {
…… // 代码会执行到此处, 假如data不为null
}
方法可以嵌套
即函数内部还可以定义函数。而且局部函数可以访问外部函数(即闭包)的局部变量。
标签处返回
由于推荐使用Lambda表达式,而默认的Lambda表达式中的return是针对函数的(因为Lambda表达式的作用跟 for()循环 类似)。因此标签处返回最重要的一个用途就是从 lambda 表达式中返回。
例如,直接返回的例子:
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return // 非局部直接返回到 foo() 的调用者
print(it)
}
println("this point is unreachable")
}
这个 return 表达式从最直接包围它的函数即 foo 中返回。
如果我们需要从 lambda 表达式中返回,我们必须给它加标签并用以限制 return。
fun foo() {
listOf(1, 2, 3, 4, 5).forEach lit@{
if (it == 3) return@lit // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
print(it)
}
print(" done with explicit label")
}
现在,它只会从 lambda 表达式中返回。
通常情况下使用隐式标签更方便。 该标签与接受该 lambda 的函数同名。
fun foo() {
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@forEach // 局部返回到该 lambda 表达式的调用者,即 forEach 循环
print(it)
}
print(" done with implicit label")
}
前文三个示例中使用的局部返回类似于在常规循环中使用 continue。并没有 break 的直接等价形式,不过可以通过增加另一层嵌套 lambda 表达式并从其中非局部返回来模拟:
fun foo() {
run loop@{
listOf(1, 2, 3, 4, 5).forEach {
if (it == 3) return@loop // 从传入 run 的 lambda 表达式非局部返回
print(it)
}
}
print(" done with nested loop")
}
函数作为第一公民
fun main(args: Array<String>) {
// 函数作为参数
fun testFunParam(param1:Int, funcParam: (i:Int, j:String) -> Int) {
val default = "200"
println( param1 + funcParam(param1, default) )
}
// 定义Lambda表达式作为变量
val lambda1: (i:Int, j:String) -> Int = { first, second ->
first + second.toInt()
}
testFunParam(1, lambda1)
// 等价于
testFunParam(1) { first, second ->
first * second.toInt()
}
}
函数作为第一公民的好处,需要对比Java来看。以下列出Java使用Lambda表达式的方式。
Java中实际上是通过定义 FunctionalInterface 来实现Lambda表达式的
Java里面实际上是借用的对象实例化。 // 1.1 使用匿名内部类根据 name 排序 players Arrays.sort(players, new Comparator<String>() { @Override public int compare(String s1, String s2) { return (s1.compareTo(s2)); } }); |
// 1.2 使用 lambda expression型变量 Comparator<String> sortByName = (String s1, String s2) -> (s1.compareTo(s2)); Arrays.sort(players, sortByName); |
// 1.3 直接使用Lambda表达式 Arrays.sort(players, (String s1, String s2) -> (s1.compareTo(s2))); |
假设有100个参数的Lambda,没有现成的对象可用,那么就只能先新建类,再新建这个实体作为参数传入。
@FunctionalInterface
public interface LambdaWithManyParams {
default void doDefaultWork() {
// Method body
}
static void doStaticWork(){
System.out.println("Hello");
}
// 在这个接口里面只能有一个抽象方法。
void run(int a, int b, int c, int d);
}
相对于这一点,Kotlin则完全无需任何预定义就可以进行Lambda表达式的声明。
类和对象
数据类
我们经常创建一些只保存数据的类。 在这些类中,一些标准函数往往是从数据机械推导而来的。在 Kotlin 中,这叫做 数据类 并标记为 data:
data class User(val name: String, val age: Int)
编译器自动从主构造函数中声明的所有属性导出以下成员:
equals()/hashCode() 对;
toString() 格式是 "User(name=John, age=42)";
componentN() 函数 按声明顺序对应于所有属性;
copy() 函数(见下文)。
为了确保生成的代码的一致性以及有意义的行为,数据类必须满足以下要求:
主构造函数需要至少有一个参数;
主构造函数的所有参数需要标记为 val 或 var;
数据类不能是抽象、开放、密封或者内部的;
此外,成员生成遵循关于成员继承的这些规则:
如果在数据类体中有显式实现 equals()、 hashCode() 或者 toString(),或者这些函数在父类中有final 实现,那么不会生成这些函数,而会使用现有函数;
复制
在很多情况下,我们需要复制一个对象改变它的一些属性,但其余部分保持不变。 在数据库更新操作中特别多。copy() 函数就是为此而生成。对于上文的 User 类,其实现会类似下面这样:
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)
这让我们可以写:
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
属性扩展和函数扩展
属性扩展
var StringBuilder.lastChar: Char get() = get(length -1)
set(value: Char) { this.setCharAt(length -1, value)
}
fun main() {
val sb = StringBuilder("kotlin") println(sb.lastChar) sb.lastChar = '!' println(sb.lastChar)
}
函数扩展:
fun String.selfDefinedFun() { …… }
“String”.selfDefinedFun()
放手去用扩展函数。每当你有一个主要用于某个对象的函数时,可以考虑使其成为一个以该对象为接收者的扩展函数。
但是为了尽量减少 API 污染,尽可能地限制扩展函数的可见性。
根据需要,使用局部扩展函数、成员扩展函数或者具有私有可视性的顶层扩展函数。
object
将类的声明和定义该类的单例对象结合在一起(即通过object就实现了单例模式)。
object declaration的类最终被编译成:一个类拥有一个静态成员来持有对自己的引用,并且这个静态成员的名称为INSTANCE,当然这个INSTANCE是单例的,故这里可以这么去使用。
和普通类的声明一样,可以包含属性、方法、初始化代码块以及可以继承其他类或者实现某个接口,但是object它不能包含构造器。
它也可以定义在一个类的内部。
伴生对象:
在Kotlin中是没有static关键字的,也就是意味着没有了静态方法和静态成员,在 Kotlin 中类没有静态方法。在大多数情况下,它建议简单地使用包级函数。如果确实需要,可以用伴生对象。
class A {
companion object coName(可以省略){
//methods and fields
}
}
实际上底层实现是把它当做静态内部类来看待的,并且目标类会持有该内部类的一个引用,那么最终调用的方法实际上是定义在这个静态内部类中的实例方法。
那么在伴生对象中定义的方法和包级别函数(顶层函数)有什么区别呢?而顶层函数则是作为一个包装类的静态方法出现的。如Test.kt里面有一个main方法,则会生成一个TestKt.class,而main方法则是这个类的静态方法。
类中的静态方法和内部类中的实例方法的区别,因为成员内部类中的方法是可以访问外部中定义的方法和成员的,哪怕是private的,而静态方法是做不到这一点的。
继承
要声明一个显式的超类型,我们把类型放到类头的冒号之后:
open class Base(p: Int)
class Derived(p: Int) : Base(p)
类上的 open 标注与 Java 中 final 相反,它允许其他类 从这个类继承。默认情况下,在 Kotlin 中所有的类都是 final。
不写open关键字的:类、方法、属性是不能被继承和重写的。
要在父类被重写的方法或属性前加open关键字,并且子类中重写的方法或属性前都要加override关键字
可以用一个 var 属性覆盖一个 val 属性,但反之则不行。这是允许的,因为一个 val 属性本质上声明了一个 getter 方法,而将其覆盖为 var 只是在子类中额外声明一个 setter 方法
覆盖方法总是使用与基类型方法相同的默认参数值。 当覆盖一个带有默认参数值的方法时,必须把子类重写的中省略默认参数值
接口
既包含抽象方法的声明,也包含实现。这个跟Java8中提供的特性一样,但是Kotlin可是兼容到Java6的。相当于在老版本实现了高版本的功能。
在接口中声明的属性要么是抽象的,要么提供 访问器的实现。
this
为了表示当前的 接收者 我们使用 this 表达式:在类的成员中,this 指的是该类的当前对象。
在扩展函数或者带有接收者的函数字面值中, this 表示在点左侧传递的 接收者 参数。
如果 this 没有限定符,它指的是最内层的包含它的作用域。
要引用其他作用域中的 this,请使用 标签限定符:限定的 this
要访问来自外部作用域的this(一个类 或者扩展函数, 或者带标签的带有接收者的函数字面值)我们使用this@label,其中 @label 是一个代指 this 来源的标签:
class A { // 隐式标签 @A
inner class B { // 隐式标签 @B
fun Int.foo() { // 隐式标签 @foo
val a = this@A // A 的 this
val b = this@B // B 的 this
val c = this // foo() 的接收者,一个 Int
val c1 = this@foo // foo() 的接收者,一个 Int
val funLit = lambda@ fun String.() {
val d = this // funLit 的接收者
}
val funLit2 = { s: String ->
// foo() 的接收者,因为它包含的 lambda 表达式
// 没有任何接收者
val d1 = this
}
}
}
}
参考
Kotlin 和 Scala的对比:https://superkotlin.com/kotlin-vs-scala/
Kotlin中文网: https://www.kotlincn.net/
Kotlin系列之顶层函数、中缀调用、解构声明 https://blog.youkuaiyun.com/u013064109/article/details/79887528
理解Scala的函数式风格:从var到val的转变 http://developer.51cto.com/art/200907/134956.htm
kotlin中val和var不为人知的故事 https://www.jianshu.com/p/4c7ca01fe99f
Kotlin系列之let、with、run、apply、also函数的使用 https://blog.youkuaiyun.com/u013064109/article/details/78786646
Kotlin学习系列之:object关键字的使用场景 https://blog.youkuaiyun.com/xlh1191860939/article/details/79460601