kotlin 实战之面向对象特性全方位总结

1,752 阅读16分钟

工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 ~

特别说明,kotlin 系列文章均以 Java 差异为核心进行提炼,与 Java 相同部分不再列出。随着 kotlin 官方版本的迭代,文中有些语法可能会发生变化,请务必留意,语言领悟精髓即可,差异只是语法层面的事情,建议不要过多关注语法。

类的定义

kotlin 的类定义比 java 简单了很多,一般类的定义如下(特殊类下面小节有总结):

//kotlin 类默认修饰符是 public 的,不用再显式指定 public
class MyClass {
}
//kotlin中一个类的类体没有任何内容则花括号可以省略,如下:
class MyClass

kotlin 中实例化一个类不需要 new 关键字,这点与 java 很不一样。

构造方法及初始化过程

在 kotlin 中一个类可以有一个 primary 构造方法以及一个或多个 secondary 构造方法。

//primary 构造方法是类头的一部分,位于类名后面,可以有若干个参数,缺省修饰符是 public
class TestClass constructor(name: String, age: Int) {
    private val name = name.toLowerCase()
    //初始化代码块中可以直接使用构造方法的参数,给类对象属性赋初值
    init {
        println(age)
    }
}

//私有构造方法的类定义
class TestClass1 private constructor(name: String)

//如果 primary 构造方法没有任何注解或是可见性关键字修饰,则 constructor 关键字可以省略
class TestClass2 (name: String)

多个构造方法声明的写法案例(包含代码执行顺序):

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class Person constructor(name: String) {
    private var name: String
    private var age: Int
    private var address: String

    init {
        println(name)   //name 是 constructor 构造方法的参数
        this.name = name
        this.age = 16
        this.address = "zhuhai"
    }
    //secondary 构造方法必须要直接或者间接调用其 primary 构造方法
    constructor(name: String, age: Int): this(name) {
        println("$name, $age")
        this.name = name
        this.age = age
        this.address = "hanzhong"
    }
    //secondary 构造方法间接调用其 primary 构造方法
    constructor(name: String, age: Int, address: String): this(name, age) {
        this.address = address
    }

    fun print() {
        println("name: ${this.name}, age: $age, address: $address")
    }
}

/**
 运行输出值为:
 ruoshui
 gongjiang
 gongjiang, 18
 android
 android, 19
 name: ruoshui, age: 16, address: zhuhai
 name: gongjiang, age: 18, address: hanzhong
 name: android, age: 19, address: guangdong
 */
fun testRun() {
    var person0 = Person("ruoshui")
    var person1 = Person("gongjiang", 18)
    var person2 = Person("android", 19, "guangdong")
    person0.print()
    person1.print()
    person2.print()
}

kotlin 类语法糖简写方式对上面案例的简写实现:

//简写:直接构造方法里声明类的成员变量属性
class Person1 (private val name: String,
                private var age: Int,
                private val address: String) {
    fun print() {
        println("name: ${this.name}, age: $age, address: $address")
    }
}

/**
 运行输出值为:
 name: yan, age: 18, address: guangdong
 */
fun testRun() {
    var person = Person1("yan", 18, "guangdong")
    person.print()
}

kotlin 的构造方法可以具备默认参数值,这点和 java 很不一样。如果 primary 构造方法的所有参数都有默认值,则编译器还会生成一个额外新的无参数构造方法,并且使用这个构造方法的默认值。具体用法类似如下:

//如果 primary 构造方法的所有参数都有默认值,则编译器还会生成一个额外新的无参数构造方法,并且使用这个构造方法的默认值
class Person2 (private val name: String = "ruoshui",
               private var age: Int = 20,
               private var address: String = "zhuhai") {
    fun print() {
        println("name: ${this.name}, age: $age, address: $address")
    }
}

class Person3 (private val name: String = "ruoshui",
               private var age: Int) {
    fun print() {
        println("name: ${this.name}, age: $age")
    }
}

/**
 运行输出值为:
 name: ruoshui, age: 20, address: zhuhai
 name: ruoshui, age: 12
 */
fun testRun() {
    var person = Person2()
    person.print()
    //因为 primary 构造方法参数默认值不全,所以不能使用无参构造
    var person1 = Person3(age = 12)
    person1.print()
}

继承与重写特性

与 java 类默认修饰符不同的另一点是,kotlin 中所有类默认都是无法被继承的,即 kotlin 中所有类默认都是 final 的。如下案例展示了类的继承:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

//默认 class 是 final 的,需要显式添加 open 以便可以被继承
open class Anim (name: String, age: Int)

//kotlin 通过 : 实现 java 的 extends 等关键字
class Dog (name: String, age: Int): Anim(name, age)

//kotlin 中如果一个类没有 primary 构造方法,
//则这个类的每个 secondary 构造方法就需要通过 super 关键字来初始化父类型,
//或是通过其他 secondary 构造方法完成这个任务。
//不同的 secondary 构造方法可以调用父类型不同的构造方法。
open class Anim1 (name: String) {
    init {
        println("Anim1--$name")
    }
}

class Dog1 : Anim1 {
    constructor(name: String): super(name) {
        println("dog1-0--$name")
    }

    constructor(name: String, age: Int): this(name) {
        println("dog1-2--$name")
    }
}

/**
运行输出值为:
Anim1--666
dog1-0--666
dog1-2--666
 */
