+3

Lớp và kế thừa trong Kotlin

Lớp

Lớp trong kotlin được khai báo sử dụng từ khóa class

class Invoice {
}

Khai báo lớp bao gồm tên lớp, tiêu đề của lớp(định nghĩa kiểu của tham số, constructor chính,...) và thân lớp bao quanh bởi dấu ngoặc nhọn. Cả tiêu đề và thân của lớp đều không bắt buộc, nếu lớp không có thân thì có thể bỏ ngoặc nhọn

class Empty

Những constructor

Một lớp trong kotlin có thể có một contructor chính và một hoặc nhiều constructor phụ. Contructor chính là một phần của tiêu đề lớp: nó đứng phía sau tên lớp (và kiểu các tham số tùy chọn)

class Person constructor(firstName: String){
}

Nếu constructor chính không có bất kỳ annotations hoặc định nghĩa quyền truy cập (public, private, protected, internal), từ khóa constructor có thể bỏ qua

class Person(firstName: String){
}

Các constructor chính có thể không chứa bất kỳ đoạn mã nào. Mã khởi tạo có thể đặt trong các khối khởi tạo cùng với từ khóa init. Trong quá trình khởi tạo instance, các khối khởi tạo được thực hiện theo thứ tự như chúng xuất hiện trong thân lớp, được xen kẽ với sự khởi tạo thuộc tính

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}")
    }
}

fun main(args: Array<String>) {
    InitOrderDemo("hello")
}
// Kết quả chạy là 
//First property: hello
//First initializer block that prints hello
//Second property: 5
//Second initializer block that prints 5

Chú ý rằng, tham số của constructor chính có thể được sử dụng trong khối khởi tạo. Nó cũng có thể sử dụng trong khai báo khởi tạo thuộc tính trong thân lớp:

class Customer(name: String) {
    val customerKey = name.toUpperCase()
}

Trên thực tế, để khai báo các thuộc tính và khởi tạo từ constructor chính, chúng ta sử dụng cú pháp ngắn gọn:

class Person(val firstName: String, val lastName: String, var age: Int) {
    // ...
}

Cũng giống như các thuộc tính thông thường, các thuộc tính được khai báo trong constructor chính có thể bị biến đổi(var) hoặc chỉ đọc(val). Nếu constructor có các annotation hoặc định nghĩa quyền truy cập thì cần phải có từ khóa constructor và định nghĩa quyền truy cập phải đứng trước

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

Tìm hiểu thêm về quyền truy cập

Những constructor phụ

Lớp cũng có thể khai báo các constructor phụ, có tiền tố là constructor:

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

Nếu một lớp có một constructor chính, mỗi constructor phụ cần phải đại diện cho các constructor chính, trực tiếp hoặc gián tiếp thông qua những constructor khác. Đại diện cho những constructor khác của cùng một lớp bằng cách sử dụng từ khóa this

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

Chú ý rằng mã trong các khối khởi tạo sẽ hiệu quả khi thành một phần của constructor chính. Đại diện cho constructor chính xảy ra như là câu lệnh đầu tiên của một constructor phụ, do đó mã trong khối khởi tạo được thực hiện trước trong thân constructor phụ. Thậm chí lớp không có constructor chính, đại diện vẫn chạy ngầm và các khối khởi tạo vẫn thực thi:

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

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

fun main(args: Array<String>) {
    Constructors(1)
}
//Init block
//Constructor

Nếu một lớp không trừu tượng không khai báo bất kỳ constructor nào (chính hoặc phụ), sẽ có một constructor chính không đối số được tạo ra. Quyền truy cập của constructor sẽ là public. Nếu bạn không muốn lớp của bạn có một public constructor, bạn cần phải khai báo một private constructor rỗng.

class DontCreateMe private constructor() {
}

Trên JVM, nếu tất cả các tham số của constructor chính có giá trị mặc định, trình biên dịch sẽ tạo thêm một parammeterless constructor, nó sẽ sử dụng các giá trị mặc định. Điều này làm cho việc sử dụng Kotlin trở nên dễ dàng hơn với các thư viện như Jackson hoặc JPA tạo instance lớp thông qua parammeterless constructor.

class Customer(val customerName: String = "")

Tạo các instance của các lớp

Để tạo một instance của một lớp, ta gọi constructor như thể nó là một hàm thông thường:

val invoice = Invoice()
val customer = Customer("Joe Smith")

Chú ý rằng kotlin không có từ khóa new

Các thành phần của lớp

Các lớp có thể chứa:

  • constructor và initializer block
  • Hàm
  • Thuộc tính
  • Nested and Inner Classes
  • Đối tượng

Kế thừa

Tất cả các lớp trong kotlin đều có chung lớp cha Any, đó là super mặc định cho một lớp mà không khai báo supertype nào:

class Example // Kế thừa từ Any

Để khai báo một lớp cha rõ ràng, chúng ta đặt type sau dấu hai chấm trong tiêu đề của lớp:

open class Base(p: Int)

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

Nếu một lớp có một constructor chính, base type có thể (và phải) được khởi tạo ngay tại đấy, sử dụng các tham số của constructor chính. Nếu lớp không có constructor chính thì mỗi constructor phụ phải khởi tạo base type bằng cách sử dụng từ khóa super hoặc ủy thác cho một constructor khác làm điều này. Chú ý rằng trong trường hợp này, các constructor phụ khác nhau có thể gọi các constructor khác nhau của một loại base type:

class MyView : View {
    constructor(ctx: Context) : super(ctx)

    constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
}

Phương thức ghi đè

