Kotlin 是一种基于 JVM 的编程语言,由 JetBrains 开发。虽然在2010年就推出了,但相对于jvm语言三巨头(Scala、Clojure、Groovy),Kotlin一直处于不温不火的状态。今年5月,Google宣布Kotlin成为Android开发一级语言,当天其搜索热度超过了Java,qq群里出现了咨询Kotlin培训的同学,知乎上甚至开始讨论Kotlin是否可以替代Java。其他的利好消息也是不断:Spring 5.0将专门为 Kotlin 引进支持、ACM-ICPC World Finals 2018将新增Kotlin作为编程语言等等。
IDE方面,同为JetBrains的产品,IntelliJ IDEA和Android Studio对Kotlin提供了非常强大的原生支持(右键点击一个java的package,New→Kotlin File/Class即可)。对于Eclipse的用户,官方也提供了相应的插件。此外,Kotlin还提供了了一个交互式命令行工具(但是非常不好用)。你也可以使用在线编辑器来体验Kotlin.
为什么要使用Kotlin呢?想当初在学校的时候,学完C之后学习Java,当时的体验可以说是十分不错的。只是后来又写了很长时间的C#和Python,但现在工作又要写Java,这时就感觉出Java笨重的一面了。而Kotlin,就能让你在不抛离Java的前提下,体验到部分C#和Python的特性。
Kotlin 在设计时就考虑了与 Java的 互操作性, 与所有基于 Java 的框架完全兼容(例如Maven、Spring、TestNG等)。可以从 Kotlin 中自然地调用现存的 Java 代码,也可以在Java 代码中调用 Kotlin 代码。例如在Kotlin中直接导入Java的类:
import java.util.HashMap
import java.util.ArrayList as Alist //导入时可以指定一个别名
当然,代码还是不能混合在同一个文件里的,Kotlin文件的后缀是.kt。此外,官方还提供了一个强大的转换功能,可以把Java代码转成Kotlin。例如在Intellij IDEA中,你只需要在.java文件中ctrl+c,到.kt文件中ctrl+v,IDE就会自动把代码转成Kotlin。
类似的,Kotlin也可以与JavaScript进行相互操作。不过,动态类型的语言差别比较大,支持程度上远不如对Java那么强,JS→Kotlin→Java这种操作是无法实现的。
用var定义变量,用val定义常量,类型可以自动推断,如
var x = "a" //声明一个变量x
var x:String = "a" //显示指定类型,在这里和上面是等效的
val y = 1 //声明一个常量y,相当于java中用final修饰
特别的,var和val可以定义在类外面,并可以用import导入。Kotliin中句末不需要加分号。
和Java类似,数值类型也是分为Byte、Short、Int、Long、Float、Double,但是注意Kotlin中没有隐式的拓宽转换(如int到long)。显示类型转换的写法如下:
var a = 100
var b = a.toLong()
字符用Char表示,如'a',但注意在Kotlin中字符不是数字。
布尔类型用Boolean表示,其值为true和false。在Kotlin中,布尔运算符只有三种:||短路逻辑或、&&短路逻辑与和!逻辑非。
数组使用Array类来表示,操作示例如下:
var arr = arrayOf(1, 2, 3) //创建一个包含3个元素的数组
val arr2 = intArrayOf(1, 2, 3) //无装箱开销的写法
val arr3 = arrayOf(arrayOf(1,2,3), arrayOf(4,5,6)) //二维数组
字符串使用String表示,和Java一样是不可变的。使用方式上,与Python非常相似,例如可以通过索引访问字符串的字符、使用for循环遍历:
var str = "abc"
var first = str[0] //值为字符'a'
for(c in str){
println(c) // 依次输出a/b/c
}
原生字符串,使用三个引号括起来(相当于Python的r"""abc"""当成多行字符串使用),内部没有转义并且可以包含换行和任何其他字符:
val text = """ for (c in "foo") print(c) """
字符串模板,即求值并把结果合并到字符串中(类似Python里的%),示例如下:
val a = 10
val str = "a = $a" //"a = 10"
val str = "a^2 = ${a*a}" //"a^2 = 100" //也可以是表达式
最后,Kotlin中使用===(或!==)判断引用相等,即比较是否指向同一对象;使用==(或!=)判断结构相等,相当于Java中的equals()
在Kotlin中,函数是一等公民,即可以定义在类的外面,关键字为fun。下面是hello world的第一种写法:
fun main(args: Array<String>) {
println("hello world")
}
不需要返回值时,返回类型可以省略,否则需要显示声明,如
/** * doc注释和Java相同 * @param x 参数1 */
fun double(x: Int): Int {
return 2*x
}
需要注意的是,虽然函数和变量都可以写在类外面,但语句(如println("hello world"))是不能像Python那样直接写在最外层的。
与Python类似,Kotlin支持给函数的参数设定默认值,且调用时可以指定赋值给哪个参数:
fun add(x:Int=1, y:Int=2):Int {
return x+y
}
//调用时
add(y=1) //y=1,x使用默认值
add(0) //第1个参数即x=0,y使用默认值
Kotlin取消了三元运算符,相应的,if变成了一个可以返回值的表达式,因此可以写成
val max = if(a > b) a else b
val max2 = if (a > b) {
print("Choose a")
a //最后的表达式作为该块的值
} else {
print("Choose b")
b
}
//也可以像java中一样使用
var max3: Int
if (a > b) {
max3 = a
} else {
max3 = b
}
when则取代了类C语言的switch,语法如下:
when (x) {
1,2 -> print("x == 1 or 2") //多个同样处理的值可以写在一起
add(1,2) -> print("x == 3") //分支条件可以是表达式
else -> { //相当于default
print("otherwise")
}
}
分支条件的类型必须相同(如上面都是Int)。特别的,when表达式还具有返回值,即符合条件的分支的值。
使用for循环通过索引遍历一个数组:
for (i in array.indices) {
print(array[i])
}
while和do while语句则和java中的一致。
在 Kotlin 中,任何表达式都可以用标签(label)来标记。 标签的格式为标识符后跟 @ 符号,在多层循环嵌套时,break和continue可利用标签来控制要操作的循环,例如:
loop@ for (i in 1..100) {
for (j in 1..100) {
if (i == 50 && j == 50) break@loop //直接break外层的循环
}
}
此外,也可用标签控制return语句返回的位置,这个特性在lambda表达式中比较实用。
在上面的代码中,for的循环条件是1..100,表示[1,10],这在Kotlin中称为区间。也可以使用downTo函数来倒序输出,或用step控制迭代间隔:
for (i in 10 downTo 1 step 3) {
println(i) // 10、7、4、1
}
如果要不包括结束元素,则可以使用until函数,即
for (i in 1 until 10){
println(i) // 1到9
上面的downTo和until都是函数而不是关键字,这种a function b的调用方式称为中缀表示法,具体说明参见下面的"类"章节。
判断是否处于区间内:
if (1 in 0..10){}
if (-1 !in 0..10){}
类似Python的tuple和list,Kotlin中也区分可变集合和不可变集合,用法如下:
val nums: List<Int> = listOf(1, 2, 3) //不可变列表 // =左边的类型可以省略,这里为了说明类型写出来了
val numbers: MutableList<Int> = mutableListOf(1, 2, 3) //可变列表
val strings = setOf("a", "b", "c", "c") //不可变set
val strs = mutableSetOf("a", "b", "c", "c") //可变set
val maps = mapOf("a" to "A", "b" to "B") //不可变map
val mappings = mutableMapOf("a" to "A", "b" to "B") //可变map
val snapshot = numbers.toList() //获取一个可变集合在一个特定时间的快照,这里toList()只是复制不是转换
numbers.filter{ it>1 } //从集合中取出符合条件的元素,此处返回[2]
使用关键字 class 声明类,一个类可以有一个主构造函数和一个或多个次构造函数。主构造函数是类头的一部分,跟在类名(和可选的类型参数)后面。如果主构造函数没有注解或可见性修饰符,也不属于主构造函数无参、次级构造函数有参的情况,则constructor关键字可以省略。
class Person constructor(firstName: String) { //主构造函数,此处constructor可省略
init{
// 初始化块,可以当成主构造函数的代码体,主构造函数的参数可以直接在这里使用
println("hello world $firstName")
}
//次级构造函数,需要委托给主构造函数
constructor(firstName: String, lastName:String): this(firstName){
println("secondry")
}
}
//如果要把构造函数的参数直接作为类的属性,可以加上val或var
class Person public constructor(val firstName: String){} //此处的constructor关键字不能省略
Kotlin中没有new关键字,创建实例时像普通函数一样调用构造函数即可(类似Python):
var p1 = Person("sun")
val p2 = Person("sun", "zy")
这里也是hello world的另一种写法。
Kotlin的类可以有属性(这块基本是照抄C#,注意和Java中的成员变量有区别),声明后会同时隐式创建默认的getter和setter,当然也可以显示自定义。属性可以用lateinit修饰,表示延后初始化,例如:
class Testing{
lateinit var location:String //lateinit的属性不能自定义getter和setter
var name:String = "java"
get() = field.toUpperCase() //自定义一个getter,将name转为大写
private set(value) { //自定义一个setter,赋值为空字符串时不写入
if (value != ""){
field = value
}
}
fun test(){
this.name = "kotlin"
this.name = ""
print(this.name) //输出值为"KOTLIN"
}
}
fun main(args: Array<String>) {
Testing().test()
}
上面代码中,getter和setter里面有一个field,表示name这个属性的值,称为幕后字段(backing field)。要使用getter和setter,不需要显式的调用,直接=操作即可。
关于继承,Kotlin中所有的类都有一个默认的超类Any,只有equals()、hashCode()和toString()三个成员。要声明一个显式的超类型,可以把类型放到类头的冒号之后:
open class Base(p: Int) class Exam(p: Int) : Base(p)
上面的代码中, Base类的open 修饰符与 Java 中 final含义相反,表示允许其他类从这个类继承(默认不能)。类似的,要覆盖超类的方法,超类中必须加上open,子类中必须加上override:
open class Base {
open fun test() {}
}
class Exam() : Base() {
override fun test() {} //覆盖的方法默认可以再次覆盖,如果要禁止,则在前面加上final关键字
}
覆盖属性的规则与覆盖方法类似。特别的,var属性可以覆盖val,反之不行。
另外注意的是,Kotlin中没有Java中那样的stastic方法,可以把方法定义在类外部,或用伴生对象来实现类似的效果:
class Test{
companion object { //运行时仍是对象的实例成员
fun hello(){
println("hello")
}
}
}
fun main(args: Array<String>) {
Test.hello()
}
即只保存数据的类(比如用来转化json的类),使用data标记:
data class User(val name: String, val age: Int)
这个类的toString()方法,格式会自动设置为"User(name=John, age=42)"的形式。另外,有时会需要复制一个对象并改变一些属性,同时保持其余部分不变,这时可以使用copy()函数:
fun main(args: Array<String>) {
var user = User("kotlin", 20).copy(age=5) //改变age的值
println(user.toString()) //输出结果为User(name=kotlin, age=5)
}
数据类可以进行解构,即把一个对象拆成很多变量:
val (name, age) = user //默认按照声明时的顺序
println(name)
println(age)
有了数据类之后,就可以方便的定义一个返回多个值的函数(在接口测试中非常有用):
data class Result(val code: String, val data: String)
fun test(): Result{
return Result("200", "{\"status\":\"success\"}")
}
fun main(args: Array<String>) {
var (code, status) = test() //如果解构中不需要使用某个变量,可以使用下划线_占位
println("code=$code, status=$status")
}
如果只需要返回两个或三个值,也可以使用Pair和Tripple:
fun test2():Pair<String, String>{
return Pair("200", "{\"status\":\"success\"}") } fun test3():Triple<String, String, Long>{
return Triple("200", "{\"status\":\"success\"}", 1498560561000L) }
Kotlin中使用is 和 !is 操作符来检查类型,检查后的语句中,编译器会将其智能转换为经过判断的类型:
if (obj is String) {
println(obj) //这里obj已经变成了String类型
}
//甚至还有这种操作
if (x !is String) return print(x.length) //x自动转换为字符串 //以及在&&和||的右侧、when语句的条件判断后,均可以进行智能转换
Kotlin能够扩展一个类的新功能,而无需继承该类或使用像装饰模式这样的的设计模式。例如给MutableList 添加一个swap 函数:
fun MutableList<Int>.swap(index1: Int, index2: Int) {
val tmp = this[index1]
this[index1] = this[index2]
this[index2] = tmp
}
然后就可以对任意该类型的变量调用此函数:
val li = mutableListOf(1, 2, 3)
li.swap(0, 2)
类似的,也可以扩展属性和伴生对象。为了扩大作用域,通常会在顶层(类外面)定义扩展,并可以在其他文件中使用import导入。
Kotlin取消了Java中的checked异常。除此之外,语法与Java的非常相似,不同之处在于Kotlin的try..catch块是由返回值的,因此可以写成:
val x = try {
// 一些代码
}
catch (e: Exception) {
// 处理程序
}
finally {
// 可选的 finally 块
}
Kotlin宣传的最大卖点之一就是空安全,主要是把NPE(NullPointerException)在编译期就进行处理。某个变量可以为null时需要专门声明,即:
var a: String = "abc"
a = null // 编译错误
var b: String? = "abc"
b = null // ok
这样一来,调用变量a的方法时就不需要担心出现NPE了。相应的,如果直接调用声明为可能为null的变量b的属性或方法,则编译器会抛出一个错误,需要先检查是否为null,即:
if(b != null){
println(b.length) //这里和前面提到的类型检查处理方式类似
}
除了先判断是否为null,还可以使用安全调用操作符?.
b?.length //如果b非空,就返回b.length,否则返回null
x?.y?.z?.name //还可以用于链式调用,任意环节为null就返回null
如果只对非null值进行操作,可以配合let使用:
for (item in listWithNulls) {
item?.let { println(it) } // null时不输出
}
如果要分别处理null和非null的情况,则可以使用?:,即Elvis操作符:
val l: Int = if (b != null) b.length else -1 //通常版本
val l = b?.length ?: -1 //Elvis版本
如果 ?: 左侧表达式非空,elvis 操作符就返回其左侧表达式,否则返回右侧表达式。还可以写成:
val parent = node.getParent() ?: return null
val name = node.getName() ?: throw IllegalArgumentException("name expected")
除了以上介绍的这些,Kotlin中还包含委托、接口/抽象类、密封类、泛型、注解、类型别名、Lambda表达式、内联函数、协程、操作符重载、反射等特性,在这里就不逐一介绍了。
相关阅读:
本文来自网易实践者社区,经作者hzsunzhengyu授权发布。