fun testRun() {
    var anim = Dog1("666", 66)
}

接着我们再来看看 kotlin 中类方法的重写,这点与 java 也很不相同。kotlin 中如果一个方法想被重写,则必须显式指定 open 关键字,否则无法编译通过;子类中重写方法必须显示指定 override 关键字,否则无法编译通过。案例如下:

open class Developer {
    //kotlin 中如果一个方法想被重写,则必须显式指定 open 关键字,否则无法编译通过
    open fun skill() {
        println("developer")
    }

    //子类默认具备该能力,但无法重写
    fun money() {
        println("666")
    }

    //可以被子类继承
    open fun wife() {
        println("haha")
    }
}

open class Android: Developer() {
    //kotlin 中重写方法必须显示指定 override 关键字,否则无法编译通过
    override fun skill() {
        println("android")
    }

    //Android 类的 wife 方法重写了父类,但是无法被子类重写,因为声明了 final
    final override fun wife() {
        super.wife()
        println("green")
    }
}

/**
运行输出值为:
android
666
haha
green
 */
fun testRun() {
    var dev = Android()
    dev.skill()
    dev.money()
    dev.wife()
}

看完类方法的重写,我们再来看看 kotlin 的类属性重写。val 的属性可以被重写成 var,var 的属性不能被重写成 val。因为一个 val 属性相当于一个 get 方法,var 相当于一个 get 和 set 方法,所以增加范围是可以的,但是缩小范围是不行的(一旦缩小如果父类中有对 var 的 set 操作则没重写方法的情况下就会异常,所以缩小范围不被允许)。如下是使用案例:

open class PaPaPa {
    open val name: String = "pa"

    open fun print() {
        println("name=$name, this.name=${this.name}")
    }
}

open class XoXoXo: PaPaPa() {
    override val name: String = "xo"
    //val 只读属性默认只有 get 方法
    open val address: String get() = "beijing"
}

//属性重写简写方式
class NcNcNc (override var name: String = "nc"): XoXoXo() {
    override fun print() {
        super.print()
        println("address is $address")
    }

    //重写父类属性并调用父类属性
    override val address: String
        get() = "6666" + super.address
}

/**
运行输出值为:
name=nc, this.name=nc
address is 6666beijing
 */
fun testRun() {
    var oo = NcNcNc()
    oo.print()
}

和 java 一样,类的继承有一个特殊的,那就是接口,kotlin 中接口的实现和 java 比较类似,如下案例:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

interface Nb {
    //接口声明
    fun test1()
    //接口可以既有声明也可以有实现
    fun test() {
        println("nb")
    }
}

class ChildNb: Nb {
    override fun test1() {
        println("test1")
    }

    override fun test() {
        println("test")
        super.test()
    }
}

kotlin 为我们提供了一种混用冲突的解决方案,当继承的类和接口拥有相同签名方法时子类必须重写父类相同签名方法,否则无法编译通过,此外需要显式通过<>语法指定使用哪个基类的方法。案例如下:

interface Nb {
    fun test() {
        println("nb")
    }
}

open class Tt {
    open fun test() {
        println("tt")
    }
}

class ChildNb: Nb, Tt() {
    //当继承的类和接口拥有相同签名方法时子类必须重写父类相同签名方法,否则无法编译通过
    //此外需要显式通过<>指定使用哪个基类的方法
    override fun test() {
        super<Tt>.test()
        println("test")
        super<Nb>.test()
    }
}

/**
运行输出值为:
tt
test
nb
 */
fun testRun() {
    var oo = ChildNb()
    oo.test()
}

对于抽象类的声明与继承基本和 java 没啥区别,具体案例如下:

open class Base {
    open fun method() {
        println("base")
    }
}

abstract class Child: Base() {
    //父类的实现方法可以被抽象子类重写成抽象方法供当前抽象类的子类实现
    override abstract fun method()
}

class Child1: Child() {
    override fun method() {
        println("child1")
    }
}

对象声明及伴生对象

kotlin 的对象声明你可以理解成一种单例的语言层面支持实现能力。定义一个类用 class,声明一个对象用 object,全局声明的对象就是一个对象实例,且全局唯一。案例如下:

//定义了一个名为 TestObject 的对象实例
object TestObject {
    fun method() {
        println("666")
    }
}

/**
运行输出值为:
666
 */
fun testRun() {
    TestObject.method()
}

kotlin 的伴生对象相对于 java 来说也是一种新的特性,本质来说,kotlin 与 java 不同的是 kotlin 的类没有 static 方法。在大多数情况下 kotlin 推荐的做法是使用包级别的函数来充当静态方法角色,kotlin 会将包级别的函数当作静态方法看待。而 kotlin 中一个类最多只能有一个伴生对象,这个伴生对象也类似 java 的 static 成员。案例如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class Yan {
    //TestObj 名字可以省略,编译器默认名字是 Companion
    //一个类中最多只能有一个伴生对象
    companion object TestObj {
        var name: String = "666"
        fun method() = println("${this.name} -- method")
    }
}

/**
运行输出值为:
666 -- method
777 -- method
class cn.yan.test.Yan$TestObj
 */
fun testRun() {
    Yan.TestObj.method()
    //简写,kotlin 语法糖,没有 @jvmStatic 情况下本质还是转为了 Yan.TestObj 静态成员调用方式
    Yan.name = "777"
    Yan.method()
    println(Yan.TestObj.javaClass)
}

