《Kotlin核心编程》笔记:面向对象

发布时间:2023年12月19日

kotlin 中的类

// Kotlin中的一个类
class Bird {
    val weight: Double = 500.0
    val color: String = "blue"
    val age: Int = 1

    fun fly() { } // 全局可见
}

把上述代码反编译成Java的版本,然后分析它们具体的差异:

public final class Bird {
    private final double weight = 500.0D;

    @NotNull
    private final String color = "blue";

    private final int age = 1;

    @NotNull
    public final double getWeight() {
        return this.weight;
    }

    @NotNull
    public final String getColor() {
        return this.color;
    }

    public final int getAge() {
        return this.age;
    }

    public final void fly() {
 
    }
}

虽然Kotlin中类声明的语法非常近似Java,但也存在很多不同:

  • 1)不可变属性成员。Kotlin用val在类中声明引用不可变的属性成员,是利用Java中的final修饰符来实现的,使用var声明的属性则反之引用可变。
  • 2)属性默认值。因为Java的属性都有默认值,所以在声明时我们不需要指定默认值。而在Kotlin中,除非显式地声明延迟初始化,不然就必须指定默认值
  • 3)不同的可访问修饰符。Kotlin类中的成员默认是全局可见,而Java的默认可见域是包作用域,因此在Java版本中,我们必须采用public修饰才能达相同的效果。

可带有属性和默认方法的接口

众所周知,Java 8 引入了一个新特性——接口方法支持默认实现:

// Java 8 中的接口
public interface Flyer {
    String kind();

    default void fly() {
        System.out.println("I can fly");
    }
}

这使得我们在向接口中新增方法时,之前继承过该接口的类则可以不需要实现这个新方法。

接下来再来看看在Kotlin中如何声明?个接口:

// Kotlin 中的接口
interface Flyer {
    val speed: Int
    fun kind()
    fun fly() {
        println("I can fly")
    }
}

与 java 类似,它还支持抽象属性,但是抽象属性不能有默认值,而在 Java 中接口中是可以有默认值的(Java 接口声明的成员变量默认是public static final的),这一点与Java不同。

假如在kotlin给它强行设置默认值,则编译器会报错:

interface Flyer {
	val height = 1000 //error Property initializers are not allowed in interfaces
}

Kotlin 提供了另外?种方式来实现这种效果,通过覆写成员的get()方法:

interface Flyer {
	val height
		get() = 1000
}

若没有指定默认行为,则在实现该接口的类中必须对该属性进行初始化。

更简洁地创建对象

Kotlin 中并没有new关键字,可以直接调用构造函数创建对象:

val bird = Bird() 

在Java中构造方法重载存在两个缺点:

  • 如果要支持任意参数组合来创建对象,那么需要实现的构造方法将会非常多。
  • 每个构造方法中的代码会存在冗余

构造方法默认参数

要解决构造方法过多的问题,在 Kotlin 中你只需要给构造方法的参数指定默认值,从而避免不必要的方法重载。

class Bird(val weight: Double = 0.0, val age: Int = 0, val color: String = "blue")
// 可以省略 {}

调用:

val bird1 = Bird(color = "black")
val bird2 = Bird(weight = 1000.00, color = "black") 

需要注意的是,由于参数默认值的存在,我们在创建类对象时,最好指定参数的名称,否则必须按照实际参数的顺序进行赋值。不然容易出现参数类型匹配不上的错误警告。

init 语句块

Kotlin引入了?种叫作init语句块的语法,它属于上述构造方法的?部分,两者在表现形式上却是分离的。

当构造方法的参数没有valvar修饰的时候,可以在init语句块被直接调用。

class Bird(
    weight: Double = 0.00, // 参数名前没有 val 
    age: Int = 0,
    color: String = "blue"
) {
    val weight: Double
    val age: Int
    val color: String

    init {
        this.weight = weight // 构造方法参数可以在 init 语句块被调用 
        this.age = age
        this.color = color
    }
}

如果我们需要在初始化时进行其他的额外操作,那么我们就可以使用init语句块来执行。比如:

class Bird(weight: Double, age: Int, color: String) {
    init {
        println("do some other things")
        println("the weight is $weight")
    }
}

其实它们还可以用于初始化类内部的属性成员的情况:

class Bird(weight: Double = 0.00, age: Int = 0, color: String = "blue") {
    val weight: Double = weight // 在初始化属性成员时调用 weight
    val age: Int = age
    val color: String = color
}

除此之外,我们并不能在其他地方使用。以下是一个错误的用法:

class Bird(weight: Double, age: Int, color: String) {
    fun printWeight() {
        print(weight) // Unresolved reference: weight 
    }
}

事实上,构造方法可以拥有多个init块,它们会在对象被创建时按照类中从上到下的顺序先后执行。

看看以下代码的执行结果:

class Bird(weight: Double, age: Int, color: String) {
    val weight: Double
    val age: Int
    val color: String

    init {
        this.weight = weight
        println("The bird's weight is ${this.weight}.")
        this.age = age
        println("The bird's age is ${this.age}.")
    }

