Kotlin继承与多态
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
方法。
重写规则
属性重写
- 属性类型:子类重写的属性类型必须与父类中被重写属性的类型相同或者是它的子类型。例如:
open class Shape {
open val area: Number = 0.0
}
class Circle : Shape() {
override val area: Double = 3.14 * 2 * 2
}
这里Circle
类重写的area
属性类型Double
是父类Shape
中area
属性类型Number
的子类型,符合重写规则。
2. 属性修饰符:子类重写属性时,可以将val
改为var
,但反之不行。因为var
属性既支持读操作又支持写操作,而val
属性仅支持读操作,所以将val
改为var
是一种更宽松的定义,符合重写规则。例如:
open class Parent {
open val value: Int = 10
}
class Child : Parent() {
override var value: Int = 20
}
方法重写
- 方法签名:重写的方法必须与父类中被重写方法具有相同的方法名、参数列表和返回类型(或返回类型是父类方法返回类型的子类型,即协变返回类型)。例如:
open class Vehicle {
open fun startEngine(): String {
return "Engine started"
}
}
class Car : Vehicle() {
override fun startEngine(): String {
return "Car engine started"
}
}
- 访问修饰符:子类重写方法的访问修饰符不能比父类中被重写方法的访问修饰符更严格。例如,如果父类方法是
protected
,子类重写方法可以是protected
或public
,但不能是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
类的主构造函数,传递了name
和age
参数。
次构造函数
如果子类有次构造函数,且父类有主构造函数,次构造函数也需要通过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中,多态主要通过继承和重写方法来实现。
基于继承的多态
- 运行时多态:通过重写父类方法,在运行时根据对象的实际类型来决定调用哪个方法。例如:
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
类型的列表,列表中可以包含Circle
和Rectangle
等Shape
的子类对象。在遍历列表调用draw
方法时,会根据对象的实际类型(Circle
或Rectangle
)调用相应的重写方法,这就是运行时多态。
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
函数,这就是编译时多态。
抽象类与接口中的继承和多态
抽象类
- 抽象类的定义:抽象类是一种不能被实例化的类,它通常包含抽象方法。抽象方法是没有方法体的方法,必须在子类中被重写。使用
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. 抽象类的继承特点:抽象类可以有构造函数,子类在继承抽象类时,同样需要在构造函数中调用父类的构造函数。抽象类可以包含属性、方法(包括抽象和非抽象方法),子类继承抽象类后,必须实现抽象类中的抽象方法,否则子类也必须声明为抽象类。
接口
- 接口的定义:接口是一种特殊的抽象类型,它只包含抽象方法和属性的声明(从Kotlin 1.1开始,接口也可以包含默认实现的方法)。接口使用
interface
关键字定义。例如:
interface Drawable {
fun draw()
}
class Square : Drawable {
override fun draw() {
println("Drawing a square")
}
}
在上述代码中,Drawable
接口定义了一个抽象方法draw
,Square
类实现了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
类同时实现了Drawable
和Printable
接口,需要实现这两个接口中的所有抽象方法。
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
关键字检查对象是否是Apple
或Banana
类型,并输出相应的信息。
类型转换
- 安全转换:使用
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
类型,如果转换成功则执行相应的操作,否则car
为null
,不会引发运行时错误。
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中,类型参数的变型(协变和逆变)对于处理继承和多态关系时的类型兼容性非常重要。
协变
- 定义:协变是指当
A
是B
的子类型时,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>
,由于Apple
是Fruit
的子类型,Producer<Apple>
也是Producer<Fruit>
的子类型,所以可以将AppleProducer
和FruitProducer
的实例放入List<Producer<Fruit>>
中。
2. 限制:协变类型参数只能用于输出位置,即只能作为函数的返回类型,不能作为函数的参数类型。
逆变
- 定义:逆变是指当
A
是B
的子类型时,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
声明为逆变。由于Apple
是Fruit
的子类型,Consumer<Fruit>
是Consumer<Apple>
的子类型,所以FruitConsumer
和AppleConsumer
的实例都可以放入List<Consumer<Apple>>
中。
2. 限制:逆变类型参数只能用于输入位置,即只能作为函数的参数类型,不能作为函数的返回类型。
总结
Kotlin的继承与多态机制为开发者提供了强大的代码复用和行为定制能力。通过合理运用继承、重写、抽象类、接口以及类型变型等特性,能够构建出更加灵活、可维护和可扩展的软件系统。在实际开发中,需要根据具体的业务需求和设计原则,谨慎选择和使用这些特性,以避免代码的复杂性和潜在的错误。同时,理解和掌握这些特性对于深入学习Kotlin以及进行高效的Kotlin编程至关重要。无论是开发小型应用还是大型企业级项目,继承与多态的正确运用都能显著提升代码的质量和开发效率。