MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Kotlin继承与多态

2023-02-266.7k 阅读

Kotlin继承基础

在Kotlin中,继承是面向对象编程的重要特性之一,它允许一个类(子类)继承另一个类(父类)的属性和方法,实现代码的复用与扩展。在Kotlin里,所有类默认继承于Any类,Any类是Kotlin类层次结构的根类,它提供了equals()hashCode()toString()等基本方法。

定义可继承的类

默认情况下,Kotlin中的类是final的,即不能被继承。如果希望一个类可以被继承,需要使用open关键字修饰该类。例如:

open class Animal {
    open var name: String = "Unnamed"
    open fun speak() {
        println("The animal makes a sound.")
    }
}

上述代码中,Animal类被声明为open,表示它可以被其他类继承。name属性和speak方法也被声明为open,这意味着子类可以重写它们。

继承类的定义

使用:来表示继承关系。例如,定义一个Dog类继承自Animal类:

class Dog : Animal() {
    override var name: String = "Doggy"
    override fun speak() {
        println("Woof! My name is $name")
    }
}

Dog类中,使用override关键字来重写父类Animal中的name属性和speak方法。

重写规则

属性重写

  1. 属性类型:子类重写的属性类型必须与父类中被重写属性的类型相同或者是它的子类型。例如:
open class Shape {
    open val area: Number = 0.0
}

class Circle : Shape() {
    override val area: Double = 3.14 * 2 * 2
}

这里Circle类重写的area属性类型Double是父类Shapearea属性类型Number的子类型,符合重写规则。 2. 属性修饰符:子类重写属性时,可以将val改为var,但反之不行。因为var属性既支持读操作又支持写操作,而val属性仅支持读操作,所以将val改为var是一种更宽松的定义,符合重写规则。例如:

open class Parent {
    open val value: Int = 10
}

class Child : Parent() {
    override var value: Int = 20
}

方法重写

  1. 方法签名:重写的方法必须与父类中被重写方法具有相同的方法名、参数列表和返回类型(或返回类型是父类方法返回类型的子类型,即协变返回类型)。例如:
open class Vehicle {
    open fun startEngine(): String {
        return "Engine started"
    }
}

class Car : Vehicle() {
    override fun startEngine(): String {
        return "Car engine started"
    }
}
  1. 访问修饰符:子类重写方法的访问修饰符不能比父类中被重写方法的访问修饰符更严格。例如,如果父类方法是protected,子类重写方法可以是protectedpublic,但不能是private

构造函数与继承

主构造函数

当子类继承父类时,如果父类有主构造函数,子类必须在其主构造函数中调用父类的主构造函数。例如:

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

class Student : Person {
    constructor(name: String, age: Int, val studentId: String) : super(name, age)
}

在上述代码中,Student类继承自Person类,Student类的主构造函数通过super关键字调用了Person类的主构造函数,传递了nameage参数。

次构造函数

如果子类有次构造函数,且父类有主构造函数,次构造函数也需要通过super关键字调用父类的主构造函数或者同一个类的其他构造函数。例如:

open class Employee(val name: String, val salary: Double)

class Manager : Employee {
    constructor(name: String, salary: Double, val department: String) : super(name, salary)
    constructor(name: String, salary: Double) : this(name, salary, "Unknown department")
}

这里Manager类有两个次构造函数,第一个次构造函数直接调用父类Employee的主构造函数,第二个次构造函数先调用了同一个类的第一个次构造函数。

Kotlin多态

多态是指同一个方法调用在不同的对象上会产生不同的行为。在Kotlin中,多态主要通过继承和重写方法来实现。

基于继承的多态

  1. 运行时多态:通过重写父类方法,在运行时根据对象的实际类型来决定调用哪个方法。例如:
open class Shape {
    open fun draw() {
        println("Drawing a shape")
    }
}

class Circle : Shape() {
    override fun draw() {
        println("Drawing a circle")
    }
}

class Rectangle : Shape() {
    override fun draw() {
        println("Drawing a rectangle")
    }
}

fun drawShapes(shapes: List<Shape>) {
    for (shape in shapes) {
        shape.draw()
    }
}