    init {
        this.color = color
        println("The bird's color is ${this.color}.")
    }
}

fun main(args: Array<String>) {
    val bird = Bird(1000.0, 2, "blue")
}

运行结果:

The bird's weight is 1000.0. 
The bird's age is 2.
The bird's color is bule. 

可以发现,多个init语句块有利于我们进一步对初始化的操作进行职能分离,这在复杂的业务开发(如Android)中显得特别有用。

注意:正常情况下,Kotlin规定类中的所有非抽象属性成员都必须在对象创建时被初始化值。

下面代码会运行报错:

class Bird(val weight: Double, val age: Int, val color: String) {
    val sex: String

    fun printSex() {
        this.sex = if (this.color == "yellow") "male" else "female"
        println(this.sex)
    }
}

fun main(args: Array<String>) {
    val bird = Bird(1000.0, 2, "blue")
    bird.printSex()
}

运行结果

Error:(2, 1) Property must be initialized or be abstract 
Error:(5, 8) Val cannot be reassigned

由于sex必须被初始化值,上述的printSex方法中,sex会被视为二次赋值,这对val声明的变量来说也是不允许的。

我们可以把sex变成用var声明并指定默认值,但是假如我们不想要默认值,可以声明为可空类型String?,这样默认值就是null

然而实际上也许我们又不想让sex具有可空性,而只是想稍后再进行赋值。

延迟初始化:by lazy 和 lateinit

更好的做法是让sex能够延迟初始化,即它可以不用在类对象初始化的时候就必须有值。

在Kotlin中,我们主要使用 lateinitby lazy 这两种语法来实现延迟初始化的效果。

class Bird(val weight: Double, val age: Int, val color: String) {
    val sex: String by lazy {
        if (color == "yellow") "male" else "female"
    }
}

fun main() {
    val bird = Bird(1000.0, 2, "blue")
    println(bird.sex)
}

总结 by lazy 语法的特点如下:

  • 该变量必须是引用不可变的,而不能通过var来声明。
  • 在被首次调用时,才会进行赋值操作。一旦被赋值,后续它将不能被更改

lazy的背后是接受一个 lambda 并返回一个Lazy<T>实例的函数,第一次访问该属性时,会执行lazy对应的 Lambda 表达式并记录结果,后续访问该属性时只是返回记录的结果。

另外系统会给 lazy 属性默认加上同步锁,也就是LazyThreadSafetyMode.SYNCHRONIZED,它在同一时刻只允许一个线程对lazy属性进行初始化,所以它是线程安全的。

但若你能确认该属性可以并行执行,没有线程安全问题,那么可以给lazy传递LazyThreadSafetyMode.PUBLICATION参数。你还可以给lazy传递LazyThreadSafetyMode.NONE参数,这将不会有任何线程方面的开销,当然也不会有任何线程安全的保证。比如:

val sex: String by lazy(LazyThreadSafetyMode.PUBLICATION) { // 并行模式 
    if (color == "yellow") "male" else "female"
}

val sex: String by lazy(LazyThreadSafetyMode.NONE) { // 不做任何线程保证也不会有任何线程开销
    if (color == "yellow") "male" else "female"
}

lazy不同,lateinit主要用于var声明的变量,然而它不能用于基本数据类型,如IntLong等,我们需要用Integer这种包装类作为替代。(但是可以用于Sring, String不是基本类型)

可以像下面这样解决前面的问题:

class Bird(val weight: Double, val age: Int, val color: String) {
    lateinit var sex: String // sex 可以延迟初始化

    fun printSex() {
        this.sex = if (this.color == "yellow") "male" else "female"
        println(this.sex)
    }
}

fun main(args: Array<String>) {
    val bird = Bird(1000.0, 2, "blue")
    bird.printSex()
}

运行结果:

female

如何让var声明的基本数据类型变量也具有延迟初始化的效果呢?

一种可参考的解决方案是通过Delegates.notNull<T>,这是利用 Kotlin 中委托的语法来实现的。

可以通过?个例子来认识这种神奇的效果:

var test by Delegates.notNull<Int>() 

fun doSomething() {
	test = 1
	println("test value is ${test}") 
	test = 2
}

主从构造方法

import org.joda.time.DateTime

class Bird(age: Int) {
    val age: Int

    init {
        this.age = age
    }

    constructor(birth: DateTime) : this(getAgeByBirth(birth)) {
		...
	}
}
  • 跟在类名后面定义的构造方法被称为主构造方法
  • 在内部通过constructor方法定义了一个新的构造方法,它被称为从构造方法
  • 每个类可最多存在一个主构造方法和多个从构造方法
  • 每个从构造方法由两部分组成。一部分是对其他构造方法的委托,另一部分是由花括号包裹的代码块执行顺序上会先执行委托的方法,然后执行自身代码块的逻辑

如果主构造方法存在 注解可见性修饰符, 也必须加上constructor关键字,如:

internal public Bird @inject constructor(age: Int) {...} 

通过this关键字来调用要委托的构造方法。如果一个类存在主构造方法,那么每个从构造方法都要直接或间接地委托给它。比如,可以把从构造方法 A 委托给从构造方法 B,再将从构造方法 B 委托给主构造方法。例如:

class Person(val name : String) {
    var age :Int = 0
    var sex: Boolean = false
    var uid: Int = 0
     
    constructor(age: Int, sex: Boolean) : this("tom") {
        this.age = age
        this.sex = sex
    }

    constructor(uid: Int, age: Int, sex: Boolean) : this(age, sex) {
        this.uid = uid
    }   
}

初始化顺序

当类中同时存在主次构造函数和 init 块时,按照如下顺序进行初始化:

  1. 主构造函数声明的属性
  2. init 块和类中声明的成员属性,按照出现的先后顺序执行
  3. 次构造函数里的属性赋值和函数调用

在这里插入图片描述

主从构造方法的一个很大的作用就是可以对某些第三方Java库中的类,进行更好地扩展自定义的构造方法。

如 Android 开发中典型的例子就是定制业务中特殊的 View 类:

class KotlinView : View {

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)

    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
        ...
    } 
}

不同的访问控制原则

限制修饰符

当你想要指定一个类、方法或属性的修改或者重写权限时,你就需要用到限制修饰符。

open class Bird {
    open fun fly() {
        println("I can fly.")
    }
}

class Penguin : Bird() {
    override fun fly() {
        println("I can't fly actually.")
    }
}

两个 Kotlin 相比 Java 不一样的语法特性:

  • Kotlin 中没有采用 Java 中的 extendsimplements 关键词,而是使用“ :” 来代替类的继承和接口实现;
  • 由于 Kotlin 中类和方法默认是不可被继承或重写的,所以必须加上 open 修饰符

类的默认修饰符:final

Kotlin 认为类默认开放继承并不是一个好的选择,这会导致很多设计上的问题。

所以在 Kotlin 中的类或方法默认是不允许被继承或重写的。

还是以 Bird 类为例:

class Bird {
	val weight: Double = 500.0 
	val color: String = "blue"
	val age: Int = 1
	fun fly() {}
}

现在我们把它编译后转换为 Java 代码:

public final class Bird {

    private final double weight = 500.0D;
    private final String color = "blue";
    private final int age = 1;

    public final double getWeight() {
        return this.weight;
    }

    public final String getColor() {
        return this.color;
    }

    public final int getAge() {
        return this.age;
    }

    public final void fly() {
         
    }
}

可以发现,转换后的 Java 代码中的类,方法及属性前面都加了一个 final 修饰符,由它修饰的内容将不允许被继承或修改。

我们经常使用的String类就是用final修饰的,它不可以被继承。

在 Java 中,类默认是可以被继承的,除非你主动加final修饰符。

而在 Kotlin 中恰好相反,默认是不可被继承的,除非你主动加可以继承的修饰符,那便是open

类默认 final 真的好吗

一些批评的声音来自Kotlin官方论坛,不少人诟病默认final的设计会给实际开发带来不便。具体表现在:

  • 与某些框架的实现存在冲突。如 Spring 会利用注解私自对类进行增强,由于 Kotlin 中的类默认不能被继承,这可能导致框架的某些原始功能出现问题。
  • 更多的麻烦还来自于对第三方 Kotlin 库进行扩展。就统计层面讨论,Kotlin 类库肯定会比 Java 类库更倾向于不开放一个类的继承,因为人总是偷懒的,Kotlin 默认 final 可能会阻挠我们对这些类库的类进行继承,然后扩展功能。

Kotlin 论坛甚至举行了一场关于类默认 final 的喜好投票,略超半数的人更倾向于把 open 当作默认情况。(参见 https://discuss.kotlinlang.org/t/classes-final-by-default/166

这些反对观点其实也很有道理。我们再基于 Kotlin 的自身定位和语言特性重新反思一下这些观点。

  • 1)Kotlin 当前是一门以 Android 平台为主的开发语言。在开发时,我们很少会频繁地继承?个类,默认 final 会让它变得更加安全。如果一个类默认 open 而在必要的时候忘记了标记 final,可能会带来麻烦。反之,如果一个默认 final 的类,在我们需要扩展它的时候,即使没有标记open,编译器也会提醒我们,这个就不存在问题。此外,Android 也不存在类似 Spring 因框架本身而产生的冲突。
  • 2)虽然 Kotlin 非常类似于 Java,然而它对一个类库扩展的手段要更加丰富。 典型的案例就是 Android 的 Kotlin 扩展库 android-ktx。Google 官方主要通过 Kotlin 中的扩展语法对 Android 标准库进行了扩展,而不是通过继承原始类的手段。这也揭示了一点,以往在 Java 中因为没有类似的扩展语法,往往采用继承去对扩展一个类库,某些场景不一定合理。相较而言,在 Kotlin 中由于这种增强的多态性支持,类默认为 final 也许可以督促我们思考更正确的扩展手段。

Kotlin 与 Java 的限制修饰符比较:

修饰符含义与 Java 比较
open允许被继承或重写相当于 Java 类与方法的默认情况
abstract抽象类或抽象方法与 Java 一致
final不允许被继承或重写(默认情况)与 Java 主动指定 final 的效果一致

