快速上手 Kotlin

1,013 阅读10分钟

前言

其实在刚接触 Kotlin 的时候,我内心是拒绝的,由于看习惯了 Java 的代码,以至于看 Kotlin 代码时有一股极大的别扭感,让我不想去学习(其实在我学 C 语言的时候也是这个感觉)。但是没办法呀,谁叫这货是 Android 官方开发语言呢,再加上现在的招聘要求多多少少也要会 Kotlin 了,于是只能硬着头皮上咯。

1. 基本数据类型

相比较 Java ,Kotlin 没有了基本数据类型,可以看作是 Java 的基本类型包装类拿了过来部分改了名字,如下:

Java Kotlin
byte Byte
short Short
int Int
long Long
float Float
double Double
boolean Boolean
char Char

另外字符串是支持模板的,看下面:

fun main() {
    val hello = "Hello"
    println("$hello, World!")
    println("length = ${hello.length}")
}

// 输出:Hello, World!
//      length = 5

2. 数组

Kotlin 中没有了 [] 进行数组声明或创建,8 种基本数据类型有自己的数组类型,如下:

Java Kotlin
byte[] ByteArray
short[] ShortArray
int[] IntArray
long[] LongArray
float[] FloatArray
double[] DoubleArray
boolean[] BooleanArray
char[] CharArray

那引用类型呢?例如我们要使用一个 String 数组,我们需要用 Array<T> 这么一个类,泛型代表数组所属类型。同理,基本数据类型也可以用 Array<T> 来声明,如 Array<Int>Array<Boolean> 等等。

初始化既可以用它们的构造函数,也可以用 Kotlin 提供的全局函数,arrayOf()emptyArray() ,前者接收一个可变参数,后者则是一个空数组。

3. 访问权限

publicprotectedprivateinternal 作为 4 大访问权限,去除了 default

public 作为默认访问权限,因此很多时候我们可以不用写上 public 。

protected 相较于 Java 不变。

private 除了可以修饰类级别,其余不变。为什么可以修饰类呢?这是因为 Kotlin 中一个文件可以声明 1 个以上的类,对这些类加上 private 则表示仅可以在当前文件中使用。

internal 表示模块级别的访问权限,被 internal 标识则表示仅在当前模块可见,这在模块封装上是很有帮助的。

4. 继承性

Kotlin 中,类、字段和函数默认是 final 的,抽象类和接口除外。因此如果我们想要它们变得可继承,需要在前面加上 open 关键字。

对于普通类,如果想要字段或者函数可继承,仅在字段或函数上加 open 是不够的,因为这时候类还是不可继承的,因此类也需要加上 open ,这时字段和函数才真正可继承。

如果不想一个已 open 的字段或函数可继承,可以用 final 关键字进行修饰。

5. 变量与函数

Kotlin 中将类型进行了后置,什么意思呢?举个栗子:

// Java 中我们这样声明变量和函数
public final class Person {
	public final String name;
	public int age;
	
	public final void talk(String str) {
		int temp = 0
	}
}

// Kotlin 中等价于
class Person {
	val name: String? = null
	var age: Int = 0
	
	fun talk(str: String?): Unit {
		var temp = 0
	}
}

首先 Kotlin 中是可以省略分号的,当然前提是你一个语句一行,如果一行有两个语句的话就不能省略了,正常情况下咱们都是一行一语句的,接着来分析上面的代码。

变量

由于类型后置,因此 Kotlin 使用关键字 valvar 来表示变量,val (value) 表示只读变量,在第一次赋值后就不可修改了;var (variable) 就是我们在 Java 中常用的变量了。

在 Kotlin 中,成员变量是没有默认值的,需要我们手动赋值。

另外,Kotlin 是支持类型推断的,在声明变量时,可以不用写类型,前提是有提供初始值。尽管如此,Kotlin 仍然是强类型语言,类型不匹配照样会报错,例如 var age = 0 ,在这个声明中,尽管我们没有显式提供类型,但初始值表明了这个变量就是一个 Int 变量(整型默认为 Int),因此后续我们只能赋 Int 值。

函数

由于类型后置,因此 Kotlin 使用关键字 fun 来表示函数,并且参数列表也遵循类型后置规则,Kotlin 中函数的参数是可以有默认值的,如:

fun setName(name: String = "aaronzzx"): Unit {
	// ...
}

这个 : Unit 是啥?Unit 相当于 Java 的 void: Unit 表示返回 Unit 类型,相当于没有返回值,当然更多时候我们不需要写上这个多余的东西,上面的代码等价于:

fun setName(name: String = "aaronzzx") {
	// ...
}

函数也可以进行类型推断,这里指返回值推断,仅在简写函数时可用,举个栗子:

fun getAge(): Int {
	return age;
}

// 等价于
fun getAge() = age

// 当我们的函数只有 1 个语句时,即使没有返回值,也可以简写。
// 前提这个语句不是赋值语句,不然就 "=" 冲突了
fun function() {
	operate()
}

// 等价于
fun function() = operate()

// 拥有可变参数的函数
fun function(vararg strs: String) {
    for (string in strs) {
        println(string)
    }
}

// 参数为接口或抽象类
fun initView() {
    HttpUtils.request(object: Callback {
        override fun onSuccess() {
            // ...
        }
        
        override fun onFailure() {
            // ...
        }
    })
}

Ps:你可能有发现类型后面跟了个 ? ,这个表示此变量可被赋值 null ,如果不加问号是没办法赋值 null 的,这和 Kotlin 的“空安全”有关,后面会说。

6. 构造函数

Kotlin 的构造函数在第一次看到可能会优点懵逼,先来看下与 Java 的对比:

// Java
public class Person {
	private String name;
	private int age;
	
	public Person() {
	
	}
	
	public Person(String name) {
		this.name = name;
	}
	
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}
}

// Kotlin
class Person(
	private var name: String? = null,
	private var age: Int = 0
)

// 等价于
class Person(
	name: String? = null,
	age: Int = 0
) {
	private var name: String? = name
	private var age: Int = age
	
	init {
		// 可选
	}
}

// 等价于
class Person() {
	private var name: String? = null
	private var age: Int = 0
	
	constructor(name: String?): this() {
		this.name = name
	}
	
	constructor(name: String?, age: Int): this() {
		this.name = name
		this.age = age
	}
}

在这里可以看到,Kotlin 具有碾压性的优势,对于需要重载构造函数,Kotlin 最少可用 4 行代码搞定,接下来分析。

首先 Kotlin 可以省略类体,因为主构造函数被设定在了类名上,主函数可以有参数,也可以没有。如果有参数,可以在参数名前面加关键字 valvar ,表示将这个参数作为成员变量,在关键字前面可以用访问权限或注解进行修饰。

init 表示主构造函数的函数体,如果没有特殊用途可以被省略。

constructor 表示次构造函数,在拥有主构造函数的情况下需要显示调用主构造函数。

7. 空安全

在使用 Java 时,对于 NullPointerException 真的是无可奈何,一不小心就报空指针异常,然后程序就 Crash 了。因此我们只能经常写如下的判空代码:

public String getMobile(Response response) {
    String mobile = null;
    if (response != null) {
        Data data = response.getData();
        if (data != null) {
            mobile = data.getMobile();
        }
    }
    return mobile;
}

或许是知道 Java 程序员处于水深火热之中,Kotlin 添加了空安全这一语言特性,使用 ? 对可空变量进行标识,而没被 ? 标识的变量呢?理所当然的是不会为空了,于是我们对于不为空的变量就不用写繁琐的判空代码了,因为它不会出现空指针异常了。

对于可为空的变量,我们还是需要进行判空的,但是形式不与 Java 一样,于是上面的代码可以这么写:

fun getMobile(response: Response?): String? {
    return response?.data?.mobile
}

?. 表示如果当前对象不为空就调用,与 Java 相比,简洁了好多。

8. 伴生对象

companion object ,这是个啥呢?看下面这段代码,你应该就明白了。

class MainActivity : AppCompatActivity() {
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val url = intent.getStringExtra(EXTRA_URL)
        // ...
    }
    
    companion object {
        private const val EXTRA_URL = "EXTRA_URL"
        
        fun start(context: Context, url: String) {
            val intent = Intent(context, MainActivity::class.java)
            intent.putExtra(EXTRA_URL, url)
            context.startActivity(intent)
        }
    }
}

说白了,这东西就相当于 Java 的 static ,只是 Kotlin 中没有 static 这个概念,所以如果我们要声明静态变量或者静态函数,就需要用到 companion object