虽然伴生对象的成员看起来像是 java 的静态成员,但在运行期他们依旧是真实对象的实例成员。在 jvm 实现上可以将伴生对象的成员真正生成为类的静态方法与属性,具体通过@JvmStatic注解实现。

伴生对象的本质原理是在编译后生成一个静态内部类来实现的,我们对上面的案例代码进行反编译(javap)结果如下:

//反编译的 cn.yan.test.Yan 类
yandeMacBook-Pro:test yan$ javap -c Yan.class
Compiled from "Test2.kt"
public final class cn.yan.test.Yan {
  //Yan 这个 kotlin 类中的伴生对象名生成了一个静态成员变量,名字为 TestObj
  public static final cn.yan.test.Yan$TestObj TestObj;
  //Yan 这个 kotlin 类的构造函数
  public cn.yan.test.Yan();
    Code:
       0: aload_0
       1: invokespecial #8                  // Method java/lang/Object."<init>":()V
       4: return
  //Yan 这个 kotlin 类的 static 代码块实例化了Yan$TestObj静态内部类并赋值给当前类的静态成员属性TestObj
  static {};
    Code:
       0: new           #38                 // class cn/yan/test/Yan$TestObj
       3: dup
       4: aconst_null
       5: invokespecial #41                 // Method cn/yan/test/Yan$TestObj."<init>":(Lkotlin/jvm/internal/DefaultConstructorMarker;)V
       8: putstatic     #43                 // Field TestObj:Lcn/yan/test/Yan$TestObj;
      11: ldc           #45                 // String 666
      13: putstatic     #20                 // Field name:Ljava/lang/String;
      16: return
  //在 Yan 这个类中新增了伴生对象里面属性 var name 的 get 操作
  public static final java.lang.String access$getName$cp();
    Code:
       0: getstatic     #20                 // Field name:Ljava/lang/String;
       3: areturn
  //在 Yan 这个类中新增了伴生对象里面属性 var name 的 set 操作
  public static final void access$setName$cp(java.lang.String);
    Code:
       0: aload_0
       1: putstatic     #20                 // Field name:Ljava/lang/String;
       4: return
}

如下是 Yan 这个 kotlin 类中伴生对象生成的静态内部类反编译代码:

//反编译的 cn.yan.test.Yan 类中伴生对象生成的静态内部类 Yan$TestObj
yandeMacBook-Pro:test yan$ javap -c Yan\$TestObj.class
Compiled from "Test2.kt"
public final class cn.yan.test.Yan$TestObj {
  //伴生对象内部属性 var name 的 get 方法
  public final java.lang.String getName();
    Code:
       0: invokestatic  #12                 // Method cn/yan/test/Yan.access$getName$cp:()Ljava/lang/String;
       3: areturn
  //伴生对象内部属性 var name 的 set 方法
  public final void setName(java.lang.String);
    Code:
       0: aload_1
       1: ldc           #18                 // String <set-?>
       3: invokestatic  #24                 // Method kotlin/jvm/internal/Intrinsics.checkParameterIsNotNull:(Ljava/lang/Object;Ljava/lang/String;)V
       6: aload_1
       7: invokestatic  #27                 // Method cn/yan/test/Yan.access$setName$cp:(Ljava/lang/String;)V
      10: return
  //伴生对象内部定义的方法
  public final void method();
    Code:
       0: new           #32                 // class java/lang/StringBuilder
       3: dup
       4: invokespecial #35                 // Method java/lang/StringBuilder."<init>":()V
       7: aload_0
       8: checkcast     #2                  // class cn/yan/test/Yan$TestObj
      11: invokevirtual #37                 // Method getName:()Ljava/lang/String;
      14: invokevirtual #41                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: ldc           #43                 // String  -- method
      19: invokevirtual #41                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      22: invokevirtual #46                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      25: astore_1
      26: iconst_0
      27: istore_2
      28: getstatic     #52                 // Field java/lang/System.out:Ljava/io/PrintStream;
      31: aload_1
      32: invokevirtual #58                 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
      35: return
  //伴生对象生成的静态内部类的构造方法
  public cn.yan.test.Yan$TestObj(kotlin.jvm.internal.DefaultConstructorMarker);
    Code:
       0: aload_0
       1: invokespecial #61                 // Method "<init>":()V
       4: return
}

接着我们将上面 kotlin 案例代码添加@JvmStatic注解,代码如下:

class Yan {
    companion object TestObj {
        @JvmStatic
        var name: String = "666"
        @JvmStatic
        fun method() = println("${this.name} -- method")
    }
}

上面代码反编译结果如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