Như chúng ta đã đề cập trước đây, chúng ta luôn làm rõ mọi thứ trong Kotlin. Và khác với Java, Kotlin yêu cầu các annotation phải rõ ràng (chúng ta gọi là open) cho các overridable member và để có thể ghi đè.

open class Base {
    open fun v() {}
    fun nv() {}
}
class Derived() : Base() {
    override fun v() {}
}

override annotation là yêu cầu bắt buộc đối với Derived.v(). Nếu nó bị thiếu, trình biên dịch sẽ báo lỗi. Nếu không có open annotation trên một hàm, giống như Base.nv(), khai báo một phương thức với cùng tên trong lớp con là bất hợp pháp dù là có ghi đè hoặc không ghi đè. Trong một final class (ví dụ một lớp không có open annotation), các thành viên open đều bị cấm.

Một thành viên được đánh dấu là ghi đè bản thân nó là open, tức là nó có thể được ghi đè trong các lớp con. Nếu bạn muốn ngăn cấm việc ghi đè, hãy sử dụng final:

open class AnotherDerived() : Base() {
    final override fun v() {}
}

Thuộc tính ghi đè

Ghi đè thuộc tính hoạt động tương tự như ghi đè phương thức; các thuộc tính được khai báo trên lớp cha và được khai báo lại trên một lớp con và có từ khóa override phía trước. Mỗi một thuộc tính được khai báo có thể được ghi đè bởi một thuộc tính cùng với một initializer hoặc bởi một thuộc tính cùng với phương thức getter.

open class Foo {
    open val x: Int get() { ... }
}

class Bar1 : Foo() {
    override val x: Int = ...
}

Bạn có thể ghi đè một thuộc tính val với một thuộc tính var, nhưng không thể làm ngược lại. Có thể làm được vậy bởi vì thuộc tính var cơ bản khai báo một phương thức getter và ghi đè nó bằng var giống như là khai báo bổ sung một phương thức setter trong lớp con. Lưu ý, bạn có thể sử dụng từ khóa override như là một phần của khai báo thuộc tính trong constructor chính

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

Lời gọi cài đặt lớp cha

Mã trong một lớp con có thể gọi các hàm lớp cha và có thể truy cập thuộc tính bằng từ khóa super

open class Foo {
    open fun f() { println("Foo.f()") }
    open val x: Int get() = 1
}

class Bar : Foo() {
    override fun f() { 
        super.f()
        println("Bar.f()") 
    }
    
    override val x: Int get() = super.x + 1
}

Bên trong một lớp inner, việc truy cập lớp cha của lớp outer được thực hiện cùng với từ khóa super cùng với tên của lớp outer super@Outer

class Bar : Foo() {
    override fun f() { /* ... */ }
    override val x: Int get() = 0
    
    inner class Baz {
        fun g() {
            super@Bar.f() // Calls Foo's implementation of f()
            println(super@Bar.x) // Uses Foo's implementation of x's getter
        }
    }
}

Quy tắc ghi đè

Trong kotlin, việc áp dụng kế thừa được áp dụng bởi các quy tắc sau: Nếu một lớp kế thừa nhiều implementation của cùng một member từ các lớp cha trực tiếp, nó phải ghi đè lên member này và cung cấp việc cài đặt của nó (có lẽ bằng cách thực hiện một trong những lớp kế thừa). Để hiển thị supertype mà từ đó việc kế thừa được thực hiện, chúng ta sử dụng super cùng với tên supertype trong dấu ngoặc nhọn super<Base>

open class A {
    open fun f() { print("A") }
    fun a() { print("a") }
}

interface B {
    fun f() { print("B") } // interface members are 'open' by default
    fun b() { print("b") }
}

class C() : A(), B {
    // The compiler requires f() to be overridden:
    override fun f() {
        super<A>.f() // call to A.f()
        super<B>.f() // call to B.f()
    }
}

Không có vấn đề gì khi kế thừa cả A và B, cũng không có vấn đề gì với hàm a(), b() khi mà C kế thừa chỉ một implementation với mỗi một hàm này. Nhưng đối với f(), chúng ta có 2 implementation được thực hiện kế thừa bời C, do đó chúng ta phải ghi đè f() trong C và cung cấp own implementation để tránh sự mơ hồ.

Lớp trừu tượng

Một lớp và một số member của nó có thể được khai báo là trừu tượng. Một member trừu tượng không có một implementation trong lớp của nó. Chú ý rằng chúng ta không cần phải annotate một lớp hoặc một hàm trừu tượng với open - điều này sẽ tự động được thực hiện Chúng ta có thể ghi đè một open member không trừu tượng với một member trừu tượng

open class Base {
    open fun f() {}
}

abstract class Derived : Base() {
    override abstract fun f()
}

So sánh với các ngôn ngữ khác

Trong kotlin, không giống với java hay C#, các lớp không có phương thức static. Trong hầu hết các trường hợp, bạn chỉ nên sử dụng hàm ở mức package. Nếu bạn phải viết một hàm mà có thể được gọi mà không cần class instance nhưng cần truy cập vào các thành phần nội tại của một lớp (ví dụ như phương thức factory), bạn có thể viết nó như một member của một đối tượng declaration bên trong lớp đó. Cụ thể hơn, nếu bạn khai báo một đối tượng companion bên trong lớp của bạn, bạn có thể gọi các member của nó với các cú pháp giống như phương thức static trong Java/C#

Kết luận

Bài viết này mình dịch lại từ nguồn https://kotlinlang.org/docs/reference/classes.html Rất mong nhận được sự đóng ghóp của các bạn


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí