自 2017 年 kotlin 成为 Android 开发一级语言,也没有认真总结,一直对 kotlin 的骚操作没有足够的认知,这篇文从就是提升对 kotlin 骚操作的认知 ~ 。

快速上手

基础语法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var age: Int = 18
val name = "台风眼" // 如果赋值 可以不用声明类型

val name2:String = null // 报错
var name2: String? = null // 编译通过 ? 表示可以为 null

fun main(args: Array<String>) {
name = name2 // 编译报错 因为 name2 可以为 null ,name 不可以为 null 所以不能赋值

name = name2!! // 编译通过 !! 表示 name2 确定不可能为 null

name2 = name // 编译通过 name2 包含的范围比 name 大

println("年龄:$age")
println("名字:$name")
}

Java 与 Kotlin 交互

1
2
3
4
5
6
7
8
9
// Utils.kt
fun echo(name:String){
print(name)
}

// Main.java
public static void main(String[] args) {
UtilsKt.echo("abc");
}

Kotlin 函数直接可以写在文件里,不需要写在类里(但是编译后依旧是在类里),Java 在调用 Kotlin 文件时,只需要在 文件名 后加上 kt 如上 UtilsKt 。Utils 文件中的所有函数和类型变量,最终编译后都会被 public static 修饰。

匿名内部类的调用:

1
2
3
4
5
6
7
8
9
10
11
12
// Utils.kt 匿名内部类
object StaticTest {
fun sayMessage(msg: String) {
println(msg)
}
}

// Kotlin
StaticTest.sayMessage("1")

// Java
StaticTest.INSTANCE.sayMessage("1");

Class 的调用:

1
2
3
4
5
6
7
8
9
10
// Utils.kt
fun testClass(clazz: Class<JavaMain>){
println(clazz.simpleName)
}

// Kotlin
testClass(JavaMain::class.java)

// Java
UtilsKt.testClass(JavaMain.class);

这是为什么呢?这是因为 Kotlin 与 Java 的 Class 格式是不一致的。Kotlin Class 类型是 KClass 。

当 Java 的变量使用到了 Kotlin 的关键字时, Kotlin 调用这个变量需要加上 ``` `

1
2
3
4
5
// JavaMain.java
public static int in = 1;

// Kotlin
JavaMain.`in`

一些问题

Kotlin 没有封装类

封装类是什么?就是 Java 中的 Integer、String 等。

Java 中的封装类在 Kotlin 都会转换成基本数据类型调用

比如 Java 中有这么一个接口,然后用 Java 的类去实现接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AInterface {
void putNumber(int num);
void putNumber(Integer num);
}

public class A implements AInterface {
@Override
void putNumber(int num) {
System.out.println("int");
}

@Override
void putNumber(Integer num) {
System.out.println("Integer");
}
}

Kotlin 在调用调用的时候,提示用只有 Kotlin 的 Int 类型。

最终的打印也只能打印出 int 。而且如果 A 是一个 Kotlin 类的话,重写方法也只能重写一个,否则会报错。

说明 Kotlin 在运行时只会执行它基本数据类型的那个方法,而不会执行封装类型的方法 。Kolin 只有 Int 没有 Integer 类型。

Kotlin 类型空值敏感

通过一段代码说明,首先 Java 有一端这样的代码

1
2
3
public static String format(String string) {
return string.isEmpty() ? null : string;
}

Kotlin 去调用

1
2
3
var fmt1 = A.format(str) //String!
var fmt2:String = A.format(str)
var fmt3:String? = A.format(str)

分别有三种返回类型接收,

第一种是编译器自动推断的返回 String! ,这个类型只会在 Kotlin 互相调用的时候才会出现,我们是不可以手动声明这个类型的。

第二种是手动声明的 String 没有任何符号,第三种是一种可空的类型 String?

如果我们将 str ="" ,那么 fmt2 将会报错,因为,我们返回的是一个 null 但是 String 却不能为空,所以会报 IllegalStateException:format(str) must not be null

