面向对象编程01

与Java一样,Kotlin中使用class关键字来声明类, 例如

1
class Person { /*...*/ }

当一个类不包含任何属性和方法时,可以简写为

1
class Person

主构造函数

Kotlin中,一个类可以有1个主构造函数,以及一个或者多个次构造函数。

主构造函数使用关键字**constructor**关键字声明,紧跟在类名后面,格式如下

1
class Person constructor(firstName: String) { /*...*/ }

如果主构造函数没有任何注解或修饰符,可以把constructor关键字省略掉,直接写作

1
class Person(firstName: String) { /*...*/ }

但是如果主构造函数前有注解或修饰符时,constructor关键字不能省略

1
class Customer public @Inject constructor(name: String) { /*...*/ }

主构造函数里不能包含任何代码块,初始语句可以放在init代码块里面,并且一个类可以包含多个init代码块,按照他们在类中出现的顺序依次执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class InitOrderDemo(name: String) {
    val firstProperty = "First property: $name".also(::println)

    init {
        println("First initializer block that prints ${name}")
    }

    val secondProperty = "Second property: ${name.length}".also(::println)

    init {
        println("Second initializer block that prints ${name.length}")
    }
}

主构造函数里的参数即可以在init块里使用,也可以在类属性初始化时使用,比如

1
2
3
class Customer(name: String) {
    val customerKey = name.uppercase()
}

定义类的属性时,只需要在主构造函数的参数前面加上val或者var即可

1
class Person(val firstName: String, val lastName: String, var age: Int)

次构造函数

如果需要多种方法来初始化类,则可以创建次构造函数,每个构造函数都是一个名称为constructor 的函数。每个次构造函数都必须使用thissuper关键字来调用另一个(主或次)构造函数,就好像它是一个函数一样(以便每个实例构造最终都调用该主构造函数)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class Person(val name: String, var age: Int) {

    constructor(name: String) : this(name, 0)

    constructor(yearOfBirth: Int, name: String) : this(name, 2018 - yearOfBirth)
}

val p1 = Person("Jim", 12)
val p2 = Person("Jack")
val p3 = Person(1988, "Sam")

创建类的对象

直接调用构造函数即可调用创建类对象,跟Java不同的是,不需要使用new关键字

1
2
3
val invoice = Invoice()

val customer = Customer("Joe Smith")

继承

Kotlin中每个未明确声明父类的类都从Any类继承,Any是类层次结构的根(类似于 Python 中的Object)通过继承Any类,每个类自动具有以下函数:

  • toString() 返回对象的字符串表示形式,类似于 Python 中的 __str__()(默认实现相当有趣,因为它仅返回类名与类似于对象 ID 的名称)

  • equals(x) 检查此对象是否与任何类的某个其他对象x相同(默认情况下,它仅检查该对象是否与x是相同的对象,类似 Python 中的 is, 但可以被子类覆盖以进行属性值的自定义比较)

  • hashCode() 返回一个整数,哈希表可以使用该整数并用于简化复杂的相等比较(根据equals() 相等的对象必须具有相同的哈希码,因此,如果两个对象的哈希码不同,则这些对象不能相等)

    1
    2
    3
    4
    5
    6
    7
    
    class Person {
    
    	var name = "Anne"
    
    	var age = 32
    
    }
    

    与 Python 相反,在类内部直接声明属性不会创建类级别的属性,而是创建实例级别的属性:Person的每个实例都有它自己的nameage。它们的值将在每个实例中分别以Anne32生成,但是每个实例中的值可以独立于其他实例进行修改。在Kotlin中要使用类属性,需要使用"伴生对象"(objects-and-companion-objects)

默认情况下,Kotlin中每个都都是final的,不能被继承,要让一个类可以被继承,需要使用open关键字

1
2
3
open class Base(p: Int)

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

方法覆写

方法覆写时,除了继承的类必须是open之类,覆写的方法也必须是open才可以,例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
open class Shape {
    open fun draw() { /*...*/
    }

    fun fill() { /*...*/
    }
}

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

    // 编译器会报错
    // override fun fill() {}
}

上面的例子中,只有draw方法才可以被覆写。

覆写的方法自身是open的,想让覆写的方法自身不被它的子类覆写,可以加上final

1
2
3
open class Rectangle() : Shape() {
    final override fun draw() { /*...*/ }
}

属性覆写

跟方法覆写一样,属性覆写也是类似的, 注释覆写的属性类型必须和父类的兼容

1
2
3
4
5
6
7
open class Shape {
    open val vertexCount: Int = 0
}

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

甚至可以使用val覆写var属性,反之亦然。

另外,也可以直接在主构造函数里使用override关键字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
interface Shape {
    val vertexCount: Int
}

class Rectangle(override val vertexCount: Int = 4) : Shape // Always has 4 vertices

