三、kotlin的类和对象(一)

534 阅读9分钟

类 ★

构造函数 ★

★ 在kotlin中构造函数有主构造函数, 初始化代码块(init)和次构造函数

主构造函数 ★

class Person constructor(val name: String) {}

换成 java 源码 类似于:

public final class Person {
    private final String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    public final String getName() {
        return this.name;
    }
    
    // public final void setName(String name) {
    //     this.name = name;
    // } 
}

什么是主构造函数?

主构造函数是写在类名之后带括号的代码, 那就是主构造函数

主构造函数只有一个

主构造函数最先执行

为什么需要主构造函数?

kotlin设计主构造函数的可能是简化代码吧

class Foo { 
    val bar: Bar
    constructor(barValue: Bar) {
        bar = barValue
    }
}

这样就完成了

class Foo(val bar: Bar)

原型是这样: class Foo constructor(val bar: Bar) 只不过constructor 关键字在没有注解, 类似private 这样的访问修饰符可以省略

class Person private /* @Inject */ constructor(name: String) {
   val name: String = name.uppercase()
}

主构造函数带来的问题

  1. 主函构造函数增加了新手入门的难度( 很奇葩的设计
  2. 添加了主构造函数, 还需要考虑构造函数的顺序 ( 奇葩
  3. 主构造函数内部不能有别的操作, 只有赋值操作, 如果还有别的操作还需要使用 init 代码块, 在 init 代码块中初始化 (超级奇葩
  4. 如果类的属性增多, 你会发现一部分属性在主构造函数的小括号内, 一部分属性在类的作用域内, 阅读性变低 (更加奇葩
  5. 看截图

image.png

主构造函数必须最先执行, 次构造函数次之, 所以在主构造函数中无法初始化 age1age2 无法被初始化, 所以报错

解决方法是:

  1. age1age2 放入主构造函数的函数参数列表中
open class A(
    var name: String
    val age1: Int
    val age2: Int
) {
    constructor(_age: Int) : this("name", _age, _age) {
    }
}
  1. init 代码块
open class A(var name: String) {
    val age1: Int
    val age2: Int
    init {
        // 这里的 0 可以改成在主构造函数传入参数 比如: open class A(var name: String, _age: Int) , 这样次构造函数的 this("name") 就需要更改了 this("name", _age), 次构造函数后面的函数代码块就不需要了
        this.age1 = 0
        this.age2 = 0
    }
    constructor(_age: Int) : this("name") {
        this.age1 = _age
        this.age2 = _age
    }
}

有新手会问为什么不用 lateinit, 那是因为 lateinit 只能用于 var 且非基本类型(Int, Double这种基础类型)上

  1. 删掉主构造函数

image.png

写好字段, 使用快捷键, 就可以创建上面的次构造函数

image.png

事情解决了!!!

当然我们也可以灵活运用 主构造函数 + init代码块 + 参数的默认值

image.png

让你选你会选哪一种???

我的结论: 怎么简单怎么来

主构造函数和初始化语句块(init)

  1. 是什么?
class Person(_nickName: String) {
    val nickName: String
    // 这就是 init 初始化代码块
    init {
        this.nickName = "${_nickName.lenght} - ${_nickName}"
    }
}

注意1, constructor 修饰符省略掉了, 有前提:

  1. 没有注解.
  2. 没有可见修饰符.

注意2: 主构造函数上 Person(val nickName: String)Person(_nickName: String) 的区别在于 带 val/var 的将变成nickName属性, 不带的变成_nickName构造函数参数

  1. 为什么需要初始化代码块?

kotlin主构造函数除了 this.nickName = nickName 这些赋值操作外, 没有任何的操作, 所以需要初始化代码块进行其他操作 this.nickName = "${_nickName.lenght} - ${_nickName}", 所以初始化代码块诞生了

初始化代码块属于主构造函数体的代码之一, 和 主构造函数类作用域内属性 属于同一个作用域

将其换成 java 源码就知道了:

class Person {
    private String nickName;
    
    Person(String _nickName) {
        // 主函数自己的代码 
        // balabala.....
        // init 代码块内部的代码 or 主函数之外的字段初始化代码 (按照定义先后顺序)
        this.nickName = "${_nickName.lenght} - ${_nickName} // 这行不是 java 代码
    }
}

构造方法参数默认值

class Person(val nickName: String , var isSubscribed: Boolean = false)

★ 如果全部是 默认值 会生成一个无参数主构造函数

class Person(val nickName: String = "", var isSubscribed: Boolean = false)

子类初始化父类字段 ★

★ 子类有责任初始化 父类字段

open class User(val nickName: String) {}
class FacebookUser(nickName: String) : User(nickName) {}

这非常的重要, 子类有责任将值给父类初始化, 父类未初始化的属性必须要子类来初始化(想要子类对象的话)

继承和实现怎么看?

interface View {}
open class Button : View {}
open class RadioButton : Button() {}

引号继承和实现区别一目了然, 实现直接写上 View , 继承则是调用 父类构造函数 Button() , 一个 没有 () 一个有 ()

如果不声明任何构造函数, 它会生成一个无参数构造函数

open class Button

定义 private 构造函数

class Person private constructor(val nickName: String) {}

这种类可以使用伴生对象构建并使用, 伴生对象就是类的对象, 而该对象的函数未必是静态的哦, 以后会学到

当然你还可以写个次构造函数, 在末尾(c++叫初始化成员列表的位置)调用主构造函数

次构造函数 ★

class Person(val name: String) {
    var age: Int = 0
    
    // 这个就是次构造函数
    constructor(name: String, age: Int) : this(name) {
        this.age = age
    }
}

构造函数优先级 ★

主构造函数优先级 高于 init初始化代码块和主构造函数外字段 高于 次构造函数

class Person(var name: String = "1") {
   var age: Int
   
   init {
      if (this.name == "1") {
         println("主构造函数第一时间初调用了")
      }
      this.name = "3"
      println("init 代码块初调用了")
      this.age = 2
   }
   
   constructor(age: Int) : this() {
      println("次构造函数调用了")
      this.age = age
   }
   
   override fun toString(): String {
      return "Person(name='$name', age=$age)"
   }
   
}

fun main() {
   val person = Person(age = 4)
   println(person)
}

在主构造函数中定义变量(注意不是属性是作为参数的变量), 则可以使用 _ 的方式在区别, 比如: _name 或者 _nickName 等等

注意:

init 代码块最后都会成为主构造函数的函数体内部的代码

class Person(var name: String) {
    // init 和 下面 age 的初始化顺序优先级按照定义顺序判断优先级
    init {
        if (this.name == "haha") {
            this.name = "zhazha"
        }
    }
    val age: Int = 1
}

将会变成

public final class Person {
    private final String name;
    private final int age;
    
    public Person() {
        this.name = name;
        if (Intrinsics.areEqual(this.name, "haha")) {
            this.name = "zhazha"
        }
        this.age = 1
    }
    
    // get/set ...
}

注意5: 只有主构造函数可以在小括号内声明成员属性, 次构造函数不允许

kotlin 初始化带来 bug 以及解决方案

private class Demo01 {
    val name: String
    private fun first() = name[0]
    init {
        // first 还没初始化呢, 直接就调用了? 这时候只能 报错 NullPointerException
        println(first())
        name = "zhazha"
    }
}

fun main() {
    val demo01 = Demo01()
}
class Demo02(_name: String) {
    val playerName: String = initPlayerName()
    val name: String = _name
    private fun initPlayerName(): String = name
}

fun main() {
    val demo02 = Demo02("zhazha")
    println(demo02.playerName) // 最终输出 null
}

解决方案任何属性都需要先初始化再使用

接口★

Kotlin 的接⼝可以既包含抽象⽅法的声明也包含实现。与抽象类不同的是,接⼝⽆法保存状态, 所以没有字段用于存储状态。但它可以有属性但必须声明为抽象或提供访问器实现(说白了就是get/set函数)

注意, 不要把 java 的字段带过来, java的字段绝对没有 get/set函数 在 kotlin 中所有的属性绝对有 get/set 中的一个, 但未必有 字段(field)

interface MyInterface {
   var name: String
   
   val age:Int
   
   fun bar()
   
   fun foo() {
      println(this::javaClass)
   }
}

在接口中抽象方法默认 public abstract, 而默认方法在接口中的实现比较复杂, 这涉及到 kotlin 早期对标的是jdk1.6, 那时的类不允许有默认方法, 所以kotlin的实现方式比较有意思, 下面是 java 源码

public interface MyInterface {
   @NotNull
   String getName();

   void setName(@NotNull String var1);

   int getAge();

   void bar();

   void foo();

   @Metadata(
      mv = {1, 5, 1},
      k = 3
   )
   public static final class DefaultImpls {
      public static void foo(@NotNull MyInterface $this) {
         String var1 = "zhazha";
         boolean var2 = false;
         System.out.println(var1);
      }
   }
}

kotlin编译器生成了个 DefaultImpls 内部静态类, 然后以静态的方式写了个了和接口中的 foo 同名函数, 参数传递了个 this, 至此我们能想到要如何调用则函数了吧?

this , 但 接口 不能直接 new 出一个 this , 只能以实现的方式 new 一个匿名类对象, 而 这个 this 参数就是 匿名类对象本身, 所以使用的话应该是这样:

这里涉及 new 接口, kotlin 中 new 接口的方式 使用的是 object, 后续会说, 现在只要知道怎么 new 就行

fun main() {
    val obj = object: MyInterface {
        override var name: String
            get() = TODO("xxxxx")
            set(value) {}
        override val age: Int
            get() = TODO("xxxxx")
        override fun bar() {
            // to do
        }
    }
    // 默认函数按照需要重写
    obj.foo()
}

obj.foo() 这段代码反编译成 java 的话, 估计将会是这样:

// obj.foo()
MyInterface.DefaultImpls.foo(obj)

接口中可以有接口也可以有默认方法还可以有属性(不带字段的属性)

interface Named {
   val name: String
   
   interface Name {
      val names: String
   }
}

interface Person : Named {
   val firstName: String
   val lastName: String
   override val name: String
      get() = "$firstName $lastName"
}

class NameClass(override val names: String) : Named.Name {
}

data class Employee(override val firstName: String, override val lastName: String) : Person {
   val position: Pair<Double, Double> = Pair(0.0, 0.0)
}

fun main(args: Array<String>) {
   val employee = Employee("zzz", "ddd")
   println(employee.name)
}

而接口中不允许有记录数据的作用域(字段), 所以在接口中定义的字段被kotlin处理成 set/get 方法

public interface MyInterface {
    @NotNull
    public String getName();

    public void setName(@NotNull String var1);

    public int getAge();
}

接⼝继承

interface Named {
   val name: String
}

interface Person : Named {
   val firstName: String
   val lastName: String
   override val name: String
      get() = "$firstName $lastName"
}

data class Employee(override val firstName: String, override val lastName: String) :Person {
   val position: Pair<Double, Double> = Pair(0.0, 0.0)
}

接口的多继承

接口和java中的接口一样, 接口之间可以多继承, 实体类也可以多实现接口

interface A {}
interface B {}
interface C : A, B {}
class D : A, B {}

如果接口 A 和 接口 B 使用有一个相同的方法, fun Hello(): Unit 被 D 发现也没事, 实现只有一个

interface A {
   fun hello()
}
interface B {
   fun hello()
}
class D : A, B {
   override fun hello() {
   }
}

接⼝中的属性

接口不能有任何字段, 所以无法存储数据, 但可以借助接口属性函数的 get 返回预先设定好的数据

private interface User {
    val nickName: String // 只有 getNickName 函数, 接口不允许有字段
}

// 主构造属性会被初始化, 所以需要添加字段和 get 函数
class PrivateUser(override val nickName: String) : User {
}

class SubscribingUser(val email: String) : User {
    override val nickName: String
        // 重写了 get 访问器, 则不需要字段
        get() = email.substringBefore('@')
}

class FaceBookUser(val accountId: Int): User {
    // 初始化了 nickName, 生成 get访问器和 nickName字段
    override val nickName = "name: $accountId"
}

子类重写接口属性, 根据子类具体的情况判断是否定义字段, 如果子类重写字段的 get/set 函数没有涉及 field (或者说get/set函数不依赖重写的字段本身)则不会直接定义一个字段, 只有 get/set 函数, 例如:

class SubscribingUser(var email: String) : User {
    override var nickName: String
        get() = email.substringBefore('@')
        set(value) {
            this.email = value.uppercase()
        }
}

上面这段代码就不会产生字段, 直接生成 get/set 函数

public final class SubscribingUser implements User {
   // 字段只有这一个
   private String email;
   public String getNickName() {}
   public void setNickName(@NotNull String value) {}
   public final String getEmail() {}
   public final void setEmail(@NotNull String var1) {}
   public SubscribingUser(@NotNull String email) {}
}

接口属性未必一定需要重写

interface User {
    val email: String
    val nickName: String
        get() = email.substringBefore('@')
}

上面第一个属性 email 子类必须要重写, 但下面一个 nickName 在子类可以被继承

函数式接口

单一抽象方法的接口, 叫函数式接口或者叫SAM接口

fun interface KRunnable {
    fun invoke()
}

注意前面的 fun 用来区分 普通接口 和 函数式接口

在 java 中函数式接口需要写上 @FunctionInterface 注解, 来标注, 但不是强迫性的, 而 kotlin 中的函数式接口必须在 interface 之前加上 fun 才能代表函数式接口

  1. java的接口只要只有一个未实现的抽象方法, 都可以被 kotlin 编译器识别为 函数式接口(有多少默认函数无所谓)

  2. 函数式接口可以有函数式接口构造函数

val kRunnable = KRunnable { println("函数式接口的特点") }

特点就是不需要 new , 直接写就行

非函数式接口不能这样:

interface IRunnable {
   fun invoke()
}

image.png

调用函数式接口的方法

fun interface IntPredicate {
   fun accept(i: Int): Boolean
}

fun isInt(i: Int, funcType: IntPredicate): Boolean = funcType.accept(i)

fun main(args: Array<String>) {
   val a = 19
   println(isInt(a){
      it is Int
   })
}

属性★

声明属性

class Address {
   var name: String =
      "Holmes, Sherlock"
   var street: String = "Baker"
   var city: String = "London"
   var state: String? = null
   var zip: String = "123456"
}

在我看来kotlin类内的字段其实不是简简单单的字段, 而是 字段 + get/set方法, 就是属性

(1) var 和 val , var 定义一个可读可写的属性, val 定义一个只读的属性

this.name 就相当调用了 getName 函数, this.name = "嘿嘿", 就相当于调用了 setName("嘿嘿")

var allByDefault: Int? // 错误:需要显式初始化器,隐含默认 getter 和 setter
var initialized = 1 // 类型 Int、默认 getter 和 setter

val simple: Int? // 类型 Int、默认 getter、必须在构造函数中初始化
val inferredType = 1 // 类型 Int 、默认 getter

(2) 自定义访问器

class Address {
   // 可以在 init 初始化字段初始化, 也可以直接在 name 中初始化
// val name: String = 0
   val name: String
      get() = field
   
   var age: Int = 0
      get() = field
      set(value) {
         field = value
      }
   init {
      this.name = ""
   }
   var isEmpty: Boolean = age == 0
}

var setterVisibility: String = "abc"
    private set // 此 setter 是私有的并且有默认实现

var setterWithAnnotation: Any? = null
    @Inject set // ⽤ Inject 注解此 setter

幕后字段

无限递归问题:

class Teacher {
   var name: String
      get() = this.name
      set(value) {
         this.name = value
      }
}

(1) 上面这段代码的 this.name 会被解析成 this.getName(), 而调用该方法的位置又是在 getName 中, 所以是无限递归

(2) this.name = value 这里会被解析成 this.setName(value), 而调用位置又是在 setName函数中, 所以还是无限递归

这时候我们就需要 field 幕后字段

class Teacher {
   var name: String = ""
      get() = field
      set(value) {
         field = value
      }
}

幕后属性

下面这段代码实现了一个延迟属性功能

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if(_table == null) {
            _table = HashMap()
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

个人认为 幕后属性 可以使用在 data class 中配合使用