但是这并不表示其他返回类型就是可靠的,如果我们在分别调用一个方法

1
2
println(fmt1.length)
println(fmt3?.length)

执行发现 fmt1.length 会报空指针,因为 String! 是一个兼容类型,只能临时的使用它,但是如果调用这个值的方法,它会和 Java 语法调用一样去执行,在 Java 中 null 调用任何方法都会报错。

而 fmt3.length 不会报错,会输出一个 null 字符串

说明,如果在赋值的时候我们不确定一个返回值是否为 null ,一定要赋值为可控类型 String? ,这要代码才能可靠。

Kotlin 没有静态变量与静态方法

1
2
3
4
5
6
7
8
9
10
11
12
// Utils.kt 匿名内部类
object StaticTest {
fun sayMessage(msg: String) {
println(msg)
}
}

// Kotlin
StaticTest.sayMessage("1")

// Java
StaticTest.INSTANCE.sayMessage("1");

这是上面讲过的一段代码,如果我们想让 Java 调用方式和 Kotlin 一样,或者说 sayMessage 编译后生成的是 public static 的方法。我们可以加入 @JvmStatic 就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
// Utils.kt 匿名内部类
object StaticTest {
@JvmStatic
fun sayMessage(msg: String) {
println(msg)
}
}

// Kotlin
StaticTest.sayMessage("1")

// Java
StaticTest.sayMessage("1");

函数 与 Lambda 闭包

参数默认值

1
2
3
fun echo(name: String) {
print(name)
}

kotlin 语法可以给参数设置默认值,在 kotlin 调用的时候可以不传参数使用默认值。

1
2
3
fun echo(name: String = "CalmCenter") {
print(name)
}

默认参数通常用于重载函数过多时,默认参数可以大大减小重载函数的数量

当函数体只有一个语句是,可以将语句赋值给这个函数

1
fun echo3(name: String = "CalmCenter") = print(name)

内部函数

内部函数可以访问外部函数的局部变量,通常用在某些条件下触发递归的函数,或者不希望被外部函数访问到的函数。会降低代码的可读性

1
2
3
4
5
6
7
8
9
10
11
12
fun function() {
val str = "hello world"

fun say(count: Int = 10) {
println(str)
// 条件递归
if (count > 0) {
say(count - 1)
}
}
say()
}

扩展函数

扩展函数书写方式,既然是函数肯定得 fun ,后面跟随 需要扩展的类名.需要扩展的成员方法

1
2
fun File.readText(charset: Charset = Charsets.UTF_8): String =
readBytes().toString(charset)

这是 FileReadWrite.kt 中的一段代码,用来扩展 File 类。

在 kotlin 中的调用方式和 File 本身的成员函数使用方法相同,但这个函数是使用扩展函数静态添加的一个函数

1
2
val file=File("Main.kt")
println(file.readText())

Java 在调用时略有不同,Java 在调用扩展函数时,需要用到 Kotlin 生成的那个类 xxxKt ,第一个参数是需要扩展的那个类的对象,第二个参数是函数本身的参数,但是函数即使写了默认参数,Java 调用时也得传,默认参数对 Java 端没有效果。

1
2
3
File file = new File("Main.kt");
String content = FilesKt.readText(file, Charsets.UTF_8);
System.out.println(content);

扩展函数是静态的给一个类添加函数,需要注意,静态的不具备多态效果

通常用于第三方 SDK 中,不能够控制的类中,添加一些需要用到的方法的时候

Lambda 闭包

1
2
3
4
5
val thread = Thread(object :Runnable{
override fun run() {

}
})
  • Lambda 闭包可以省略冗余信息,没有参数的方法,还可以省略 ->

    • val thread = Thread(Runnable { -> Unit })
      val thread = Thread(Runnable { })
      val thread = Thread({})
      <!--code19-->
  • 如果除 lambda(匿名函数)后没有其他参数,则可以省略类名后的 ()

    • val thread = Thread {}
      <!--code20-->
      