总的来说,我们需要辩证地看待 Kotlin 中类默认 final 的原则,它让我们的程序变得更加安全,但也会在其他场合带来一定的不便。最后,关于限制修饰符,还有一个abstract。Kotlin 中的 abstract 和Java中的完全一样。

密封类

Kotlin 除了可以利用 final 来限制一个类的继承以外,还可以通过密封类的语法来限制一个类的继承。

Kotlin通过 sealed 关键字来修饰一个类为密封类,若要继承则需要将子类定义在同一个文件中,其他文件中的类将无法继承它。

sealed class Bird {
	open fun fly() = "I can fly" 
	class Eagle : Bird() 
}

但这种方式有它的局限性,即它不能被初始化,因为它背后是基于一个抽象类实现的。(Java)密封类的使用场景有限,它其实可以看成一种功能更强大的枚举,所以它在模式匹配中可以起到很大的作用。使用密封类的好处就是,当我们使用 when 表达式时不用去考虑非法的情况了,也就是可以省略else分支。

sealed class Day {
    class SUN : Day()
    class MON : Day()
    class TUE : Day()
    class WED : Day()
    class THU : Day()
    class FRI : Day()
    class SAT : Day()
}
fun schedule(day: Day): Unit = when (day) {
    is Day.SUN -> fishing()
    is Day.MON -> work()
    is Day.TUE -> study()
    is Day.WED -> library()
    is Day.THU -> writing()
    is Day.FRI -> appointment()
    is Day.SAT -> basketball()
}

注意:Kotlin 1.0 的时候,密封类的子类只能定义在父类结构体中,而 Kotlin 1.1 之后可以不用将子类定义在父类中了。

可见性修饰符

除了限制类修饰符之外,还有一种修饰符就是可见性修饰符。

若你想要指定类、方法及属性的可见性,那么就需要可见性修饰符。Kotlin 中的可见性修饰符也与 Java 中的很类似。但也有不一样的地方,主要有以下几点:

  • 1)默认修饰符不同,Kotlin 中是 public,而 Java 中是 default
  • 2)Kotlin 中有一个独特的修饰符 internal,表示模块内可见
  • 3)Kotlin 可以在一个文件内单独声明方法及常量,同样支持可见性修饰符。
  • 4)Java 中除了内部类可以用 private 修饰以外,其他类都不允许 private 修饰,而 Kotlin 可以。
  • 5)Kotlin 和 Java 中的 protected 的访问范围不同,Java 中是包、类及子类可访问,而 Kotlin 只允许类及子类

Kotlin 中的 internal 修饰符,和 default 有点像但也有所区别。internal 在 Kotlin 中的作用域可以被称作“ 模块内访问”。

总的来说,一个模块可以看作一起编译的 Kotlin 文件组成的集合。那么,Kotlin 中为什么要诞生这么一种新的修饰符呢?Java 的包内访问不好吗?

Java 的包内访问中确实存在一些问题。举个例子,假如你在 Java 项目中定义了一个类,使用了默认修饰符,那么现在这个类是包私有,其他地方将无法访问它。然后,你把它打包成一个类库,并提供给其他项目使用,这时候如果有个开发者想使用这个类,除了 copy 源代码以外,还有一个方式就是在程序中创建一个与该类相同名字的包,那么这个包下面的其他类就可以直接使用我们前面的定义的类

而 Kotlin 默认并没有采用这种包内可见的作用域,而是使用了模块内可见,模块内可见指的是该类只对一起编译的其他 Kotlin 文件可见。开发工程与第三方类库不属于同一个模块,这时如果还想使用该类的话只有复制源码一种方式了。这便是 Kotlin 中 internal 修饰符的一个作用体现。

关于 private,在 Java 中很少用于文件对应的类,因为 Java 中一个.java文件只能有一个同名的Java Class,假如我们创建了Rectangle.java这个文件,那么它代表的类要么是public给别人用的,要么是default包私有的,而不会创建一个.java文件它的class却是private,这没有意义,因此 Java 中的 private Class 只存在于一个类的内部。而 kotlin 中则可以用 private 给文件级别的类修饰,因为 kotlin 中一个文件中可以同时存在多个顶级的 Class,我们希望某些 Class 只能在当前文件中使用,其作用域就是当前这个 Kotlin 文件。比如:

class BMWCar(val name: String) {
    private val bMWEngine = Engine("BMW")
    fun getEngine(): String {
        return bMWEngine.engineType()
    }
}
private class Engine(val type: String) {
    fun engineType(): String {
        return "the engine type is $type"
    }
}

Kotlin 与 Java 的可见性修饰符比较:

修饰符含义与 Java 比较
publicKotlin中默认修饰符,全局可见与 Java 中 public 效果相同
protected受保护修饰符,类及子类可见含义一致,但作用域除了类及子类外,包内也可见
private私有修饰符,类内修饰只有本类可见,类外修饰文件内可见私有修饰符,只有类内可见
internal模块内可见

解决多继承问题

