Kotlin是一种现代编程语言,可编译为Java字节码。 它是免费的, 开放 源码 ,并承诺使编码为Android更有趣。
在上一篇文章中 ,您了解了有关Kotlin属性的更多信息,例如后期初始化,扩展和内联属性。 不仅如此,您还了解了Kotlin中的高级类,例如数据,枚举,嵌套和密封类。
在本文中,您将通过学习抽象类,接口和继承来继续学习Kotlin中的面向对象编程。 作为奖励,您还将学习类型别名。
1.抽象类
Kotlin支持抽象类,就像Java一样,这些类是您从不打算从中创建对象的类。 没有一些具体的(非抽象的)子类,您可以从中实例化对象,因此抽象类是不完整的或无用的。 抽象类的具体子类实现了抽象类中定义的所有方法和属性,否则子类也是抽象类!
我们使用abstract
修饰符(类似于Java)创建一个抽象类。
abstract class Employee (val firstName: String, val lastName: String) {
abstract fun earnings(): Double
}
请注意,并非所有成员都必须是抽象的。 换句话说,我们可以在抽象类中使用方法默认实现。
abstract class Employee (val firstName: String, val lastName: String) {
// ...
fun fullName(): String {
return lastName + " " + firstName;
}
}
在这里,我们在抽象类Employee
创建了非抽象函数fullName()
。 具体的类(抽象类的子类)可以覆盖抽象方法的默认实现,但前提是该方法指定了open
修饰符(稍后您将详细了解)。
我们还可以将状态存储在抽象类中。
abstract class Employee (val firstName: String, val lastName: String) {
// ...
val propFoo: String = "bla bla"
}
即使抽象类没有定义任何方法,我们也需要在实例化它之前创建一个子类,如下例所示。
class Programmer(firstName: String, lastName: String) : Employee(firstName, lastName) {
override fun earnings(): Double {
// calculate earnings
}
}
我们的Programmer
类扩展了Employee
抽象类。 在我们Kotlin使用一个单一的冒号( :
)代替了Java extends
关键字来扩展类或实现一个接口。
然后,我们可以创建类型为Programmer
的对象,并在其自身的类或超类(基类)中调用方法。
val programmer = Programmer("Chike", "Mgbemena")
println(programmer.fullName()) // "Mgbemena Chike"
让您惊讶的一件事是,我们能够用var
(可变)覆盖val
(不可变)属性。
open class BaseA (open val baseProp: String) {
}
class DerivedA : BaseA("") {
private var derivedProp: String = ""
override var baseProp: String
get() = derivedProp
set(value) {
derivedProp = value
}
}
确保您明智地使用此功能! 请注意,我们不能相反-用val
覆盖var
属性。
2.接口
接口只是相关方法的集合,通常使您能够告诉对象该做什么以及默认情况下该如何做。 (接口中的默认方法是Java 8中新增的功能。)换句话说,接口是实现类必须遵守的契约。
接口是使用Kotlin中的interface
关键字定义的(类似于Java)。
class Result
class Student
interface StudentRepository {
fun getById(id: Long): Student
fun getResultsById(id: Long): List<Result>
}
在上面的代码中,我们声明了StudentRepository
接口。 此接口包含两个抽象方法: getById()
和getResultsById()
。 请注意,在接口方法中包含abstract
关键字是多余的,因为它们已经隐式抽象了。
没有一个或多个实现者的接口是无用的,因此让我们创建一个将实现该接口的类。
class StudentLocalDataSource : StudentRepository {
override fun getResults(id: Long): List<Result> {
// do implementation
}
override fun getById(id: Long): Student {
// do implementation
}
}
在这里,我们创建了一个类StudentLocalDataSource
实现了StudentRepository
接口。
我们使用override
修饰符标记要从接口或超类重新定义的方法和属性,这类似于Java中的@Override
注释。
请注意Kotlin中的以下其他接口规则:
- 一个类可以实现任意数量的接口,但是只能扩展一个类(类似于Java)。
- 与Java不同,
override
修饰符在Kotlin中是强制性的。 - 除了方法,我们还可以在Kotlin接口中声明属性。
- Kotlin接口方法可以具有默认实现(类似于Java 8)。
让我们来看一个带有默认实现的接口方法的示例。
interface StudentRepository {
// ...
fun delete(student: Student) {
// do implementation
}
}
在前面的代码中,我们添加了一个具有默认实现的新方法delete()
(尽管我没有为演示目的添加实际的实现代码)。
如果需要,我们还可以自由重写默认实现。
class StudentLocalDataSource : StudentRepository {
// ...
override fun delete(student: Student) {
// do implementation
}
}
如前所述,Kotlin接口可以具有属性,但是请注意,它不能维护状态。 (但是,请记住抽象类可以维护状态。)因此,下面的带有属性声明的接口定义将起作用。
interface StudentRepository {
val propFoo: Boolean // will work
// ...
}
但是,如果我们尝试通过为属性分配值来向接口添加某些状态,则它将不起作用。
interface StudentRepository {
val propFoo: Boolean = true // Error: Property initializers are not allowed in interfaces
// ..
}
但是,Kotlin中的接口属性可以具有getter和setter方法(尽管如果该属性是可变的,则只能使用后者)。 还请注意,接口中的属性不能具有后备字段。
interface StudentRepository {
var propFoo: Boolean
get() = true
set(value) {
if (value) {
// do something
}
}
// ...
}
我们还可以根据需要重写接口属性,以重新定义它。
class StudentLocalDataSource : StudentRepository {
// ...
override var propFoo: Boolean
get() = false
set(value) {
if (value) {
}
}
}
让我们看一下我们有一个用相同的方法签名实现多个接口的类的情况。 类如何确定要调用的接口方法?
interface InterfaceA {
fun funD() {}
}
interface InterfaceB {
fun funD() {}
}
在这里,我们有两个接口,它们的方法具有相同的签名funD()
。 让我们创建一个实现这两个接口并覆盖funD()
方法的类。
class classA : InterfaceA, InterfaceB {
override fun funD() {
super.funD() // Error: Many supertypes available, please specify the one you mean in angle brackets, e.g. 'super<Foo>'
}
}
编译器对于调用super.funD()
方法感到困惑,因为该类实现的两个接口具有相同的方法签名。
为了解决此问题,我们将要为其调用方法的接口名称放在尖括号<InterfaceName>
。 (IntelliJ IDEA或Android Studio在出现时会提示您解决此问题。)
class classA : InterfaceA, InterfaceB {
override fun funD() {
super<InterfaceA>.funD()
}
}
在这里,我们将调用InterfaceA
的funD()
方法。 问题解决了!
3.继承
通过获取现有类(超类)的成员并可能重新定义其默认实现来创建新的类(子类)。 这种机制在面向对象编程(OOP)中称为继承 。 使Kotlin如此出色的原因之一是它同时包含了OOP和函数式编程范式-全部都用一种语言编写。
Kotlin中所有类的基类为Any
。
class Person : Any {
}
Any
类型等效于Java中的Object
类型。
public open class Any {
public open operator fun equals(other: Any?): Boolean
public open fun hashCode(): Int
public open fun toString(): String
}
Any
类型包含以下成员: equals()
, hashcode()
和toString()
方法(类似于Java)。
我们的类不需要显式扩展此类型。 如果未明确指定新类扩展到哪个类,则该类将隐式扩展Any
。 出于这个原因,您通常不需要在代码中包括: Any
我们在上面的代码中包括了这些用于演示。
现在让我们研究在Kotlin中创建类时要牢记的继承性。
class Student {
}
class GraduateStudent : Student() {
}
在上面的代码中, GraduateStudent
类扩展了超类Student
。 但是此代码无法编译。 为什么? 因为类和方法在Kotlin中是默认的final
。 换句话说,它们默认情况下无法扩展-与Java中默认打开类和方法的情况不同。
最佳软件工程实践建议您默认开始将类和方法final
类,即,如果它们不是专门用于子类中的重新定义或覆盖。 Kotlin团队(JetBrains)在开发这种现代语言时应用了这种编码原理和更多开发最佳实践。
为了允许从超类创建子类,我们必须使用open
修饰符显式标记超类。 此修饰符也适用于应被子类覆盖的任何超类属性或方法。
open class Student {
}
我们只需将open
修饰符放在class
关键字之前。 现在,我们指示编译器允许我们的Student
类打开以进行扩展。
如前所述,默认情况下,Kotlin类的成员也是最终的。
open class Student {
open fun schoolFees(): BigDecimal {
// do implementation
}
}
在前面的代码中,我们将schoolFees
函数标记为open
,以便子类可以覆盖它。
open class GraduateStudent : Student() {
override fun schoolFees(): BigDecimal {
return super.schoolFees() + calculateSchoolFees()
}
private fun calculateSchoolFees(): BigDecimal {
// calculate and return school fees
}
}
在这里, GraduateStudent
类将override
超类Student
的open schoolFees
函数,方法是在fun
关键字之前添加override
修饰符。 请注意,如果您覆盖超类或接口的成员,则默认情况下,覆盖的成员也将处于open
,如下例所示:
class ComputerScienceStudent : GraduateStudent() {
override fun schoolFees(): BigDecimal {
return super.schoolFees() + calculateSchoolFess()
}
private fun calculateSchoolFess(): BigDecimal {
// calculate and return school fees
}
}
即使我们没有标注schoolFees()
的方法GraduateStudent
与类open
修改,我们仍然可以覆盖它,就像我们在做ComputerScienceStudent
类。 为了防止这种情况,我们必须将覆盖的成员标记为final
。
如果我们的超类具有这样的主要构造函数:
open class Student(val firstName: String, val lastName: String) {
// ...
}
然后,任何子类都必须调用超类的主要构造函数。
open class GraduateStudent(firstName: String, lastName: String) : Student(firstName, lastName) {
// ...
}
我们可以像往常一样简单地创建GraduateStudent
类的对象:
val graduateStudent = GraduateStudent("Jon", "Snow")
println(graduateStudent.firstName) // Jon
如果子类要从其辅助构造函数调用超类构造函数,则可以使用super
关键字(类似于在Java中调用超类构造函数的方式)。
open class GraduateStudent : Student {
// ...
private var thesis: String = ""
constructor(firstName: String, lastName: String, thesis: String) : super(firstName, lastName) {
this.thesis = thesis
}
}
如果您需要关于Kotlin中的类构造函数的复习,请访问我的类和对象文章。
4.奖励:别名类型
我们在Kotlin中可以做的另一件事是给类型赋予别名。
让我们来看一个例子。
data class Person(val firstName: String, val lastName: String, val age: Int)
在上面的类中,我们可以使用Kotlin中的typealias
修饰符为Person
属性的别名分配String
和Int
类型。 此修饰符用于在Kotlin中创建任何类型的别名,包括您创建的别名。
typealias Name = String
typealias Age = Int
data class Person(val firstName: Name, val lastName: Name, val age: Age)
如您所见,我们分别为String
和Int
类型创建了一个别名Name
和Age
。 现在,我们已经将firstName
和lastName
属性类型替换为别名Name
,并且将Int
类型替换为Age
别名。 请注意,我们没有创建任何新类型,而是为这些类型创建了别名。
当您想为Kotlin代码库中的类型提供更好的含义或语义时,这些方法会很方便。 因此,请明智地使用它们!
结论
在本教程中,您了解了有关Kotlin中面向对象编程的更多信息。 我们介绍了以下内容:
如果您已经通过Kotlin From Scratch系列学习了Kotlin ,请确保您已键入看到的代码并在IDE上运行它。 真正掌握正在学习的新编程语言(或任何编程概念)的一个重要技巧是确保不仅阅读阅读学习资源或指南,而且还要键入实际的代码并运行它!
在Kotlin From Scratch系列的下一个教程中,将向您介绍Kotlin中的异常处理。 再见!