yandeMacBook-Pro:test yan$ javap -c Yan.class
Compiled from "Test2.kt"
public final class cn.yan.test.Yan {
  //与上面没注解类似,静态属性
  public static final cn.yan.test.Yan$TestObj TestObj;
  //与上面没注解类似,构造方法
  public cn.yan.test.Yan();
    Code:
       ......
  //与上面没注解类似,静态代码块并对静态成员赋值
  static {};
    Code:
       ......
  //与上面没注解类似,内部指令 access 方法
  public static final java.lang.String access$getName$cp();
    Code:
       ......
  //与上面没注解类似,内部指令 access 方法
  public static final void access$setName$cp(java.lang.String);
    Code:
       ......
  //添加@JvmStatic后在伴生对象依赖的 Yan 类中添加的 static 属性 get 方法
  public static final java.lang.String getName();
    Code:
       0: getstatic     #39                 // Field TestObj:Lcn/yan/test/Yan$TestObj;
       3: getstatic     #20                 // Field name:Ljava/lang/String;
       6: areturn
  //添加@JvmStatic后在伴生对象依赖的 Yan 类中添加的 static 属性 set 方法
  public static final void setName(java.lang.String);
    Code:
       0: getstatic     #39                 // Field TestObj:Lcn/yan/test/Yan$TestObj;
       3: aload_0
       4: putstatic     #20                 // Field name:Ljava/lang/String;
       7: return
  //添加@JvmStatic后在伴生对象依赖的 Yan 类中添加的 static 方法的调用
  public static final void method();
    Code:
       0: getstatic     #39                 // Field TestObj:Lcn/yan/test/Yan$TestObj;
       3: invokevirtual #46                 // Method cn/yan/test/Yan$TestObj.method:()V
       6: return
}

如下是 Yan 这个 kotlin 类中伴生对象生成的静态内部类反编译代码:

yandeMacBook-Pro:test yan$ javap -c Yan\$TestObj.class
Compiled from "Test2.kt"
public final class cn.yan.test.Yan$TestObj {
  //与上面不加注解生成的基本没区别,所以不再列出
  ......
}

通过上面例子可以看出,对于伴生对象成员加不加@JvmStatic注解对使用者没有任何区别,只对编译后生成的字节码有区别,区别如上分析,但是一定要记住,伴生对象的本质是通过静态内部类和静态成员实现的。

对象表达式与对象声明、伴生对象的区别:

  • 对象表达式是立刻初始化或者执行的。
  • 对象声明是延迟初始化的,在首次访问的时候进行。
  • 伴生对象是在其所对应的类被加载时初始化,对应 java 的静态初始化。

属性及延迟初始化

kotlin 中类成员属性的定义与 java 类似,如下案例:

class Do(name: String, address: String) {
    //定义只读属性
    val name: String
        get() = "gongjiang"
    //定义可读可写属性
    var address: String = address
        get() {
            println("get address")
            return field
        }
        set(value) {
            println("set address")
            //这里不能用 this.address = value,否则死循环
            field = value
        }
}

/**
 运行结果:
 gongjiang
 set address
 get address
 hanzhong
 */
fun testRun() {
    val dx = Do("yb", "zhuhai")
    //本质调用的 name 属性的 get 方法
    println(dx.name)
    dx.address = "hanzhong"
    println(dx.address)
}

我们对上面 Do 类进行反编译,结果如下:

yandeMacBook-Pro:test yan$ javap Do.class
Compiled from "Test2.kt"
public final class cn.yan.test.Do {
  //val 定义的只读属性 name 只生成了 public 的 get 方法
  public final java.lang.String getName();
  //val 定义的读写属性 address 生成了 public 的 get、set 方法
  public final java.lang.String getAddress();
  public final void setAddress(java.lang.String);
  //生成了构造方法
  public cn.yan.test.Do(java.lang.String, java.lang.String);
}

kotlin 要求不可空类型的属性必须要在构造方法中进行初始化,有时这种要求不太方便,譬如对于依赖注入方式(Spring、Room 等),这时可以通过 lateinit 关键字标示属性为延迟初始化,延迟初始化需要满足三个条件:

  • lateinit 只能用在类体中声明的 var 属性上,不能用在 primary 构造方法声明的属性。
  • 属性不能拥有自定义的 get、set 方法。
  • 属性类型要为非空且不能是原生类型(譬如 Int)。

如下是属性延迟初始化的案例:

class Qq {
    //延迟初始化,不添加就必须初始化或者申明为 String? 类型
    lateinit var name: String
    //无法编译通过:Property must be initialized or be abstract
    //var age: Int

    //无法编译通过:'lateinit' modifier is allowed only on mutable properties
    //lateinit val address: String

    //无法编译通过:'lateinit' modifier is not allowed on properties of primitive types
    //lateinit var count: Int

    fun print() {
        println(name)
    }
}

/**
 运行结果直接崩溃:
 Exception in thread "main" kotlin.UninitializedPropertyAccessException: lateinit property name has not been initialized
 at cn.yan.test.Qq.print(Test2.kt:324)
 at cn.yan.test.Test2Kt.testRun(Test2.kt:337)
 at cn.yan.test.Test2Kt.main(Test2.kt:22)
 at cn.yan.test.Test2Kt.main(Test2.kt)
 */
fun testRun() {
    val qq = Qq()
    qq.print()
}

可见性及 kotlin 扩展

kotlin 提供了四种可见性修饰符:private、protected、internal、public。对于 kotlin 可见性修饰符默认不写就是 public。

kotlin 文件中顶层非类内部的方法或者属性或者类添加 private 修饰符则使用范围为当前文件。

internal 表示只能在同一个模块下使用,java 是没有模块的概念,kotlin 的模块是指在最后编译时编译到一起的一个模块。

protected 修饰符不能用在顶层类或者函数、属性中。可以用在非顶层申明中,范围为子类或者当前类。

局部变量是没有访问修饰符的概念。