很显然,kotlin 要方便很多。

声明一个闭包

1
2
3
var print = { name:String ->
println(name)
}

大括号括起来的地方就是闭包,闭包可以有参数,但是参数有上限,最多 22 个。多于 22 会报 NoClassDefFoundError: kotlin/Function23

因为 kotlin 的类在编译后会被编译成 class 文件,kotlin 的 lambda 在编译后会被编译成匿名内部类,有多少个参数就会生成 FunctionX,但是前提得有这个类。

在 Functions.kt 中,声明了 0 ~ 22 个参数的 Function 。所以如果想要使用 23 个参数的闭包,我们需要自己创建这个类。

在报错中我们可以看到 kotlin/Funtion23 ,说明这个类应该在包名为 kotlin 下放着。

但是如果我们使用 kotlin 语言中在 kolint 包下创建一个 Function23 ,编译时会有一个报错 Only the Kotlin standard library is allowed to use the kotlin package,只有 Kotlin 标准库才能使用 kotlin 这样的包名。

遇到这个问题,我们就应该想到 Kotlin 和 Java 是完全兼容的,如果我们不能用 Kotlin 语言声明一个类,我们可以使用 Java 语言实现。

在 kotlin 包下创建 Funtion23.java 文件

1
2
3
4
5
package kotlin;

public interface Function23<P1, P2, P3, P4, P5, P6, P7, P8, P9, P10, P11, P12, P13, P14, P15, P16, P17, P18, P19, P20, P21, P22,P23, R> extends Function<R> {
R invoke(P1 p1, P2 p2, P3 p3, P4 p4, P5 p5, P6 p6, P7 p7, P8 p8, P9 p9, P10 p10, P11 p11, P12 p12, P13 p13, P14 p14, P15 p15, P16 p16, P17 p17, P18 p18, P19 p19, P20 p20, P21 p21, P22 p22, P23 p23);
}

调用闭包函数时,和函数方法调用一样

1
print("hello")

高阶函数

函数 或 lambda(匿名函数) 的参数又是一个 函数 或者 lambda 时,这个函数称为高阶函数

1
2
3
4
5
fun onlyif(isDebug: Boolean, block: () -> Unit) {
if (isDebug) {
block()
}
}
  • 高阶函数也是函数,肯定也得用 fun 修饰
  • 第二个匿名函数 返回值为 Unit
    • Unit 表示没有返回值的函数,默认返回的一个隐藏类型,在函数作为参数时必须显示表示出返回值

高阶函数的使用,

1
2
3
onlyif(true) {
println("打印")
}

函数的声明与传递

获取函数声明,可以使用 (对象名::方法名) ,获取声明后可以赋值给函数对象,并且可以传递到高阶函数中。

高阶函数的参数传递时,只能是匿名函数或者一个函数声明,(对象名.方法名) 表示执行这个函数。

1
2
3
4
5
6
7
8
9
10
val runnable = Runnable {
println("Runnable::run")
}
// 函数对象
val function: () -> Unit
// runnable::run (对象::方法名) 这种写法表示这个函数的声明, 而不是 runnable.run() ,这表示执行这个函数
// 将 run 函数的声明 赋值给 function
function = runnable::run
// 传递高阶函数的时候必须传递函数的声明,如果传递的是 runnable.run() ,实际传递的是 函数的返回值
onlyif(true,function)

内联优化代码

使用 inline 修饰一个函数

Kotlin 的 Lambda 在编译后会生成一个匿名对象

可以使用 inline 修饰方法,这样当方法在编译时就会拆解方法的调用为语句调用(把函数内的方法直接复制到调用的地方),进而减少创建不必要的对象

1
2
3
4
5
6
7
8
9
inline fun onlyif(isDebug: Boolean, block: () -> Unit) {
if (isDebug) {
block()
}
}

onlyif(true) {
println("打印")
}

编译后,调用的地方会直接替换成以下代码。

1
2
3
4
5
boolean isDebug = true;
if(isDebug){
String str = "打印";
System.out.println(str);
}

过度使用 inline 会增加编译器的编译负担,并且回加大编译后的代码块,所以 inline 通常只会用于修饰高阶函数,不会随便使用。

类与对象

1
open class MainActivity : AppCompatActivity(), DialogInterface.OnClickListener
  • kotiln 声明使用 class 一个类
  • : 后跟随父类或接口,如果没有指定父类,那么他的父类是 Any
  • 实现接口只需要用 , 分割,继续添加就可以,不需要使用 implements 来声明,接口和父类没有先后关系。
  • kotlin 的类默认 public final 的,如果不需要 final ,则需要添加 open 关键字

构造函数

1
open class MainActivity(var int: Int) : AppCompatActivity()

声明一个构造函数,可以在类名后直接写构造参数,而不是写一个构造方法

如果想在构造函数中写逻辑,可以写在 init 代码块,init 是类的构造函数被调用的时候去执行的

1
2
3
init {
println("===init")
}

如果有多个构造函数,需要显示的声明次级构造函数,主构造函数和次级构造函数可以同时写

1
2
3
4
5
6
7
class TestView : View {
constructor(context: Context?) : super(context){
println("constructor")
}
constructor(context: Context?, attrs: AttributeSet?) : this(context, attrs,0)
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
}

主构造函数跟在类名后,这里重写了 三个次级构造函数,次级构造函数必须 直接 或 间接 的 继承 主构造函数 或 父类构造函数,比如上面代码中的这样 : super(context) 或 : this(context, attrs,0)

访问修饰符

在 koltin 中访问修饰符有:

  • private:类所有成员都是私有的
  • protected:这个类以及子类都可以访问
  • public:所有类都可以访问
  • internal:同一模块可以访问(同一 library/module)

可用于项目结构化扩展。

伴生对象

之前说过 Kotlin 中没有静态方法,解决办法之前也说过一种加入 @JvmStatic ,就可以在编译后生成静态方法

还有第二种方法,就是伴生对象 companion object

1
2
3
4
5
6
7
8
9
10
11
12
13
class TestCompanion {
companion object {
fun sayMessage(msg: String) {
println(msg)
}
}
}

// Java
TestCompanion.Companion.sayMessage("1");

// Kotlin
TestCompanion.sayMessage("1");

伴生对象,在编译器编译后会在这个类里生成一个 companion 静态对象,Java 在调用时是调用静态对象,再调用内部方法。

那 object 和 companion object 有啥区别呢?

  • object 声明(一个类)是延迟加载的,只有当第一次被访问时才会初始化
    • object 可以定义在全局也可以在类的内部使用
  • companion object 是当包含它的类被加载时就初始化了的,这一点和 Java 的 static 还是一样的
    • companion object 只能定义在对应的类中
    • companion object 就是 Java 中的 static 变量

单例类

之前通过 object 生成过匿名内部类,这也是一种单例的写法

还有一种更好的写法就是通过伴生对象实现

1
2
3
4
5
6
7
8
9
10
11
12
13
class Single private constructor(){
companion object{
fun get():Single{
return Holder.instance
}
}
private object Holder{
val instance = Single()
}
}

// 调用
Single.get()

动态代理

Kotlin 默认支持动态代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Animal {
fun bark()
}

class Dog : Animal {
override fun bark() {
println("Wang")
}
}
// 代理执行,将传入的 animal 作为代理,代理这个对象执行方法。
class Zoo(animal: Animal):Animal by animal

fun mainProxy(){
Zoo(Dog()).bark()
}

如果 Zoo 重写了 bark 方法,那么代理将会失效

1
2
3
4
5
class Zoo(animal: Animal):Animal by animal{
override fun bark() {
println("Zoo")
}
}

将会返回 Zoo 。