多重继承机制如C++会导致钻石问题也叫菱形问题(骡子的多继承困惑)。类的多重继承如果使用不当,就会在继承关系上产生歧义。而且,多重继承还会给代码维护带来很多的困扰:一来代码的耦合度会很高,二来各种类之间的关系令人眼花缭乱。

于是,Kotlin 跟 Java 一样只支持类的单继承。Kotlin 中的接口与 Java 很相似,支持多继承,但它除了可以定义带默认实现的方法之外,还可以声明抽象的属性

interface Flyer {
    fun fly()
    fun kind() = "flying animals"
}
interface Animal {
    val name: String
    fun eat()
    fun kind() = "flying animals"
}
class Bird(override val name: String) : Flyer, Animal {
    override fun eat() {
        println("I can eat")
    }
    override fun fly() {
        println("I can fly")
    }
    override fun kind() = super<Flyer>.kind()
}

fun main(args: Array<String>) {
    val bird = Bird("sparrow")
    println(bird.kind())
}

Bird类同时实现了FlyerAnimal两个接口,但由于它们都拥有默认的kind方法,同样会引起上面所说的钻石问题。而 Kotlin 提供了对应的方式来解决这个问题,那就是super关键字,我们可以利用它来指定继承哪个父接口的方法,比如上面代码中的super<Flyer>.kind()。当然我们也可以主动实现方法,覆盖父接口的方法。如:

override fun kind() = "a flying ${this.name}" 

接口使用规则:

  • 1)在 Kotlin 中实现一个接口时,需要实现接口中没有默认实现的方法及未初始化的属性,若同时实现多个接口,而接口间又有相同方法名的默认实现时,则需要主动指定使用哪个接口的方法或者重写方法;
  • 2)如果是默认的接口方法,你可以在实现类中通过“ super<T>” 这种方式调用它,其中T为拥有该方法的接口名;
  • 3)在实现接口的属性和方法时,都必须带上override关键字,不能省略。

除此之外,注意到,我们通过主构造方法参数的方式来实现Animal接口中的name属性。我们之前说过,通过val声明的构造方法参数,其实是在类内部定义了一个同名的属性,所以我们当然还可以把name的定义放在Bird类内部。

class Bird(name: String) : Flyer, Animal {

    override val name: String // override 不要忘记 

    init {
        this.name = name
    }
}

name的赋值方式其实无关紧要。比如我们还可以用一个getter对它进行赋值。

class Bird(chineseName: String) : Flyer, Animal {
    override val name: String
        get() = translate2EnglishName(chineseName)
}

getter 和 setter

Kotlin 类不存在字段,只有属性,在你声明一个类的属性时,背后 Kotlin 编译器帮你自动生成了gettersetter方法。

当然你也可以主动声明这两个方法来实现一些特殊的逻辑。还有以下两点需要注意:

  • 1)用 val 声明的属性将只有 getter 方法,因为它不可修改;而用 var 修饰的属性将同时拥有gettersetter方法。
  • 2)用 private 修饰的属性编译器将会省略gettersetter方法,因为在类外部已经无法访问它了,这两个方法的存在也就没有意义了。

内部类解决多继承问题的方案

在 Kotlin 中声明一个内部类需要使用inner关键字:(在java中则不需要)

class OuterKotlin {
    val name = "This is truely Kotlin's inner class syntax."
    inner class InnerKotlin {
        fun printName() {
            print("the name is $name")
        }
    }
}

内部类 vs 嵌套类

在 Java 中,我们通过在内部类的语法上增加?个static关键词,把它变成?个嵌套类。然而, Kotlin 则是相反的思路,默认是?个嵌套类,必须加上 inner 关键字才是?个内部类。也就是说可以把 Java 的静态的内部类看成是一种嵌套类。

内部类和嵌套类的差别,主要体现在对外部类成员的访问权限上:

  • 内部类包含着对其外部实例的引用,在内部类中我们可以直接使用外部类中的成员属性;(比如上面例子中的name属性)
  • 而嵌套类不包含对其外部类实例的引用,所以它无法访问其外部类的属性

可以通过定义private inner class使用不同父类对象的方法来解决多继承问题(其实就是通过组合的方式替代了继承):

open class Horse { //马
    fun runFast() {
        println("I can run fast")
    }
}
open class Donkey { //驴
    fun doLongTimeThing() {
        println("I can do some thing long time")
    }
}
class Mule { //骡?
    fun runFast() {
        HorseC().runFast()
    }
    fun doLongTimeThing() {
        DonkeyC().doLongTimeThing() // 调用Donkey类的方法
    }
    private inner class HorseC : Horse()
    private inner class DonkeyC : Donkey()
}

使用委托代替多继承

如何通过委托来代替多继承实现需求:

interface CanFly {
    fun fly()
}
interface CanEat {
    fun eat()
}
open class Flyer : CanFly {
    override fun fly() {
        println("I can fly")
    }
}
open class Animal : CanEat {
    override fun eat() {
        println("I can eat")
    }
}
class Bird(flyer: Flyer, animal: Animal) : CanFly by flyer, CanEat by animal {}

