2017kotlin 成为 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 如上 UtilsKtUtils 文件中的所有函数和类型变量,最终编译后都会被 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);

这是为什么呢?这是因为 KotlinJavaClass 格式是不一致的。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 在调用调用的时候,提示用只有 KotlinInt 类型。

最终的打印也只能打印出 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 语法调用一样去执行,在 Javanull 调用任何方法都会报错。

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 文件,kotlinlambda 在编译后会被编译成匿名内部类,有多少个参数就会生成 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 这样的包名。

遇到这个问题,我们就应该想到 KotlinJava 是完全兼容的,如果我们不能用 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 修饰一个函数

KotlinLambda 在编译后会生成一个匿名对象

可以使用 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 在调用时是调用静态对象,再调用内部方法。

objectcompanion object 有啥区别呢?

  • object 声明(一个类)是延迟加载的,只有当第一次被访问时才会初始化
    • object 可以定义在全局也可以在类的内部使用
  • companion object 是当包含它的类被加载时就初始化了的,这一点和 Javastatic 还是一样的
    • 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 才拆解后,分别赋值给 agenamecomponent1 代表括号中的第一个元素,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.. 运算符,表示一个区间

循环 110i 表示变量

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

until

循环 19

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

循环 101

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: letwith 两个函数的结合体,以闭包形式返回,返回值为最后一行的值或者指定的 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