class Polygon : Shape {
    override var vertexCount: Int = 0  // Can be set to any number later
}

派生类的执行顺序

首先是父类先初始化,然后才是子类。子类里覆写父类的属性或也要在子类初始化完成后才正式可用,假如在父类里调用了被覆写的属性,可能会导致错误的结果。

因此在设计父类的时候,应当避免在构造参数,属性初始化,或者init模块中,使用带open修饰的成员。

并且

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
open class Base(val name: String) {

    init { println("Initializing a base class") }

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

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

    init { println("Initializing a derived class") }

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

fun main() {
    println("Constructing the derived class(\"hello\", \"world\")")
    Derived("hello", "world")
}

输出如下

1
2
3
4
5
6
Constructing the derived class("hello", "world")
Argument for the base class: Hello
Initializing a base class
Initializing size in the base class: 5
Initializing a derived class
Initializing size in the derived class: 10

从上面的输出可以看出,size属性签名有两个值。

调用父类的方法

在派生类中,如果想调用父类的方法,使用super关键字即可

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
open class Rectangle {
    open fun draw() { println("Drawing a rectangle") }
    val borderColor: String get() = "black"
}

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

inner class里,想要访问外部类的父类方法时,可以使用super关键字加上外部类的类名来实现,类似super@OuterClassName

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class FilledRectangle: Rectangle() {
    override fun draw() {
        val filler = Filler()
        filler.drawAndFill()
    }

    inner class Filler {
        fun fill() { println("Filling") }
        fun drawAndFill() {
             // 调用外部类FilledRectangle的父类Rectangle的draw()方法
            super@FilledRectangle.draw() // Calls Rectangle's implementation of draw()
            fill()
            println("Drawn a filled rectangle with color ${super@FilledRectangle.borderColor}") // Uses Rectangle's implementation of borderColor's get()
        }
    }
}

覆写顺序

当一个类同时实现了多个相同的方法时,必须明确指定它要调用的是哪个的实现(使用super<Base>的形式) 或者自己重写这个方法。

例如

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
open class Rectangle {
    open fun draw() { /* ... */ }
}

interface Polygon {
    fun draw() { /* ... */ } // interface members are 'open' by default
}

class Square() : Rectangle(), Polygon {
    // The compiler requires draw() to be overridden:
    override fun draw() {
        super<Rectangle>.draw() // call to Rectangle.draw()
        super<Polygon>.draw() // call to Polygon.draw()
    }
}

属性的getter/setter函数

Kotlin中类属性实际上是一个 幕后字段(对象内部为隐藏变量的种类)与两个访问器函数:一个用于获取变量的值,另一个用于设置值。访问函数在每次调用属性时都会执行,类似计算属性

可以重写一个或两个访问器(未被重写的访问器会自动获得默认行为,即直接返回或设置幕后字段)。

在访问器内部,可以使用field引用幕后字段。Setter访问器必须采用参数 value,这是赋值给属性的值。

一个Getter 主体可以是一个以=开头的单行表达式,也可以是一个花括号括起来的更复杂的主体,而Setter主体通常包括一个赋值,因此必须括在花括号中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.github.crazygit.koltlin.demo

class Person(age: Int) {

    var age = 0
        set(value) {
            if (value < 0) throw IllegalArgumentException(
                    "Age cannot be negative"
            )
            field = value
        }

    init {
        this.age = age
    }