fun main(args: Array<String>) {
    val flyer = Flyer()
    val animal = Animal()
    val b = Bird(flyer, animal)
    b.fly()
    b.eat()
}

委托方式接口实现多继承如此相似,跟组合也很像,那么它到底有什么优势呢?主要有以下两点:

  • 1)前面说到接口是无状态的,所以即使它提供了默认方法实现也是很简单的,不能实现复杂的逻辑,也不推荐在接口中实现复杂的方法逻辑。我们可以利用上面委托的这种方式,虽然它也是接口委托,但它是用一个具体的类去实现方法逻辑,可以拥有更强大的能力。
  • 2)假设我们需要继承的类是A,委托对象是BC、我们在具体调用的时候并不是像组合一样A.B.method,而是可以直接调用A.method,这更能表达A拥有该method的能力,更加直观,虽然背后也是通过委托对象来执行具体的方法逻辑的。

用 data class 创建数据类

类似 java 中的 JavaBean

data class Bird(var weight: Double, var age: Int, var color: String) 

Kotlin 编译器帮我们做了很多事情。我们来看看这个类反编译后的 Java 代码:

public final class Bird {
    private double weight;
    private int age;
    @NotNull
    private String color;
    public final double getWeight() {
        return this.weight;
    }
    public final void setWeight(double var1) {
        this.weight = var1;
    }
    public final int getAge() {
        return this.age;
    }
    public final void setAge(int var1) {
        this.age = var1;
    }
    @NotNull
    public final String getColor() {
        return this.color;
    }
    public final void setColor(@NotNull String var1) {
        Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
        this.color = var1;
    }
    public Bird(double weight, int age, @NotNull String color) {
        Intrinsics.checkParameterIsNotNull(color, "color");
        super();
        this.weight = weight;
        this.age = age;
        this.color = color;
    }
    public final double component1() { //Java中没有
        return this.weight;
    }
    public final int component2() { //Java中没有
        return this.age;
    }
    @NotNull
    public final String component3() { //Java中没有
        return this.color;
    }
    @NotNull
    public final Bird copy(double weight, int age, @NotNull String color) { //Java中没有
        Intrinsics.checkParameterIsNotNull(color, "color");
        return new Bird(weight, age, color);
    }
// $FF: synthetic method
// $FF: bridge method
    @NotNull
    public static Bird copy$default(Bird var0, double var1, int var3, String var4, Int var5){ //Java中没有
        if ((var5 & 1) != 0) {
            var1 = var0.weight;
        }
        if ((var5 & 2) != 0) {
            var3 = var0.age;
        }
        if ((var5 & 4) != 0) {
            var4 = var0.color;
        }
        return var0.copy(var1, var3, var4);
    }
    public String toString() {
        ...
    }
    public int hashCode() {
        ...
    }
    public boolean equals(Object var1) {
        ...
    }
}

这段代码是不是和 JavaBean 代码很相似,同样有getter/setterequalshashcode、构造函数等方法,其中的equalshashcode使得一个数据类对象可以像普通类型的实例一样进行判等,我们甚至可以像基本数据类型一样用==来判断两个对象相等,如下:

val b1 = Bird(weight = 1000.0, age = 1, color = "blue")
val b2 = Bird(weight = 1000.0, age = 1, color = "blue")
b1.equals(b2)
>>> true
b1 == b2
>>> true

copy、componentN 与解构

我们继续来看上面代码中的?段:

@NotNull
public final Bird copy(double weight, int age, @NotNull String color) { 
    Intrinsics.checkParameterIsNotNull(color, "color");
    return new Bird(weight, age, color);
}
@NotNull
public static Bird copy$default(Bird var0, double var1, int var3, String var4, Int var5){ 
    if ((var5 & 1) != 0) {
        var1 = var0.weight; // copy时若未指定具体属性的值,则使?被copy对象的属性值
    }
    if ((var5 & 2) != 0) {
        var3 = var0.age;
    }
    if ((var5 & 4) != 0) {
        var4 = var0.color;
    }
    return var0.copy(var1, var3, var4);
}

这段代码中的copy方法的主要作用就是帮我们从已有的数据类对象中拷贝一个新的数据类对象。当然你可以传入相应参数来生成不同的对象。但同时我们发现,在copy的执行过程中,若你未指定具体属性的值,那么新生成的对象的属性值将使用被copy对象的属性值,这便是我们平常所说的浅拷贝

实际上copy更像是一种语法糖,假如我们的类是不可变的,属性不可以修改,那么我们只能通过copy来帮我们基于原有对象生成一个新的对象。

比如下面的两个例子:

// 声明的Bird属性可变
data class Bird(var weight: Double, var age: Int, var color: String)
val b1 = Bird(20.0, 1, "blue")
val b2 = b1
b2.age = 2

// 声明的Bird属性不可变
data class Bird(val weight: Double, val age: Int, val color: String)
val b1 = Bird(20.0, 1, "blue")
val b2 = b1.copy(age = 2) // 只能通过copy

