【Kotlin】初识Kotlin之面向对象

1、类

在Kotlin中,类用关键字class来定义

如果一个类具有类体,那么需要使用{ }来写类体内容,如果不需要类体,那么只需要定义类名就可以了

// 定义一个alarmClock类
class AlarmClock {
    fun alarm() = println("叮铃铃...")
}

// 定义没有类体的alarm类
class Alarm

1. 类修饰符

  • final:不能被继承
  • open:可以被继承
  • abstract:抽象类
  • enum:枚举类
  • data:数据类
  • sealed:密封类
  • annotation:注解类

Java 中 默认类都是public 的 ,kotlin 类默认都是final修饰的,不能用来继承,需要添加open 修饰符

2. 成员修饰符

  • override: 重写函数
  • open:可以被重写
  • final:不能被重写
  • abstract:抽象函数
  • lateinit:延迟初始化

3. 泛型修饰符

泛型修饰符多用于协变

  • in:相当于Java中的super关键字的作用
  • out:相当于Java中的extends关键字的作用

2、构造函数

在Kotlin中,构造函数与Java大有不同。

Kotlin的一个类中可以有一个主构造函数和多个次构造函数

1. 主构造函数

所谓主构造函数其实就是类头括号中的一部分,例如

class Person constructor(name: String) {...}

如果主构造函数没有任何的注解或者可见性的修饰符,那么可以省略constructor关键字

class Person(name: String) {...}

但是这样我们仅仅是给Persion类传入了name这个参数,并没有让他给成员变量赋值,这样的效果和如下的Java代码一样

class Person1 {
    public Person1(String name) {
        
    }
}

所以如何让我们传入的name成为成员变量呢?

在Kotlin的类中,有一个init关键字,可以作类的为初始化块,可以理解为Java构造方法中的代码部分。

如果我们想给成员变量赋值,可以这么做

class Person(name: String) {
    var name = "no name"
    init {
        this.name = name
    }
}

但是这样其实是仿照Java的对象模式,而且这里使用了var的变量

如果想直接一步到位,可以

class Person(name: String) {
    val name = name
}

Kotlin支持下面这样的化简,一步到位,与上述代码是一样的功能

class Person(val name: String) {}

当然,和函数一样,我们可以给这个成员变量赋默认值

class Person(val name: String = "woodwhale") {}

2. 次构造函数

次构造函数就直接在类体中,使用constructor声明

class Person(val pets: MutableList<Pet> = mutableListOf())

class Pet {
    constructor(owner: Person) {
        owner.pets.add(this) // adds this pet to the list of its owner's pets
    }
}

上述代码的Pet类没有使用主构造函数而使用了次构造函数进行初始化

class Person(val name: String) {
    val children: MutableList<Person> = mutableListOf()
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

观察上述代码,如果类有一个主构造函数,每个次构造函数需要委托给主构造函数, 可以直接委托或者通过别的次构造函数间接委托。委托到同一个类的另一个构造函数用 this 关键字即可

请注意,初始化块中的代码实际上会成为主构造函数的一部分。委托给主构造函数会作为次构造函数的第一条语句,因此所有初始化块与属性初始化器中的代码都会在次构造函数体之前执行,例如:

class Constructors {
    init {
        println("Init block")
    }

    constructor(i: Int) {
        println("Constructor $i")
    }
}

fun main() {
    Constructors(1)
}

/*
    Init block
    Constructor 1

    进程已结束,退出代码0
*/

如果我们的一个类的主构造函数可见性设置为private,那么我们将无法直接创建这个对象

image-20220224183612887

可以使用单例模式来实现,这里举一个懒汉式的例子:

fun main() {
    DontCreateMe.get()
}

class DontCreateMe private constructor () {
    companion object {
        private val me: DontCreateMe? = null
        get() {
            return field ?: DontCreateMe()
        }
        fun get(): DontCreateMe {
            return me!!
        }
    }
}

这里的companion object 是伴生类,之后会将到

3、创建对象

对象就是实例化一个类,我们可以理解一下——类是一个蓝图,而对象是蓝图设计出来的产物

创建对象在Kotlin中不需要使用new关键字,和python类似,只需要在类名之后写上一个()就可以

fun main() {
    val person = Person()
}

class Person

如果类带有参数,这样处理就好了

fun main() {
    val person = Person("woodwhale",18)
    println("姓名: ${person.name}, 年龄: ${person.age}")
}

class Person(val name: String, val age: Int)

/*
    姓名: woodwhale, 年龄: 18

    进程已结束,退出代码0
*/

4、类成员

一个类中可以有如下的成员:

  • 构造函数和初始化块
  • 方法(函数)
  • 属性
  • 嵌套类和内部类
  • 伴生对象
  • 对象声明

上述的成员中,需要讲解的就是嵌套类和内部类,还有一个对象声明,其他的内容之前都讲述过了

1. 嵌套类和内部类

首先在Java中我们只听说过内部类这种名词,嵌套类是什么呢?

在Kotlin中,内部类用专门的inner来修饰,而类中没有使用inner的类,就属于嵌套类

例如:

// 嵌套类
class A {
    class B
}

// 内部类
class C {
    inner class D
}

值得注意的是,内部类可以访问外部类的属性

2. get与set

在Kotlin中,没有Java传统的getter和setter,我们直接通过实例对象.属性的方式来访问或者赋值

但是如果我想在get和set的过程中进行操作呢?例如,我想每次都获取Person类中name的大写

在Kotlin中,为了解决上述问题,在类中有getset两个函数。如果我们没有使用的话,那么就是默认的。

使用方式:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

这个getset是针对一个属性来写的,如果一个类中有很多的属性,那么这些属性都可以使用自己的getset方法

举个例子:

fun main() {
    val student = Student()
    student.name = "woodwhale"
    println(student.name)
}

class Student {
    var name: String = ""
    get() = field.lowercase()
    set(value) {
        field = value.uppercase()
        println(field)
    }
}

/*
    WOODWHALE
    woodwhale

    进程已结束,退出代码0
*/

在上述代码中,我们在get函数中,返回了 field.lowercase(),这里的field指的就是name这个属性,但是不能直接使用name.lowercase(),因为这样的name也会有get和set,会导致无限递归从而内存溢出。而set函数中,我们给field赋值value.uppercase()也就是转为大写。

注意:

  • 一般情况下,我们不会修改get和set,使用默认的就可以了
  • val修饰的属性不允许使用set方法,因为它是只读的
  • get和set的原则是,作用上面最近的一个属性

3. 对象声明

在Kotlin中我们可以直接使用object关键字来声明一个对象,这个对象所有的属性可以直接访问。对象声明非常适合用来写Utils类

注意,使用object声明得到的对象是一个单例,引用这个对象只需要使用使用其名就可以

object DataProviderManager {
    fun registerDataProvider(provider: DataProvider) {
        // ……
    }

    val allDataProviders: Collection<DataProvider>
        get() = // ……
}

fun main() {
    DataProviderManager.registerDataProvider(……)
}

因为对象声明得到的是一个单例,如果引入变量,那么这两个变量指向的地址相同

var data1 = DataProviderManager
var data2 = DataProviderManager
data1.name = "test"
print("data1 name = ${data2.name}")  

4. 伴生对象

在Kotlin中,没有static关键字,那么如何在类中声明一个静态常量或者函数(方法)呢

这个时候,我们就使用Kotlin中的伴生对象,使用关键字companion 标记,这样,类中的伴生对象就与这个类进行了关联,我们可以直接通过这个外部类访问伴生对象中的内部元素

class MyClass {
    companion object Factory {
        fun create(): MyClass = MyClass()
    }
}

// 访问到对象的内部元素
val instance = MyClass.create()

当然,我们可以省略掉这个伴生对象的名字,在上述代码中是Factory,我们可以直接调用如下:

class MyClass {
    companion object {
        fun create(): MyClass = MyClass()
    }
}

val instance = MyClass.Companion.create()

注意:

  • 一个类中只能声明一个伴生对象,也就是companion只能使用一次

  • 尽管伴生对象看起来像其他语言中的静态成员,但是他们在运行的时候是真实对象的实例成员,我们还可以让伴生对象实现接口

interface Factory<T> {
    fun create(): T
}


class MyClass {
    companion object : Factory<MyClass> {
        override fun create(): MyClass = MyClass()
    }
}

对象表达式和对象声明之间有一个重要的语义差别:

  • 对象表达式是在使用他们的地方立即执行的
  • 对象声明是在第一次被访问到时延迟初始化的
  • 伴生对象的初始化是在相应的类被加载(解析)时,与 Java 静态初始化器的语义相匹配

5. 延迟初始化

在Kotlin中,如果我们在类中普通的声明一个成员属性,那么需要赋初始值,因为Kotlin为了避免null的出现。

如果我们不想进行更多的开销去初始化一个用的很少的成员属性,我们可以使用延迟初始化的方式。

使用lateinit关键字修饰属性,我们就不必声明其初始值,编译器或默认你的这个属性会在使用前被用户赋值

class MyTest {
    lateinit var subject: String
}

注意:

  • lateinit修饰符只能用于在类体中的属性(不是在主构造函数中声明的var属性,并且仅当该属性没有定义get和set的手)
  • lateinit修饰符修饰的属性不能是原生类型(Int等),可以是String等
  • 如果没有给lateinit修饰符修饰的属性赋值就直接读取,会报错
  • 我们可以使用value.isInitialized属性来判断value是否被初始化过
  • 被初始化过的属性无法再次被初始化,相当于一个单例属性

5、继承

1. 父子类

在Kotlin中,所有的类都有一个公共的父类(超类),这个类是Any,类似于Java中的Object,对于没有声明父类的类,其父类默认是Any

Kotlin和Java在继承这块和一样,只能单继承,但是可以多实现(实现多个接口)

在默认情况下,Kotlin的类是final的,他们不能被继承,如果想让这个类可以被继承,需要使用open关键字标记这个类

open class Base // 该类开放继承

如果需要声明一个子类,可以使用如下方式,使用:表示继承或实现

open class Base(p: Int)

class Derived(p: Int) : Base(p)

如果子类有一个主构造函数,那么必须使用父类的主构造函数的参数当场初始化

如果子类没有主构造函数,那么每个次构造函数的都使用super关键字初始化,或者委托给另一个次构造函数,不同的次构造函数可以调用父类的不同的构造函数

class MyView : View {
    constructor(ctx: Context) : super(ctx)
    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

2. 重写方法

在Java中,我们使用@override注解的方式来声明一个重写的方法,而在Kotlin中,我们直接使用override修饰符来进行修饰函数就可以了

open class Shape {
    open fun draw() { /*……*/ }
    fun fill() { /*……*/ }
}

class Circle() : Shape() {
    override fun draw() { /*……*/ }
}

需要注意的是,重写的方法也需要加上open来修饰,如果不使用open修饰(例如Shape.fill()),那么子类中将不能拥有一个名为fill()的函数。在此基础上,如果一个类没有用open修饰,那么这个类中的方法即使用open来修饰也没有作用

子类的子类也可以重写方法,如果一个子类不想让其子类再次重写该方法,可以加上final修饰符

open class Rectangle() : Shape() {
    final override fun draw() { /*……*/ }
}

3. 重写属性

与Java不同的是,Kotlin可以重写属性,而Java的子类中的属性可以与父类同名,这样父类中的属性就成了隐藏属性。

Kotlin中的重写属性,同样也是使用override来修饰属性,前提是父类中的这个属性被open修饰

open class Shape {
    open val vertexCount: Int = 0
}

class Rectangle : Shape() {
    override val vertexCount = 4
}

我们可以使用var来覆写val的属性,但是反过来就不行,原理就是val只有get方法,如果给val重写成了var,那么这个set如何管理呢?

我们可以直接在主构造器中使用override关键字作为属性声明的一部分

// 形状接口
interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // 总是有 4 个顶点

4. 子类初始化顺序

在子类实例化对象的过程中,第一步是完成父类的初始化,父类初始化之后,就到了子类进行初始化

fun main() {
   Derived("w","d")
}

open class Base(val name: String) {

    init { println("Initializing Base") }

    open val size: Int =
        name.length.also { println("Initializing size in Base: $it") }
}

class Derived(
    name: String,
    val lastName: String,
) : Base(name.capitalize().also { println("Argument for Base: $it") }) {

    init { println("Initializing Derived") }

    override val size: Int =
        (super.size + lastName.length).also { println("Initializing size in Derived: $it") }
}

/*
    Argument for Base: W
    Initializing Base
    Initializing size in Base: 1
    Initializing Derived
    Initializing size in Derived: 2

    进程已结束,退出代码0

*/

这意味着,父类构造函数执行时,子类中声明或覆盖的属性都还没有初始化。如果在父类初始化逻辑中(直接或通过另一个覆盖的 open 成员的实现间接)使用了任何一个这种属性,那么都可能导致不正确的行为或运行时故障。设计一个父类时,应该避免在构造函数、属性初始化器以及 init 块中使用 open 成员。

5. 子类调用父类

在子类中,使用super关键字可以调用父类(超类)中的函数和属性

open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

class FilledRectangle : Rectangle() {
    override fun draw() {
        super.draw()
        println("Filling the rectangle")
    }
    // 调用父类中的borderColor
    val fillColor: String get() = super.borderColor
}

如果在一个内部类中需要访问外部类的父类(超类),那么美可以通过由外部类名限定的super关键字来实现,super@OuterClass

class FilledRectangle: Rectangle() {
    override fun draw() { 
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
            // 调用 Rectangle 的 draw() 实现
            super@FilledRectangle.draw() 
            fill()
            // 使用 Rectangle 所实现的 borderColor 的 get()
            println("Drawn a filled rectangle with color ${[email protected]}") 
        }
    }
}

6. super覆盖冲突解决

在Kotlin中,如果一个类从它的直接超类继承相同成员的多个实现,那么这个类必须覆盖掉这个成员,并且自己提供实现方式。

为了表示从哪个超类继承了实现,我们使用带尖括号的super<Base>来限定使用那个超类

open class Rectangle {
    open fun draw() { /* …… */ }
}

interface Polygon {
    fun draw() { /* …… */ } // 接口成员默认就是“open”的
}

class Square() : Rectangle(), Polygon {
    // 编译器要求覆盖 draw():
    override fun draw() {
        super<Rectangle>.draw() // 调用 Rectangle.draw()
        super<Polygon>.draw() // 调用 Polygon.draw()
    }
}

在上述例子中,Square类既继承Rectangle类,又实现了Polygon接口。但是父类和接口中都有draw()这个方法函数,为了消除歧义,Square类必须自己实现draw()方法函数,可以是使用super实现,也可以自己重写

6、抽象类

类或者其中的成员可以声明为abstract,表示抽象类或者抽象成员

抽象成员在当前这个类中不需要实现

抽象类不需要标注open

我们一般是让一个类实现抽象类

class Polygon : Rectangle(){ 
     override fun draw() {}
}

abstract class Rectangle {
    abstract fun draw()
}

当然,我们也可以用一个抽象成员覆盖一个非抽象的开放成员

open class Polygon {
    open fun draw() {}
}

abstract class Rectangle : Polygon() {
    abstract override fun draw()
}

7、接口

1. 接口定义

在面向对象中,接口很重要!在Kotlin中,也是使用关键字 interface来定义接口

interface MyInterface {
    fun bar()
    fun foo() {
      // 可选的方法体
    }
}

2. 接口实现

接口的实现和继承相似,也是使用:,只不过继承是: 类名(),接口是: 接口名

class Child : MyInterface {
    override fun bar() {
        println(114514)
    }
}

3. 接口属性

接口中可以定义属性,要么是抽象的,要么提供get函数来实现。接口声明的属性没有幕后字段(backing field),也就是无法使用field来指明这个属性

fun main() {
    val child = Child()
    println(child.propertyWithImplementation)
    child.sout1()

}

interface MyInterface {
    val test: Int // 抽象的

    val propertyWithImplementation: String
        get() = "default"

    fun sout1() {
        print(test)
    }
}

class Child : MyInterface {
    override val test: Int = 29
    override fun sout1() {
        super.sout1()
    }
}

/*
    default
    29
    进程已结束,退出代码0
*/

4. 接口继承

一个接口可以从其他接口进行继承,从而提供父类的成员

interface Named {
    val name: String
}

// 继承Named接口
interface Person : Named {
    val firstName: String
    val lastName: String
    // 重写属性
    override val name: String get() = "$firstName $lastName"
}

// 实现Person接口
data class Employee(
    // 不必实现name    override val firstName: String,
    override val lastName: String,
    val position: Position
) : Person

5. super覆盖冲突解决

实现多个接口的时候,我们可能会遇到同一个方法结成多个实现的问题

我们的解决方式和继承一样,使用super<interface>.funciton()来实现

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

在上述代码中,接口A和B都定义了foo()bar()方法,两者都实现了foo(),但是只有B实现了bar(),因为C是实现了A,所以一定要重写bar()方法。而D实现了A和B,所以需要两个方法都重写,可以使用super进行选择

6. 函数式接口

在Kotlin中,如果一个接口有且仅有一个抽象成员,可以有多个非抽象成员,那么我们可以使用函数式接口来缩短代码,使代码更加简洁

例如有这样一个接口

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

如果我们使用最原本的方法来实现这个接口中的accept()方法,需要这样写

// 创建一个类的实例
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

但是,因为这个接口中,有且仅有一个抽象成员,那么可以使用函数式接口来缩短代码量

// 通过 lambda 表达式创建一个实例,这样得到的isEven对象具有重写好的accept方法
val isEven = IntPredicate { it % 2 == 0 }

使用方式也和普通对象一样

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

val isEven = IntPredicate { it % 2 == 0 }

fun main() {
   println("Is 7 even? - ${isEven.accept(7)}")
}

8、更多的Kotlin内容

由于篇幅限制,本章仅仅讲述Kotlin的面向对象,之后将补充Kotlin的重点知识!