    //创建实际上没有幕后字段的属性,而只需引用另一个属性
    val isNewborn
        get() = age == 0
}

fun main() {
    val p = Person(10)
    println(p.age)
    println(p.isNewborn)
}

请注意,尽管由于使用val声明了isNewborn是一个只读属性(在这种情况下,即使没有提供Setter),但它的值仍然可以更改,因为它是从可变属性中读取的——只是无法给该属性赋值。另外,请注意,属性类型是根据Getter的返回值推断出来的。

访问器前面的缩进是由于约定,像Kotlin的其他地方一样,它没有语法意义。编译器可以知道哪些访问器属于哪些属性,因为访问器的唯一合法位置是在属性声明之后(并且最多可以有一个 Getter 与一个 Setter——因此无法拆分属性声明与访问器声明。然而,访问器的顺序并不重要

Setter函数中的一个坑

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Person(age: Int) {
    var age = age
        set(value) {
            println("call age setter with value: $value")
            if (value < 0) throw IllegalArgumentException(
                    "Age cannot be negative"
            )
            field = value
        }
}


fun main() {
    val p = Person(-10) //初始为负数,但是不会报错, setter中的println语句并不会执行
    print(p.age)
}

上面的类虽然设置了ageSetter,但是初始化并未调用Setter逻辑,而是直接设置了幕后字段。必须用下面的写法,通过init模块才能让初始化调用setter

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class Person(age: Int) {
    var age: Int = 0
        set(value) {
            println("call age setter with value: $value")
            if (value < 0) throw IllegalArgumentException(
                    "Age cannot be negative"
            )
            field = value
        }

    init {
        this.age = age
    }
}

接口

实现接口和继承类使用的是同样的关键字:,另外接口的名称后面不用加上括号,因为它没有主构造函数需要实现.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.github.crazygit.koltlin.demo

interface Study {
    // 接口中同样可以声明属性
    val prop: Int // abstract
    fun readBooks()
    fun doHomework() {
        println("do homework default implementation")
    }
}

与Java1.8一样,Kotlin的接口允许拥有默认实现,在实现接口时,拥有默认实现的函数可以自由选择实现或者不实现,不实现时自动使用默认的实现逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.github.crazygit.koltlin.demo

class Student(val sno: String, val grade: String, name: String, age: Int) : Person(name, age), Study {
    override val prop: Int = 29

    override fun readBooks() {
        println("$name is reading books")
    }

    override fun doHomework() {
        println("$name is doing homework")
    }
}

接口直接也是可以相互继承的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
interface Named {
    val name: String
}

interface Person : Named {
    val firstName: String
    val lastName: String

    override val name: String get() = "$firstName $lastName"
}

data class Employee(
    // implementing 'name' is not required
    override val firstName: String,
    override val lastName: String,
    val position: Position
) : Person

当实现多个具有相同函数的接口时,需要显示指定调用哪个的接口

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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调用接口里的方法
        super<A>.foo()
        super<B>.foo()
    }

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

Functional (SAM) interfaces

一个接口只有一个抽象方法时,这个接口被叫做功能性接口(functional interface),或者Single Abstract Method (SAM) interface. 它可以有几个非抽象的属性。

使用fun关键字声明功能性接口

1
2
3
fun interface KRunnable {
   fun invoke()
}

SAM接口有个语法,可以使用Lambda表达式来简化接口的实现

比如有这么一个接口

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

不使用SAM语法,实现这个接口的写法是

1
2
3
4
5
6
// Creating an instance of a class
val isEven = object : IntPredicate {
   override fun accept(i: Int): Boolean {
       return i % 2 == 0
   }
}

使用SAM语法,可以直接简化为

1
2
// Creating an instance using lambda
val isEven = IntPredicate { it % 2 == 0 }

函数可见性修饰符

Java与Kotlin函数可见性修饰符对照表

数据类

当在一个类前面声明了data关键字时,就表明你希望这个类是一个数据类,Kotlin会根据主构造函数中的参数帮你将如下函数自动生成。

  • eaquals()/hashCode()
  • toString()
  • componentN()
  • copy()

另外数据类有一下几个必要条件

  1. 主构造函数只要要有一个属性

  2. 所有主构造属性的属性应该标记为var或者val

  3. 数据类不能是 abstract, open, sealed or inner类型

  4. 不允许覆写数据类的componentN()Copy()方法

常规类

1
2
3
4
5
6
7
8
class Phone(val brand: String, val price: Double)

fun main() {
    val phone1 = Phone("Apple", 6999.99)
    val phone2 = Phone("Apple", 6999.99)
    println(phone1)
    println("phone1 equals phone2: " + (phone1 == phone2))
}

输出为

1
2
com.github.crazygit.koltlin.demo.Phone@17c68925
phone1 equals phone2: false

使用数据类

1
2
3
4
5
6
7
8
data class Phone(val brand: String, val price: Double)

fun main() {
    val phone1 = Phone("Apple", 6999.99)
    val phone2 = Phone("Apple", 6999.99)
    println(phone1)
    println("phone1 equals phone2: " + (phone1 == phone2))
}

输出为

1
2
Phone(brand=Apple, price=6999.99)
phone1 equals phone2: true

Properties declared in the class body

在类里面声明属性时,是不会被自动添加到生成的toString(), equals(), hashCode()等方法里面的

1
2
3
4
5
6
7
8
data class Person1(val name: String, val age: Int)
data class Person2(val name: String) {
    val age: Int = 0
}

println(Person1("Jack", 10)) // Person1(name=Jack, age=10)
println(Person2("Jack")) // Person2(name=Jack)

使用copy()方法复制对象并修改部分属性

1
2
3
4
5
val jack = User(name = "Jack", age = 1)
val olderJack = jack.copy(age = 2)
println(jack)  // User(name=Jack, age=1)
// 复制对象的同时修改age属性
println(olderJack) // User(name=Jack, age=2)

Data classes and destructuring declarations

1
2
3
4
5
6
7
data class User(val name: String, val age: Int)

// componentN()方法的用法
val u = User("Jack", 10)
val (name, age) = u //注意写法
print(name) // Jack,相当于调用u.component1()
print(age)  // 10, 相当于调用u.component2()