接口测试之Kotlin篇(上)

达芬奇密码2018-07-18 13:46

一、Kotlin简介

    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的特性。

二、主要特性和语法介绍

Java互操作

    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表达式、内联函数、协程、操作符重载、反射等特性,在这里就不逐一介绍了。

相关阅读:

接口测试之Kotlin篇(下)

本文来自网易实践者社区,经作者hzsunzhengyu授权发布。