Kotlin 的动态代理在编译后将会转换为静态代理去调用。所以 Kotlin 的动态代理一定比 Java 的动态代理效率高。因为 Java 的动态代理是通过反射实现的。

Kotlin 特有的类

  • 数据类

数据类是 Kotlin 中很特殊的一个类

1
data class User(var id:Int,var name:String)

它可以自动的将类中的成员变量生成 geter()/setter() ,以及我们经常需要重新的 toString()/hashCode()/equals()/copy() ,这些方法都是由编译器帮我们重写好了。

注意:数据类是 final 类型的,不可以添加 open 去修饰它。所以它不可以被继承。

  • 密闭类

Kotlin 中也有枚举类,和 Java 使用方式一样,但是平常 Kotlin 是不适用枚举类的,而是使用 密闭类

1
2
3
4
5
6
7
8
9
10
11
12
13
sealed class SuperCommand() {
object A :SuperCommand()
object B :SuperCommand()
object C :SuperCommand()
object D :SuperCommand()
}

fun exec(superCommand: SuperCommand) = when(superCommand){
SuperCommand.A->{}
SuperCommand.B->{}
SuperCommand.C->{}
SuperCommand.D->{}
}

密闭类通过 sealed 修饰,密闭类可以有子类,但是必须在同一个文件中,所以子类一般写在密闭类内部。如上

密闭类使用方法和枚举基本相同,它的最大的特性就是可以扩展它的子类

比如我们需要对一个 View 做上下左右的移动 或者 放大缩小,可以通过密闭类,轻松的实现各个功能的分工,而且子类也可以传入参数,可以更灵活的控制和操作每一个选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sealed class SuperCommand() {
object UP : SuperCommand()
object DOWN : SuperCommand()
object LEFT : SuperCommand()
object RIGHT : SuperCommand()
class PACE(var pace: Int) : SuperCommand()
}

fun exec(view: View, superCommand: SuperCommand) = when (superCommand) {
SuperCommand.UP -> {
}
SuperCommand.DOWN -> {
}
SuperCommand.LEFT -> {
}
SuperCommand.RIGHT -> {
}
is SuperCommand.PACE -> {
}
}

高级特性

解构

在 Kotlin 中,允许将一个类拆解然后分别赋值

需要用到 operator 重载运算符,将已经有的函数赋予它们新的约定

1
2
3
4
class User(var age: Int, var name: String) {
operator fun component1() = age
operator fun component2() = name
}

component1 component2 是固定的 componentX,如果需要解构这里不可以改变,也可以有 component3、component4...

类的解构

1
2
3
4
5
val user = User(12, "name")
// 将 user 赋值给一个拆解后的对象
val (age, name) = user
println(age)
println(name)

将 user 才拆解后,分别赋值给 age、name ,component1 代表括号中的第一个元素,component2 代表第二个元素,以此类推

Map 的解构

1
2
3
4
5
val map = mapOf("key1" to "value1", "key2" to "value2")
//for (entry in map) {
for ((k, v) in map) {
println("$k $v")
}

将原本的 entry 直接解构成 k , v 进行操作。

循环

按照正常 java 思路,循环应该这样写

1
2
var count: Int
for (count = 0;count < 10;count++){}

但是 kotlin 中没有这个语法,但是它有更多适合的语法

Kotlin 循环符

kotlin 用 .. 运算符,表示一个区间

循环 1 到 10 ,i 表示变量

1
2
3
for (i in 1..10){
println(i)
}

until

循环 1 到 9

1
2
3
for (i in 1 until 10){
println(i)
}

until 是一个扩展函数

1
2
3
4
5
public infix fun Int.until(to: Int): IntRange {
// 是否合法 否则返回 空区间
if (to <= Int.MIN_VALUE) return IntRange.EMPTY
return this .. (to - 1).toInt()
}

这里只拿 Int 举例, 最终也是使用 .. 运算符,执行到 to - 1

downTo

循环 10 到 1

1
2
3
for (i in 10 downTo 1){
println(i)
}

downTo 源码