fun main() {
    val shapes = listOf(Circle(), Rectangle())
    drawShapes(shapes)
}

在上述代码中,drawShapes函数接受一个Shape类型的列表,列表中可以包含CircleRectangleShape的子类对象。在遍历列表调用draw方法时,会根据对象的实际类型(CircleRectangle)调用相应的重写方法,这就是运行时多态。 2. 编译时多态:在Kotlin中,编译时多态主要通过函数重载来实现。函数重载是指在同一个类中定义多个同名但参数列表不同的函数。例如:

class Calculator {
    fun add(a: Int, b: Int): Int {
        return a + b
    }

    fun add(a: Double, b: Double): Double {
        return a + b
    }
}

Calculator类中,定义了两个add函数,一个接受两个Int类型参数,另一个接受两个Double类型参数。编译器会根据调用时传入的参数类型来决定调用哪个add函数,这就是编译时多态。

抽象类与接口中的继承和多态

抽象类

  1. 抽象类的定义:抽象类是一种不能被实例化的类,它通常包含抽象方法。抽象方法是没有方法体的方法,必须在子类中被重写。使用abstract关键字来定义抽象类和抽象方法。例如:
abstract class AbstractShape {
    abstract fun area(): Double
    open fun draw() {
        println("Drawing an abstract shape")
    }
}

class Triangle : AbstractShape() {
    private val base: Double = 5.0
    private val height: Double = 3.0

    override fun area(): Double {
        return 0.5 * base * height
    }

    override fun draw() {
        println("Drawing a triangle")
    }
}

在上述代码中,AbstractShape类是抽象类,它包含一个抽象方法area和一个非抽象的draw方法。Triangle类继承自AbstractShape类,并实现了抽象方法area,同时重写了draw方法。 2. 抽象类的继承特点:抽象类可以有构造函数,子类在继承抽象类时,同样需要在构造函数中调用父类的构造函数。抽象类可以包含属性、方法(包括抽象和非抽象方法),子类继承抽象类后,必须实现抽象类中的抽象方法,否则子类也必须声明为抽象类。

接口

  1. 接口的定义:接口是一种特殊的抽象类型,它只包含抽象方法和属性的声明(从Kotlin 1.1开始,接口也可以包含默认实现的方法)。接口使用interface关键字定义。例如:
interface Drawable {
    fun draw()
}

class Square : Drawable {
    override fun draw() {
        println("Drawing a square")
    }
}

在上述代码中,Drawable接口定义了一个抽象方法drawSquare类实现了Drawable接口,并实现了draw方法。 2. 接口的多继承:Kotlin支持一个类实现多个接口。例如:

interface Printable {
    fun print()
}

class Document : Drawable, Printable {
    override fun draw() {
        println("Drawing a document")
    }

    override fun print() {
        println("Printing a document")
    }
}

Document类同时实现了DrawablePrintable接口,需要实现这两个接口中的所有抽象方法。 3. 接口中的默认方法:从Kotlin 1.1开始,接口可以包含有默认实现的方法。例如:

interface AnimalActions {
    fun eat()
    fun sleep() {
        println("The animal is sleeping")
    }
}

class Cat : AnimalActions {
    override fun eat() {
        println("The cat is eating")
    }
}

在上述代码中,AnimalActions接口的sleep方法有默认实现,Cat类实现了AnimalActions接口,只需要实现eat方法,对于sleep方法可以直接使用接口中的默认实现。

类型检查与转换

在实现继承和多态的过程中,有时需要检查对象的实际类型并进行类型转换。

is关键字

is关键字用于检查一个对象是否是某个类型的实例。例如:

open class Fruit
class Apple : Fruit()
class Banana : Fruit()

fun printFruitType(fruit: Fruit) {
    if (fruit is Apple) {
        println("It's an apple")
    } else if (fruit is Banana) {
        println("It's a banana")
    }
}

在上述代码中,printFruitType函数接受一个Fruit类型的对象,通过is关键字检查对象是否是AppleBanana类型,并输出相应的信息。

类型转换

  1. 安全转换:使用as?操作符进行安全类型转换,如果转换失败返回null。例如:
open class Vehicle
class Car : Vehicle()

fun drive(vehicle: Vehicle) {
    val car = vehicle as? Car
    car?.let {
        println("Driving a car")
    }
}

在上述代码中,vehicle被安全转换为Car类型,如果转换成功则执行相应的操作,否则carnull,不会引发运行时错误。 2. 不安全转换:使用as操作符进行不安全类型转换,如果转换失败会抛出ClassCastException异常。例如:

open class Shape
class Rectangle : Shape()

fun calculateArea(shape: Shape) {
    val rectangle = shape as Rectangle
    val area = rectangle.width * rectangle.height //假设Rectangle有width和height属性
    println("The area of the rectangle is $area")
}

如果传入calculateArea函数的shape对象实际上不是Rectangle类型,就会抛出ClassCastException异常,所以这种转换需要谨慎使用。

协变与逆变

在Kotlin中,类型参数的变型(协变和逆变)对于处理继承和多态关系时的类型兼容性非常重要。

协变

  1. 定义:协变是指当AB的子类型时,List<A>也是List<B>的子类型。在Kotlin中,使用out关键字来声明协变类型参数。例如:
interface Producer<out T> {
    fun produce(): T
}

class AppleProducer : Producer<Apple> {
    override fun produce(): Apple {
        return Apple()
    }
}

class FruitProducer : Producer<Fruit> {
    override fun produce(): Fruit {
        return Fruit()
    }
}

fun processProducers(producers: List<Producer<Fruit>>) {
    for (producer in producers) {
        val fruit = producer.produce()
        println("Produced a $fruit")
    }
}

fun main() {
    val appleProducer = AppleProducer()
    val fruitProducer = FruitProducer()
    val producers = listOf(appleProducer, fruitProducer)
    processProducers(producers)
}

在上述代码中,Producer接口的类型参数T使用out声明为协变。AppleProducer实现了Producer<Apple>FruitProducer实现了Producer<Fruit>,由于AppleFruit的子类型,Producer<Apple>也是Producer<Fruit>的子类型,所以可以将AppleProducerFruitProducer的实例放入List<Producer<Fruit>>中。 2. 限制:协变类型参数只能用于输出位置,即只能作为函数的返回类型,不能作为函数的参数类型。

逆变

  1. 定义:逆变是指当AB的子类型时,Consumer<B>Consumer<A>的子类型。在Kotlin中,使用in关键字来声明逆变类型参数。例如:
interface Consumer<in T> {
    fun consume(item: T)
}

class FruitConsumer : Consumer<Fruit> {
    override fun consume(item: Fruit) {
        println("Consuming a $item")
    }
}

class AppleConsumer : Consumer<Apple> {
    override fun consume(item: Apple) {
        println("Consuming an apple")
    }
}

fun feedConsumers(consumers: List<Consumer<Apple>>) {
    val apple = Apple()
    for (consumer in consumers) {
        consumer.consume(apple)
    }
}

fun main() {
    val fruitConsumer = FruitConsumer()
    val appleConsumer = AppleConsumer()
    val consumers = listOf(fruitConsumer, appleConsumer)
    feedConsumers(consumers)
}

在上述代码中,Consumer接口的类型参数T使用in声明为逆变。由于AppleFruit的子类型,Consumer<Fruit>Consumer<Apple>的子类型,所以FruitConsumerAppleConsumer的实例都可以放入List<Consumer<Apple>>中。 2. 限制:逆变类型参数只能用于输入位置,即只能作为函数的参数类型,不能作为函数的返回类型。

总结

Kotlin的继承与多态机制为开发者提供了强大的代码复用和行为定制能力。通过合理运用继承、重写、抽象类、接口以及类型变型等特性,能够构建出更加灵活、可维护和可扩展的软件系统。在实际开发中,需要根据具体的业务需求和设计原则,谨慎选择和使用这些特性,以避免代码的复杂性和潜在的错误。同时,理解和掌握这些特性对于深入学习Kotlin以及进行高效的Kotlin编程至关重要。无论是开发小型应用还是大型企业级项目,继承与多态的正确运用都能显著提升代码的质量和开发效率。