kotlin 的扩展相对于 java 来说是不可思议的概念,如果我们已经定义了一个类,现在想给这个类添加额外的功能,用 java 方式解决的办法只有继承或者装饰者实现,而对于 kotlin 来说,扩展可以轻松做到这一点。如下是一个扩展案例:

//Test2.kt 文件
//假设这个类不是自己写的或者是存在于 jar 中的类
class Xunit {
    var count: Int = 0

    fun add(a: Int, b: Int) = a + b
    fun sub(a: Int, b: Int) = a - b

    fun printCount() = println("count=$count")
}

//kotlin 扩展给 Xunit 增加方法,背后本质中 multi 方法不会被编译插入到 Xunit 类中
fun Xunit.multi(a: Int, b: Int) = a * b
fun Xunit.doCount(a: Int) {
    this.count = a
}

/**
 运行结果:
 2
 1
 4
 count=12
 */
fun testRun() {
    val xunit = Xunit()
    println(xunit.add(1, 1))
    println(xunit.sub(2, 1))
    //调用类的扩展函数,本质是一种语法糖
    println(xunit.multi(2, 2))
    xunit.doCount(12)
    xunit.printCount()
}

上面的扩展代码实现是不是很神奇,为了探究扩展的实现原理,我们需要明确的是,扩展本身不会真正的修改目标类,方法不会被编译插入到 Xunit 类中,而是在其定义的类中。可以看上面案例的反编译字节码得知:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

yandeMacBook-Pro:test yan$ javap Xunit.class
Compiled from "Test2.kt"
//Xunit 类中没有我们扩展的属性和方法
public final class cn.yan.test.Xunit {
  public final int getCount();
  public final void setCount(int);
  public final int add(int, int);
  public final int sub(int, int);
  public final void printCount();
  public cn.yan.test.Xunit();
}

扩展方法代码反编译实现:

Compiled from "Test2.kt"
//multi 和 doCount 扩展方法在此,且多了一个 Xunit 类型的第一个参数,所以本质是一种封装实现
public final class Test2Kt {
  //两扩展方法为静态方法
  public static final int multi(Xunit, int, int);
  public static final void doCount(Xunit, int);
  public static final void testRun();
  public static final void main();
  public static void main(java.lang.String[]);
}

通过反编译可以发现,扩展本身不会真正的修改目标类,而是被编译进了定义的类中。此外可以发现扩展函数的解析是静态分发的,不是动态的,不支持多态,调用只取决于对象的声明类型,而不是运行实际类型,如下:

open class Parent
class Children: Parent()

fun Parent.method() = "parent"
fun Children.method() = "children"

/**
 运行结果:
 parent
 children
 parent
 */
fun testRun() {
    val tmp1 = Parent()
    println(tmp1.method())
    val tmp2 = Children()
    println(tmp2.method())
    val tmp3: Parent = Children()
    println(tmp3.method())
}

接着再来看下扩展方法是否能够被继承的表现,如下两个例子:

open class Parent
class Children: Parent()

fun Parent.method() = "parent"

/**
 运行结果:
 parent
 parent
 parent
 */
fun testRun() {
    val tmp1 = Parent()
    println(tmp1.method())
    val tmp2 = Children()
    println(tmp2.method())
    val tmp3: Parent = Children()
    println(tmp3.method())
}
open class Parent
class Children: Parent()

fun Children.method() = "children"

/**
 编译报错
 */
fun testRun() {
    val tmp1 = Parent()
    //println(tmp1.method()) 编译错误:找不到方法 method
    val tmp2 = Children()
    println(tmp2.method())
    val tmp3: Parent = Children()
    //println(tmp3.method()) 编译错误:找不到方法 method
}

当扩展函数签名与被扩展类已有函数签名发生重名操作时(尽量不要这么做),则类原有方法优先级更高。案例如下:

fun Sss.method() {
    println("sss method 2")
}
fun Sss.method(str: String) {
    println("sss method 2 $str")
}

/**
 运行结果:
 sss method 1
 sss method 2 666
 */
fun testRun() {
    val ss = Sss()
    //如果类中方法与其扩展方法签名完全一致,则类原有方法优先级更高
    ss.method()
    //扩展支持方法重载
    ss.method("666")
}

此外,kotlin 还可以对可空类型进行扩展,使用案例如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

fun Any?.toString(): String {
    if (this == null) {
        return "null---"
    }
    return toString()
}

/**
 运行结果:
 null---
 1
 */
fun testRun() {
    var tmp: Int? = null
    println(tmp.toString())
    tmp = 1
    println(tmp.toString())
}

除过扩展函数,kotlin 的扩展属性与扩展函数基本类似。案例如下:

class Fff
val Fff.name: String get() = "gongjiang"

/**
 运行结果:
 gongjiang
 */
fun testRun() {
    val ff = Fff()
    println(ff.name)
}

此外我们还可以对伴生对象进行扩展。如下案例:

class Fff {
    companion object {
        val name: String = "999"

        fun go() {
            println("go--$name")
        }
    }
}

fun Fff.Companion.method() {
    println("method--${this.name}")
}

/**
 运行结果:
 method--999
 go--999
 */
fun testRun() {
    val ff = Fff()
    //ff.method() 编译错误,对象没有这个方法
    //ff.go() 编译错误,对象没有这个方法
    Fff.method()
    Fff.go()
}

扩展也是有作用域的,上面的扩展案例都是定义在 kotlin 的顶层文件中,其实扩展也可以定义在另外的类中。扩展函数所定义在的类实例叫做分发接受者(dispatch receiver),扩展函数所扩展类的实例叫做扩展接收者(extension receiver),当以上两个名字出现冲突时,扩展接收者的优先级更高。案例演示如下:

class HH {
    fun method() = println("HH method")
}

class KK {
    fun method2() = println("KK method2")

    fun HH.inn() {
        //可以直接使用 HH 类已经拥有的成员
        method()
        //也可以使用定义这个扩展函数所在类的成员
        method2()
    }

    fun go(hh: HH) {
        //KK 中可以直接调用 HH 的扩展成员,因为扩展定义在 KK 中
        hh.inn()
    }

    fun HH.out() {
        //HH与KK都有toString()方法,所以实际调用的优先级更高的HH
        println(toString())
        println(this.toString())
        //指定调用 KK 的toString()方法
        println(this@KK.toString())
    }

    fun test() {
        val hh = HH()
        hh.out()
    }
}

/**
 运行结果:
 cn.yan.test.HH@19469ea2
 cn.yan.test.HH@19469ea2
 cn.yan.test.KK@13221655
 */
fun testRun() {
    val hh = HH()
    //编译错误 Error:(429, 8) Kotlin: Unresolved reference: out
    //hh.out() //因为没有定义在顶层,所以只能在申明的范围内使用
    val kk = KK()
    kk.test()
}

通过上面一系列的扩展案例,我们可以发现,kotlin 可以很好的解决 java 的扩展辅助类(各种 Utils 封装定义)。

kotlin 数据类(data class)与解构

kotlin 的数据类(data class)本质其实就是 java 的 Bean 实体类,只不过在 kotlin 中数据类的定义有自己的特殊语法,且更加方便便捷。案例如下:

//数据类
data class Data(var name: String, var age: Int)
//普通类
class Data1(var name: String, var age: Int)

/**
 运行结果:
 Data(name=name, age=12)
 cn.yan.test.Data1@19469ea2
 */
fun testRun() {
    val data = Data("name", 12)
    println(data)
    val data1 = Data1("name1", 13)
    println(data1)
}

kotlin 中定义一个数据类需要满足如下要求:

  • 主构造方法至少要有一个参数。
  • 所有的主构造方法参数都需要被标记为 var 或者 val。
  • 数据类不能是抽象的、open的、sealed的、inner的。

对于数据类来说 kotlin 编译器会为我们做如下额外的事情:

  • 自动重写了 equals 和 hashCode 方法。
  • 自动固定格式重写了 toString 方法。
  • 自动生成针对属性的 componentN 方法,并且是按照属性的声明顺序来生成的。

kotlin 数据类的反编译如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

yandeMacBook-Pro:test yan$ javap Data.class
Compiled from "Test2.kt"
//可以看到数据类 kotlin 为我们生成了很多额外的方法
public final class cn.yan.test.Data {
  public final java.lang.String getName();
  public final void setName(java.lang.String);
  public final int getAge();
  public final void setAge(int);
  public cn.yan.test.Data(java.lang.String, int);
  public final java.lang.String component1();
  public final int component2();
  public final cn.yan.test.Data copy(java.lang.String, int);
  public static cn.yan.test.Data copy$default(cn.yan.test.Data, java.lang.String, int, int, java.lang.Object);
  public java.lang.String toString();
  public int hashCode();
  public boolean equals(java.lang.Object);
}

kotlin 非数据类反编译如下:

yandeMacBook-Pro:test yan$ javap Data1.class
Compiled from "Test2.kt"
//可以看到非数据类 kotlin 为我们只生成了我们声明的一些属性方法和方法
public final class cn.yan.test.Data1 {
  public final java.lang.String getName();
  public final void setName(java.lang.String);
  public final int getAge();
  public final void setAge(int);
  public cn.yan.test.Data1(java.lang.String, int);
}

对于 kotlin 的数据类成员的继承我们要额外注意以下几个要点:

  • 如果数据类中显式定义的 equals、hashCode、toString 方法,或者在数据类的父类中将这些方法声明成了 final,那么这些方法将不再自动生成,转而使用已有的方法。
  • 如果父类拥有了 componentN 方法,并且这些方法是 open 的,以及返回兼容的类型,则编译器会在数据类中生成对应的 componentN 方法并且重写父类的这些方法;如果父类的这些方法由于不兼容的签名或者被定义为 final,那么编译器会报错。
  • 在数据类中显式提供 componentN 方法以及 copy 方法实现是不允许的。

如下是数据类的使用案例:

data class Data(var name: String, var age: Int)

/**
 运行结果:
 Data(name=name, age=12)
 Data(name=name, age=12)
 Data(name=name, age=30)
 a=name, b=30
 */
fun testRun() {
    val data = Data("name", 12)
    println(data)
    //复制并修改其中一个属性值
    val data1 = data.copy(age = 30) //数据类自动生成的特殊方法 copy
    println(data)
    println(data1)
    val (a, b) = data1 //解构赋值,利用 componentN 特性
    println "a=$a, b=$b"
}

kotlin 的数据类可以被解构声明使用,在 kotlin 数据类的主构造方法中有多少个参数,就会依次生成对应多个的 componentN 方法,这些方法返回对应顺序字段的值。componentN 方法是用来解构声明的。