1
2
3
4
public infix fun Int.downTo(to: Int): IntProgression {
// 创建一个闭区间,每次步长为 -1 ,直到 to
return IntProgression.fromClosedRange(this, to, -1)
}

step

循环 1 到 10 每次步长为 2,结果为 1 3 5 7 9

1
2
3
for (i in 1..10 step 2){
println(i)
}

step 源码

1
2
3
4
5
public infix fun IntProgression.step(step: Int): IntProgression {
// 只能为正数
checkStepIsPositive(step > 0, step)
return IntProgression.fromClosedRange(first, last, if (this.step > 0) step else -step)
}

in

1
2
3
4
5
6
7
8
9
var list = arrayListOf("a","b","c")
for (str in list) {
println(str)
}

// 解构
for ((index,str) in list.withIndex()) {
println("第 $index 个元素 $str")
}

list.withIndex 源码

1
2
3
public fun <T> Iterable<T>.withIndex(): Iterable<IndexedValue<T>> {
return IndexingIterable { iterator() }
}

可以看到这里返回 IndexedValue<T> ,它实际是一个 data class 。

1
public data class IndexedValue<out T>(public val index: Int, public val value: T)

repeat

这是一个高阶函数

1
2
3
4
5
6
7
8
@kotlin.internal.InlineOnly
public inline fun repeat(times: Int, action: (Int) -> Unit) {
contract { callsInPlace(action) }

for (index in 0 until times) {
action(index)
}
}

实际就是封装了一个 for 循环

集合操作符

与 rxjava 的操作符相同,rxjava 允许对数据进行一系列的链式调用,在每一步中对数据进行筛选或处理,最终得到我们想要的数据

kotlin 对集合添加了很多操作符,rxJava 有的 在 Kotlin 中基本上都有

这里简单介绍几个

1
2
3
4
5
6
7
8
        var list = arrayListOf('a', 'b', 'c', 'e')
var find = list.map { it - 'a' }
.filter { it > 0 }
.reduce { s, s1 ->
s1 - s
}
// .findLast { it>1 }
// .find { it > 1 }
  • map:将数据进行改变处理,输入值和输出值类型可以不一样

  • filter:筛选,返回满足条件的集合

  • find:查找,返回满足条件的第一个值

  • findLast:返回满足条件的最后一个值

  • reduce:可以同时拿到两个值进行操作,第一次计算的结果是第二次的第一个值

  • let:let 扩展函数的实际上是一个作用域函数,当需要去定义一个变量在一个特定的作用域范围内,let 函数的是一个不错的选择;let 函数另一个作用就是可以避免写一些判断 null 的操作。

    • var let:Int = user?.let {//表示 user 不为 null 的条件下,才会去执行 let 函数体
          println("my name is ${it.name}, I am ${it.age} years old, my phone number is ${it.phoneNum}") // it 就不需要再写 ?
          1000
      }
      <!--code56-->
      
      在使用 `User` 参数时,可以直接只用属性名,可以省去重复的 `user` 对象的书写,`result` 为函数块内最后一行或者 `return` 指定返回的值
  • run: let 和 with 两个函数的结合体,以闭包形式返回,返回值为最后一行的值或者指定的 return 的表达式。

    • var run:Int = user?.run {
          println("my name is $name, I am $age years old, my phone number is $phoneNum")
          1000
      }
      <!--code57-->
  • also:also 函数的结构实际上和 let 很像唯一的区别就是返回值的不一样,let是以闭包的形式返回,返回函数体内最后一行的值,如果最后一行为空就返回一个 Unit 类型的默认值。而 also 函数返回的则是传入对象的本身

    • var also:User = user?.also {
          println("my name is ${it.name}, I am ${it.age} years old, my phone number is ${it.phoneNum}")
      }
      <!--code58-->
      

MutableList 可变集合,在 Kotlin/JVM 平台中,它的实现就相当于是 ArrayList 。

作用域函数

中缀表达式

DSL