另外上面的代码,可能有些地方你看起来不太明白,这里说一下。

  • 继承与实现,通通使用 : 操作符,不同的类或接口用 , 隔开,并且需要显式调用父类构造函数。

  • 首先 Kotlin 中创建对象是省略 new 关键字的,直接用构造函数即可。

  • Java 中的 @Override 注解在 Kotlin 中变成了关键字 override ,重写必不可少。

  • 对于无参的 getter 函数,可以以变量名的形式书写,例如 getIntent() 就是 intent

  • const val 关键字用来声明常量,仅在 objectcompanion object顶层变量 中可用。

  • object 匿名内部类和单例关键字,如果要创建一个单例类,只需这么写。

    object Singleton {
        fun exec() {
            // ...
        }
    }
    
    class Test {
        fun function() {
            // 尽管看起来有点像静态调用,但这就是单例
            Singleton.exec()
        }
    }
    
  • 顶层变量,用代码比较直观,如下:

    const val TAG = "当前文件"
    
    class MainActivity : AppCompatActivity() {
        // ...
    }
    

    顶层变量 属于文件,不属于类或接口等等。

9. 分支、循环语句

9.1 分支语句

在 Kotlin 中分支语句属于表达式,因此我们可以这么写代码:

fun function(type: Int) {
    // if-else
    var str = if (type == 1) {
        "Hello, World!"
    } else if (type == 2) {
        "Goodbye!"
    } else ""
    
    // switch ,在 Kotlin 中关键字变为 when
    str = when (type) {
        1 -> "Hello, World!"
        2 -> "Goodbye!"
        else -> ""
    }
    // 或者 when 不带参数
    str = when {
        type == 1 -> "Hello, World!"
        type == 2 -> "Goodbye!"
        else -> ""
    }
}

当分支语句作为表达式时,除非你的条件已经包括所有情况了,否则一定要有 else

9.2 循环语句

whiledo-while 相较于 Java 不变,for 有改动,举个栗子:

fun function() {
    // for (int i = 0; i < 10; i++)
    // 打印的 i 值 是一样的
    for (i in 0..9) {
        println(i)
    }
    for (i in 0 until 10) {
        println(i)
    }
    
    // 下面这段将依次打印 10-0
    for (i in 10 downTo 0) {
        println (i)
    }
    
    // 还可以控制步长
    // 依次打印 0, 2, 4, 6, 8, 10
    for (i in 0..10 step 2) {
        println(i)
    }
}

10. 扩展函数

对于任何类型,我们都可以擅自给他添加新的函数,然后就可以用这个类对象调用我们自定义的扩展函数了,直接看代码:

fun String.suffix(suffix: String): String {
    return this + suffix
}

fun function() {
    val str = "Hello"
    val newStr = str.suffix(", World!")
    println(str)
    println(newStr)
}

// 将依次输出:Hello
//           Hello, World!

11. 高阶函数

高阶函数是将函数用作参数或返回值的函数。

在这里我们会接触到一个东西:闭包, 闭包就是能够读取其他函数内部变量的函数。

class Operator {
    fun add(a: Int, b: Int) = a + b
    fun minus(a: Int, b: Int) = a - b
    
    fun change(
        a: Int,
        b: Int,
        closure: (Int, Int) -> Int
    ): String {
        return "${closure(a, b)}"
    }
}

fun main() {
    val operator = Operator()
    val add: (Int, Int) -> Int = operator::add
    val str1 = operator.change(2, 2, add)
    val str2 = operator.change(5, 2) { a: Int, b: Int ->
        a - b
    }
    println("str1 = $str1")
    println("str2 = $str2")
}

// 输出:str1 = 4
//      str2 = 3

从 main 函数中,可以看到变量 add 拥有函数类型 (Int, Int) -> Int ,这意味着这个变量只能被函数赋值,对象名::add 表示拿到这个函数的函数类型。

接着看下面的 change 函数,这个大括号里面的代码其实就是闭包,虽然看起来好像是重新定义了一个函数,当一个函数的参数列表最后一个参数是一个闭包时,可以单独将闭包抽出来放在小括号后面,如果参数列表只有一个闭包,那么小括号都可以省略。闭包中,-> 左边表示闭包的参数,类型可以省略。

闭包其实看起来有点像回调,我们先写好代码,然后在 change 函数中进行调用。

函数类型可以没有参数,但是小括号是不能省略的,包括返回类型也不可省略,例如一个没有参数没有返回值的函数类型,可以这样表示:() -> Unit

另外函数类型也是可空的,举个栗子:

fun main() {
    // 当加上?时,函数类型需要用括号括起来
    val function: (() -> Unit)? = {
        // ...
    }
    
    // 调用时需要用 invoke 进行调用
    function?.invoke()
}