不过这里有个坑大家要注意,由于解构是有顺序要求的,所以对于 kotlin 的数据类属性迭代新增请务必尽量在最后追加属性,不要在前面或者中间插新属性,不然很容易打乱顺序,导致解构调用方错误出现问题。

如下是解构赋值案例:

data class Data(var name: String, var age: Int)
class Data1(var name: String, var age: Int)

/**
 编译运行
 */
fun testRun() {
    val data = Data("name", 12)
    val data1 = Data1("name1", 13)
    val (na, ag) = data
    println("name=$na, age=$ag")    //name=name, age=12
    //非 data class 默认不会生成 componentN 方法
//编译报错:Error:(435, 22) Kotlin: Destructuring declaration initializer of type Data1 must have a 'component1()' function
//    val (na1, ag1) = data1
//    println("name1=$na1, age1=$ag1")
}

密封类(scaled class)

kotlin 的密封类类似 Java 的枚举类,但是又不一样,kotlin 也有自己的枚举类,密封类相对 java 来说是一种新的东西。密封类描述的是一种类的受限制层次关系,父子关系。密封类可以有子类,且密封类的子类可以创建多个实例。密封类的直接子类必须要和密封类定义在同一个文件中,间接子类没有这个限制。密封类是抽象的,不允许被实例化,密封类不允许有私有构造方法。

密封类在 when 表达式中用的比较多,默认 when 表达式必须提供 elase 分支,但是使用密封类(因为明确只有两种子类)就不用提供 else。使用案例如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

sealed class Option
class AddOption: Option()
class SubOption: Option()

fun option(num1: Int, num2: Int, opt: Option) = when (opt) {
    is AddOption -> num1 + num2
    is SubOption -> num1 - num2
    //可以省略 else,因为 'when' is exhaustive so 'else' is redundant here
    //else -> num1 * num2
}

/**
 运行结果:
 3
 -1
 */
fun testRun() {
    println(option(1, 2, AddOption()))
    println(option(1, 2, SubOption()))
}

下面是一个案例密封类在 when 表达式中没有群举完全场景的案例:

sealed class Option
class AddOption: Option()
class SubOption: Option()
class MaxOption: Option()

//无法编译通过,因为 when 没有群举完密封类的子类个数 'when' expression must be exhaustive, add necessary 'is MaxOption' branch or 'else' branch instead
fun option(num1: Int, num2: Int, opt: Option) = when (opt) {
    is AddOption -> num1 + num2
    is SubOption -> num1 - num2
}

嵌套类与内部类

kotlin 的嵌套类对应于 java 的静态内部类,java 中有 static 关键字,但是 kotlin 没有。只要在一个类的内部定义一个类就是 kotlin 的嵌套类。

kotlin 的内部类对应于 java 中的非静态内部类,使用 inner 关键字修饰且在一个类的内部定义的类就是 kotlin 的内部类。

无论内部类还是外部类都可以添加修饰符,譬如 private 等。如下是嵌套类与内部类使用案例演示:

class OutClass {
    private val name: String = "out name"

    //定义嵌套类
    class NestedClass {
        fun method() = "nested method"
    }
}

class OutClass2 {
    private val name: String = "out name"

    //定义内部类 必须添加 inner 关键字
    inner class InnerClass {
        //内部类访问外部类私有属性,如果内部类自己也有同名 name 属性,则可以 this.name 访问自己的成员属性
        fun method() = "inner method ${this@OutClass2.name}"
    }
}

/**
 输出结果:
 nested method
 inner method out name
 */
fun testRun() {
    //嵌套类的实例化及调用
    //访问嵌套类不需要先有外部类实例,有点像 java 的静态内部类
    println(OutClass.NestedClass().method())

    //内部类的实例化及调用
    //访问内部类必须先有外部类实例才能访问,此时外部类实例为OutClass2()
    println(OutClass2().InnerClass().method())
}

kotlin 也有局部嵌套类,但是没有局部内部类,如下:

class OutClass {
    private val name: String = "out name"

    fun get(): String {
        //局部嵌套类
        class LocalNestedClass {
            val name = "666"
        }
        return LocalNestedClass().name //666
    }

//没有局部内部类
//    fun get1(): String {
//        //编译错误 因为 Modifier 'inner' is not applicable to 'local class'
//        inner class LocalInnerClass {
//            val test = "666"
//        }
//        return LocalInnerClass().test
//    }
}

kotlin 对象表达式与对象声明

java 中匿名内部类在很多场景下非常有用,使用的也很多,而 kotlin 的对象表达式就是匿名内部类实例。java 的匿名内部类需要满足如下几个条件:

  • 匿名内部类没有名字;
  • 匿名内部类一定是继承了一个父类或者实现了某个接口;
  • java 运行时会将该匿名内部类当作它所实现的接口或继承的父类来看待;

与之对应,kotlin 的对象表达式满足条件也与 java 类似,所以其语法如下:

object [: 若干个父类型,中间用,隔开] {}

如下是各种 kotlin 对象表达式的使用案例:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

interface InterfaceBase {
    fun print(i: Int)
}

abstract class AbsInterfaceBase {
    abstract val age: Int

    abstract fun printAbs()
}

/**
 * 使用样例
 */
fun testRun() {
    //继承接口的对象表达式
    var obj = object: InterfaceBase {
        override fun print(i: Int) {
            println("InterfaceBase i=$i")
        }
    }
    obj.print(10)

    //没有任何继承的对象表达式
    val obj2 = object {
        fun test() {
            println("obj2 test")
        }
    }
    obj2.test()

    //继承抽象类的对象表达式
    val obj3 = object: AbsInterfaceBase() {
        override val age: Int
            get() = 12

        override fun printAbs() {
            println("abs age=$age")
        }
    }

    //同时继承抽象类、接口的对象表达式
    val obj4 = object: InterfaceBase, AbsInterfaceBase() {
        override val age: Int
            get() = 12

        override fun printAbs() {
            println("abs age=$age")
        }

        override fun print(i: Int) {
            println("interface age=$age, i=$i")
        }
    }
}

kotlin 对象表达式(匿名对象)只能在局部变量范围内或者是被 private 修饰的成员变量范围内才能被识别出其真正的类型。如果将对象表达式(匿名对象)当作一个非 private 方法的返回类型或者是非 private 的类型,那么方法或者属性的真正类型是该匿名类型的父类型,如果没有声明任何父类型,那么其类型就是 Any,在这种情况下,匿名对象中所声明的任何成员都是无法访问的。上个例子就是局部范围的匿名对象,下面是成员变量案例:

class TClass {
    //有private修饰符
    private var obj = object {
        fun out() {
            println("obj out method")
        }
    }

    fun test() {
        obj.out()   //obj out method
    }
}

class TClass1 {
    //无private修饰符
    var obj = object {
        fun out() {
            println("obj out method")
        }
    }

    fun test() {
        //编译报错 Unresolved reference: out
        //obj.out()
    }
}

java 匿名内部类只能通过 final 访问外部类的成员,且不能修改外部类成员。而 kotlin 的对象表达式没有这个限制,可以直接访问到或者修改外层对象的成员。如下案例:

/**
运行结果
 click index is 1
 click index is 2
 */
fun testRun() {
    var index: Int = 1
    val obj = object {
        fun click() {
            println("click index is $index") //访问外部成员
            ++index //修改外部成员
        }
    }
    obj.click()
    obj.click()
}

如果对象是 java 函数式接口的一个实例(即只拥有一个抽象方法的接口),则可以通过 lambada 表达式来调用,其中 lambada 前面加上接口的类型。

特别注意:如果对象是函数式 Java 接⼝(即具有单个抽象⽅法的 Java 接⼝)的实例,你可以使⽤带接⼝类型前缀的 lambda 表达式创建它。在 kotlin 1.4 版本之前一定记得是说 Java 函数式接口,不是 kotlin 中定义的函数式接口。kotlin 最新版本已经不存在这个限制了,kotlin 中定义的函数式接口也可以啦。

如下是演示案例:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

class View {
    private var listener: OnClickListener? = null

    fun setOnClickListener(l: OnClickListener) {
        this.listener = l
    }

    //kotlin 定义的函数式接口
    interface OnClickListener {
        fun click(view: View)
    }
}

/**
 * 使用案例
 */
fun testRun() {
    val view = View()
    //匿名对象方式
    view.setOnClickListener(object: View.OnClickListener {
        override fun click(view: View) {
            println("view is ${view.javaClass}")
        }
    })

    //编译报错 Interface OnClickListener does not have constructors
    //view.setOnClickListener(View.OnClickListener {
    //})

    //JButton 和 ActionListener 是 java 的接口
    val button = JButton()
    //匿名对象方式
    button.addActionListener(object: ActionListener {
        override fun actionPerformed(e: ActionEvent?) {
            println("666")
        }
    })
    //lambada表达式方式
    button.addActionListener(ActionListener {
        println("777")
    })
    //lambada表达式方式简写1
    button.addActionListener({
        println("888")
    })
    //lambada表达式方式简写2(kotlin函数调用特性)
    button.addActionListener {
        println("999")
    }
    //lambada表达式方式用参数
    button.addActionListener { e ->
        println("action $e")
    }
}

对象表达式与对象声明、伴生对象的区别:

  • 对象表达式是立刻初始化或者执行的。
  • 对象声明是延迟初始化的,在首次访问的时候进行。
  • 伴生对象是在其所对应的类被加载时初始化,对应 java 的静态初始化。

kotlin 枚举

kotlin 枚举定义基本与 java 完全一致,没啥特殊要注意的,所以直接给出案例如下:

//【工匠若水 加微信 yanbo373131686 联系我,关注微信公众号:码农每日一题  未经允许严禁转载 https://blog.csdn.net/yanbober】

enum class Reason {
    ERROR, WARNING, INFO
}

enum class Reason1(val msg: String) {
    ERROR("this error"),
    WARNING("this waring"),
    INFO("this info")
}

enum class Reason3(val msg: String) {
    ERROR("this error"),
    WARNING("this waring"),
    INFO("this info");

    fun getMessage(): String {
        return this.msg
    }
}

enum class Reason4() {
    ERROR {
        override fun getMessage(): String = "this error"
    },
    WARNING {
        override fun getMessage(): String = "this waring"
    },
    INFO {
        override fun getMessage(): String = "this info"
    };

    abstract fun getMessage(): String
}

/**
 调用结果
 this error
 this waring
 this info
 */
fun testRun() {
    val reasons = Reason3.values()
    reasons.forEach { println(it.getMessage()) }
}

到此 kotlin 对象相关的全方位差异总结就结束了。