copy提供了一种简洁的方式帮我们复制一个对象,但它是一种浅拷贝的方式。所以在使用copy的时候要注意使用场景,因为数据类的属性可以被修饰为var,这便不能保证不会出现引用修改问题。

componentN可以理解为类属性的值,其中N代表属性的顺序,比如component1代表第1个属性的值,component3代表第3个属性的值。

componentN主要为了提供解构语法

val bird = Bird(20.0, 1, "blue")

// 通常方式
val weight = bird.weight
val age = bird.age
val color = bird.color

// Kotlin 解构赋值
val (weight, age, color) = bird

你可能写过类似下面的代码:

String birdInfo = "20.0,1,bule";
String[] temps = birdInfo.split(",");
double weight = Double.valueOf(temps[0]);
int age = Integer.valueOf(temps[1]);
String color = temps[2];

这样代码有时真的很烦琐,我们明明知道值的情况,却要分好几步来给变量赋值。很幸运, Kotlin 提供了更优雅的做法:

val (weight, age, color) = birdlnfo.split(","); 

当然 Kotlin 对于数组的解构也有一定限制,在数组中它默认最多允许赋值 5 个变量,因为若是变量过多,效果反而会适得其反,因为到后期你都搞不清楚哪个值要赋给哪个变量了。

在数据类中,你除了可以利用编译器帮你自动生成componentN方法以外,甚至还可以自己实现对应属性的componentN方法。

注意:数据类中的解构基于componentN函数,如果自己不声明componentN函数,那么就会默认根据主构造函数参数来生成具体个数的componentN函数,与次构造函数中的参数无关。

Pair 和 Triple

除了数组支持解构外,Kotlin 也提供了其他常用的数据类,让使用者不必主动声明这些数据类,它们分别是PairTriple。其中Pair是二元组,可以理解为这个数据类中有两个属性;Triple是三元组,对应的则是3个属性。我们先来看一下它们的源码:

// Pair
data class Pair<out A, out B>(
    val first: A,
    val second: B
)

// Triple
data class Triple<out A, out B, out C>(
    val first: A,
    val second: B,
    val third: C
)

可以发现Pair和Triple都是数据类,它们的属性可以是任意类型,我们可以按照属性的顺序来获取对应属性的值。

因此我们可以通过解构来获取其中的数据:

val pair = Pair(20.0, 1)
val triple = Triple(20.0, 1, "blue")

// 利用属性顺序获取值
val weightP = pair.first
val ageP = pair.second
val weightT = triple.first
val ageT = triple.second
val colorT = triple.third

// 利用解构
val (weightPD, agePD) = Pair(20.0, 1)
val (weightTD, ageTD, colorTD) = Triple(20.0, 1, "blue")

数据类的约定与使用

如果你要在Kotlin声明一个数据类,必须满足以下几点条件:

  • 数据类必须拥有一个构造方法,该方法至少包含一个参数,一个没有数据的数据类是没有任何?处的;
  • 与普通的类不同,数据类构造方法的参数强制使用var或者val进行声明;
  • data class的前面不能有 abstractopensealed或者inner等修饰符;
  • 数据类既可以实现接口也可以继承类。

数据类的另一个典型的应用就是代替我们在 Java 中的建造者模式。正如你所知,建造者模式主要化解 Java 中书写一大串参数的构造方法来初始化对象的场景。然而由于 Kotlin 中的类构造方法可以指定默认值,你可以想象,依靠数据类的简洁语法,我们就可以更方便地解决这个问题。

伴生对象

它属于这个类所有,因此伴生对象跟Java中static修饰效果性质一样,全局只有一个单例。它需要声明在类的内部,在类被装载时会被初始化。

class Prize(val name: String, val count: Int, val type: Int) {
    companion object {
        const val TYPE_REDPACK = 0
        const val TYPE_COUPON = 1
    }
}

fun isRedpack(prize: Prize): Boolean {
    return prize.type == Prize.TYPE_REDPACK
}

fun main(args: Array<String>) {
    val prize = Prize("红包", 10, Prize.TYPE_REDPACK)
    print(isRedpack(prize))
}

可以发现,该版本在语义上更清晰了。而且,companion object 用花括号包裹了所有静态属性和方法,使得它可以与Prize类的普通方法和属性清晰地区分开来。最后,我们可以使用点号来对一个类的静态的成员进行调用。

伴生对象也是实现工厂方法模式的另一种思路:

class Prize private constructor(val name: String, val count: Int, val type: Int) {
    companion object {
        const val TYPE_COMMON = 1
        const val TYPE_REDPACK = 2
        const val TYPE_COUPON = 3
        val defaultCommonPrize = Prize("普通奖品", 10, TYPE_COMMON)
        fun newRedpackPrize(name: String, count: Int) = Prize(name, count, TYPE_REDPACK)
        fun newCouponPrize(name: String, count: Int) = Prize(name, count, TYPE_COUPON)
        fun defaultCommonPrize() = defaultCommonPrize
    }
}

fun main(args: Array<String>) {
    val redpackPrize = Prize.newRedpackPrize("红包", 10)
    val couponPrize = Prize.newCouponPrize("十元代金券", 10)
    val commonPrize = Prize.defaultCommonPrize()
}

