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

news2024/11/18 20:49:00

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的后面没有带参数。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1313780.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

逆向经历回顾总结

逆向经历回顾总结 一、前言 将自己的逆向经验做个总结&#xff0c;希望新手对逆向大方向能快速了解。高手有啥不一样的经验也可以讨论讨论。 二、个人经历 本人入行逆向全因一部韩剧“幽灵”&#xff0c;里面涉及渗透、病毒分析、取证的攻防对抗&#xff0c;我觉得对新手来说…

S1试讲讲稿

习题题目 答案 用到的概念&#xff1a; 概率之和等于1 E ( x ) ∑ i 1 4 x i P i E(x)\sum_{i1}^4x_iP_i E(x)∑i14​xi​Pi​ E ( x 2 ) ∑ i 1 4 x i 2 P i E(x^2)\sum_{i1}^4x_i^2P_i E(x2)∑i14​xi2​Pi​ V a r ( X ) Var(X) Var(X) ∑ i 1 4 ( x i − x ‾ ) 2…

美易官方:道指续创收盘历史新高

美国股市周三继续上涨&#xff0c;道琼斯工业平均指数连续六个交易日收高&#xff0c;并再次创下收盘历史新高。尽管市场对全球经济增长放缓的担忧依然存在&#xff0c;但美国经济数据的强劲表现以及企业盈利的良好预期&#xff0c;使得投资者对股市的信心得到提振。 在今日的交…

Volatility3内存取证工具安装及入门在Linux下的安装教程

1-1. Volatility3简介 Volatility 是一个完全开源的工具&#xff0c;用于从内存 (RAM) 样本中提取数字工件。支持Windows&#xff0c;Linux&#xff0c;MaC&#xff0c;Android等多类型操作系统系统的内存取证。 针对竞赛这块&#xff08;CTF、技能大赛等&#xff09;基本上都…

系列二十七、Apache Jmeter使用

一、安装 下载安装包>解压到指定目录>双击打开D:\Programs\apache-jmeter-5.5\bin\ApacheJmeter.jar即可。我分享的ApacheJmeter链接&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1VI7f3buIWZbQEeq2CRbwlg?pwdyyds 提取码&#xff1a;yyds 二、使用 2.1、添…

docker创建镜像 Dockerfile

目录 docker的创建镜像的方式 dockerfile形成&#xff08;原理&#xff09; docker的核心作用 docker的文件结构 dockerfile的语法 CMD和ENTRPOINT的区别 创建dockerfile镜像 区别 RUN命令的优化 如何把run命令写在一块 copy和ADD区别 区别 centos7 构建Apache的d…

jdk常用命令

1.jinfo命令 jinfo命令主要查看虚拟机配置参数信息&#xff0c;也可用于调整虚拟机的配置参数。jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值&#xff0c; 甚至可以在运行时修改部分参 数&#xff0c;并使之立即生效。 但是&#xff0c;并非所有参数都支持动态修改。…

springboot使用validation-api对入参进行校验

springboot使用validation-api对入参进行校验 前言&#xff1a;在实际开发中&#xff0c;我们不能完全相信前端给我们的参数是否符合预期或规范&#xff0c;为了避免恶意入参&#xff0c;引发系统或数据安全问题&#xff0c;需要对前端传过来的参数进行校验。例如字符串长度校…

牛客网BC107矩阵转置

答案&#xff1a; #include <stdio.h> int main() {int n0, m0,i0,j0,a0,b0;int arr1[10][10]{0},arr2[10][10]{0}; //第一个数组用来储存原矩阵&#xff0c;第二个数组用来储存转置矩阵scanf("%d%d",&n,&m); if((n>1&&n<10)&&am…

ShopsN commentUpload 文件上传漏洞复现

0x01 产品简介 ShopsN 是一款符合企业级商用标准全功能的真正允许免费商业用途的开源网店全网系统。 0x02 漏洞概述 ShopsN commentUpload 接口处存在任意文件上传漏洞,攻击者可以利用文件上传漏洞执行恶意代码、写入后门、读取敏感文件,从而可能导致服务器受到攻击并被控…

千梦网创:Too Young,to simple

大多数人啊&#xff0c;还是too young&#xff0c;包括我。 网上的评论对我而言并不影响我通过提供价值服务经营生活&#xff0c;但是有时候对于我的思考还是有一些帮助的。 我把很多可以争与不可争的事件看做是一种现象&#xff0c;这种现象往往可以给予我新的能量。 当学员…

2024美赛备战-美赛必备技能(matlab 和SPSS入门必备)

( 一 )Matlab 1.数值计算和符号计算功能 Matlab 以矩阵作为数据操作的基本单位&#xff0c;它的指令表达式与数学、工程中 常用的符号、表达式十分相似&#xff0c;故用Matlab 来解算问题要比用C、FORTRAN 等 语 言完成相同的事情简捷得多&#xff0c;使学者易于学习和掌握…

完蛋,我的AI自己动起来了

故事的开始 一开始&#xff0c;我只是给我的公众号接入了星火认知大模型。但是公众号的应用场景不足&#xff0c;没办法当成群机器人来用。所以我后来办了张新的电话卡&#xff0c;注册了个小号微信。想把小号打造成微信群聊助手&#xff0c;在我没时间翻冗长的聊天记录的时候…

平面腔体谐振计算与仿真

PCB的电源网络是由电介质材料隔开的两个平行金属板所组成&#xff0c;可以通过以下的3种方法对其谐振模式进行分析&#xff1a; 1. 基于腔体模型的计算&#xff1b; 2. 基于SPICE等效电路&#xff1b; 3. 基于全波数值电磁算法的3D模型。 设计得当的前提下&#xff0c;上述3种方…

西工大计院计算机系统基础实验二(配置gdb插件)

第二次实验是二进制炸弹实验&#xff0c;为了简化操作&#xff0c;并且让大家接下来能够按照作者之前已经为网安院写好的博客西工大网络空间安全学院计算机系统基础实验二&#xff08;清楚实验框架及phase_1&#xff09;-CSDN博客来走&#xff0c;大家需要下载一款好用的gdb插件…

退回论文如何修改最好最快 快码论文

大家好&#xff0c;今天来聊聊退回论文如何修改最好最快&#xff0c;希望能给大家提供一点参考。 以下是针对论文重复率高的情况&#xff0c;提供一些修改建议和技巧&#xff0c;可以借助此类工具&#xff1a; 退回论文如何修改最好最快 当论文被退回时&#xff0c;如何修改才…

“快速排序:一种美丽的算法混沌”

欢迎来到我的博客&#xff01;在今天的文章中&#xff0c;我将采用一种独特且直观的方式来探讨我们的主题&#xff1a;我会使用一幅图像来贯穿整篇文章的讲解。这幅精心设计的图表不仅是我们讨论的核心&#xff0c;也是一个视觉辅助工具&#xff0c;帮助你更深入地理解和掌握本…

【idea】idea尾部自动删除空格,idea2023.1.2关闭自动去除行尾空格的功能

这个功能是由于git或者svn上的代码有许多空格的时候&#xff0c;会自动把空格去掉&#xff0c;就会导致出现许多更改的地方&#xff0c;会自动删空格。 尾部刚打好空格准备写代码&#xff0c;自动就删掉空格&#xff0c;又得重打空格后继续编码&#xff0c;非常不爽。 设置如…

linux sed批量修改替换文件中的内容/sed特殊字符

sed系列文章 linux常用命令(9)&#xff1a;sed命令(编辑/替换/删除文本)linux sed命令删除一行/多行_sed删除第一行/linux删除文件某一行linux sed批量修改替换文件中的内容/sed特殊字符 文章目录 sed系列文章一、sed替换文本语法1.1、基础语法1.2、高阶语法 二、实战/实例2.1…

群晖7.2使用Docker安装容器魔方结合内网穿透实现远程访问

最近&#xff0c;我发现了一个超级强大的人工智能学习网站。它以通俗易懂的方式呈现复杂的概念&#xff0c;而且内容风趣幽默。我觉得它对大家可能会有所帮助&#xff0c;所以我在此分享。点击这里跳转到网站。 文章目录 1. 拉取容器魔方镜像2. 运行容器魔方3. 本地访问容器魔…