阅读 180

三、kotlin的类和对象(一)

类 ★

构造函数 ★

★ 在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

由于主构造函数的特性, 他会捕捉类内属性到他的作用域内初始化, 但他发现我们没有初始化(我们调用的是主构造函数, 次构造函数无所谓初始化了没有), 所以他会报错, 而且他也必须报错, 因为主构造函数必须可以 构造出一个对象, 但两个属性没有初始化, kotlin没有默认参数, 所以报错了

那我们需要次构造函数的初始化又有什么办法呢? 简单, 删掉主构造函数

image.png

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

image.png

事情解决了!!!

当然我们可以用 主构造函数 + init 代码块来包装

image.png

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

我的结论: 非必要, 最好别用主构造, 他带来的麻烦和便利相比, 我宁愿不要他, 况且 IDEA 有快捷键, 只要你写了属性, 使用快捷键照样给出符合我们要求的构造函数

上面言论仅代表我个人的使用习惯

主构造函数和初始化语句块(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")
    }
复制代码
文章分类
Android
文章标签