总的来说,伴生对象就是 Kotlin 中用来代替 static 关键字的一种方式,任何在 Java 类内部用static定义的内容都可以用 Kotlin 中的伴生对象来实现。然而,它们是类似的,一个类的伴生对象跟一个静态类一样,全局只能有一个。

天生的单例:object

单例模式最大的一个特点就是在系统中只能存在一个实例对象,所以在 Java 中我们必须通过设置构造方法私有化,以及提供静态方法创建实例的方式来创建单例对象。

在 Kotlin 中,由于object的存在,我们可以直接用它来实现单例:

object DatabaseConfig {
    var host: String = "127.0.0.1"
    var port: Int = 3306
    var username: String = "root"
    var password: String = ""
}

由于object全局声明的对象只有一个,所以它并不用语法上的初始化,甚至都不需要构造方法。

因此,我们可以说,object创造的是天生的单例,我们并不需要在 Kotlin 中去构建一个类似 Java 的单例模式。

由于DatabaseConfig的属性是用var声明的String,我们还可以修改它们:

DatabaseConfig.host = "localhost" 
DatabaseConfig.port = 3307 

由于单例也可以和普通的类一样实现接口和继承类,所以你可以将它看成一种不需要我们主动初始化的类,它也可以拥有扩展方法。

单例对象会在系统加载的时候初始化,当然全局就只有一个

那么,object声明除了表现在单例对象及上面的说的伴生对象之外,还有其他的作用吗?

它还有一个作用就是替代 Java 中的匿名内部类。

object 表达式

写Java的时候很多人肯定被它的匿名内部类弄得很烦燥,有时候明明只有一个方法,却要用一个匿名内部类去实现。比如我们要对一个字符串列表排序:

List<String> list = Arrays.asList("redpack", "score", "card");
Collections.sort(list, new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        if (s1 == null) return -1;
        if (s2 == null) return 1;
        return s1.compareTo(s2);
    }
});

而在Kotlin中,可以利用 object 表达式对它进行改善:

val comparator = object : Comparator<String> {
    override fun compare(s1: String?, s2: String?): Int {
        if (s1 == null) return -1
        else if (s2 == null) return 1
        return s1.compareTo(s2)
    }
}

Collections.sort(list, comparator)

简单来看,object表达式跟Java的匿名内部类很相似,但是我们发现,object表达式可以赋值给一个变量,这在我们重复使用的时候将会减少很多代码。另外,我们说过object可以继承类和实现接口,匿名内部类只能继承一个类及实现一个接口,而object表达式却没有这个限制。

匿名内部类与object表达式并不是对任何场景都适合的, 可以将上面的代码用 Lambda 表达式的方式重新改造一下:

val comparator = Comparator<String> { s1, s2 ->
    if (s1 == null) return @Comparator -1
    else if (s2 == null) return @Comparator 1
    s1.compareTo(s2)
}

Collections.sort(list, comparator)

使用 Lambda 表达式后代码变得简洁很多。

对象表达式与 Lambda 表达式哪个更适合代替匿名内部类?

  • 当你的匿名内部类使用的类接口只需要实现一个方法时,使用 Lambda 表达式更适合;
  • 当匿名内部类内有多个方法实现的时候,使用 object 表达式更加合适。

构造代数数据类型 ADT 与 when 表达式

例如,我们想根据一些条件来计算下面几种图形的面积:

  • 圆形(给定半径)
  • 长方形(给定长和宽)
  • 三角形(给定底和高)

首先,找到它们的共同点,即它们都是几何图形(Shape)。然后我们就可以利用密封类来进行抽象:

sealed class Shape {
    class Circle(val radius: Double) : Shape()
    class Rectangle(val width: Double, val height: Double) : Shape()
    class Triangle(val base: Double, val height: Double) : Shape()
}

使用ADT的最大好处就是可以很放心地去使用when表达式。我们就利用when表达式去定义一个计算各个图形面积的方法:

fun getArea(shape: Shape): Double = when (shape) {
    is Shape.Circle -> Math.PI * shape.radius * shape.radius
    is Shape.Rectangle -> shape.width * shape.height
    is Shape.Triangle -> shape.base * shape.height / 2.0
}

通过使用ADTwhen表达式,上面求面积的代码看上去非常简洁。如果我们使用 Java 来实现,则需要写一堆 if-else 表达式,而且还要考虑非法的情况,代码的可读性显得一般。

fun logicPattern(a: Int) = when (a) {
    in 2..11 -> (a.toString() + " is smaller than 10 and bigger than 1")
    else -> "Maybe" + a + "is bigger than 10, or smaller than 1"
}
fun logicPattern(a: String) = when {
    a.contains("Yison") -> "Something is about Yison"
    else -> "It`s none of Yison`s business"
}

通过上面两个例子,我们展示了when是如何匹配逻辑表达式的。注意,上面的when表达式与我们前面几个节中不同,这里关键字when的后面没有带参数。

文章来源:https://blog.csdn.net/lyabc123456/article/details/135024601
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。