Lập trình Kotlin — Lớp và tính kế thừa

Liem Vo
Viet Android Developers
8 min readAug 17, 2020

Trong bài trước chúng ta đã tìm hiểu dòng điều khiển các bạn có thể xem lại ở video bài trước hoặc link. Bài học hợp này sẽ học về lớp trong ngôn ngữ lập trình Kotlin và một phần không thể thiếu trong các ngôn ngữ lập trình hướng đối tượng cũng như Kotlin.

Lớp

Lớp trong ngôn ngữ lập trình kotlin được khai báo với từ khoá `class`

class Student { /*… */ }

Một lớp khai báo bao gồm tên, phần đầu (nơi chứa những thông số, hàm khởi tạo đầu tiên — primary constructor) và thân (được bao quanh bởi dấu ngoặc nhọn). Phần đầu và thân là không bắt buộc, nếu lớp không có đầu và thân thì có thể được lược bỏ.

class Student

Hàm khởi tạo

Một lớp trong ngôn ngữ Kotlin có thể có một khởi tạo đầu tiên và một hoặc nhiều hàm khởi tạo thứ hai. Hàm khởi tạo đầu tiên là một phần của phần đầu của lớp. Nó thường đi sau tên lớp (và có thể có những thông số hoặc không)

class Student constructor(studentNumber: String) { /*…*/}

Từ khoá `constructor` không yêu cầu trong hàm khởi tạo đầu tiên nếu nó không định nghĩa annotations (lời chú thích) hoặc scope (phạm vi của dùng hàm này). Class có thể được viết lại như sau:

class Student(studentNumber: String) { /*…*/}

Hàm khởi tạo đầu tiên không thể có bất kỳ đoạn code nào. Để đặt một số đoạn mã chúng ta có thể đặt trong đoạn khởi tạo bắt đầu với từ khoá `init`. Một lớp có thể có một hoặc nhiều đoạn khởi tạo và thứ tự thực thi theo thứ tự của chung trong thân lớp.

package com.vad.lop

class Student(studentNumber: String) {
val firstProperty = "First property: $studentNumber".also (::println)

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

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

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

fun main() {
val student = Student(studentNumber = "32902")
}

Kết quả sẽ theo thứ tự như sau

First property: 32902
First initializer block that prints 32902
Second property: 5
Second initializer block that prints 5

Thông số trong hàm khởi tạo đầu tiên có thể dùng trong khối khởi tạo hoặc thuộc tính khởi tạo. Ví dụ vừa rồi chúng ta có thể thấy `studentNumber` được dùng. Những thông số của hàm khởi tạo đầu tiên có thể thay đổi hoặc không thay đổi và có thể giới hạn phạm vi với từ khoá về scope (private, internal, …). Ví dụ lớp `Student` có thể khai báo với annotations và phạm vi như sau:

class Student public @Inject constructor(studentNumber: String)

Những hàm khởi tạo số hai (secondary constructors)

Lớp cũng có thể có một hoặc nhiều hàm khởi tạo số hai và bắt đầu với từ khoá constructor.

class Student {
val subjects = mutableListOf<String>()

constructor(initialSubjects: List<String>) {
subjects.addAll(initialSubjects)
}
}

Nếu lớp đã có hàm khởi tạo đầu tiên thì trong các hàm khởi tạo số hai phải gọi lại hàm khởi tạo đầu tiên.

class Student(name: String) {
val subjects = mutableListOf<String>()

constructor(name: String, initialSubjects: List<String>) : this(name){
subjects.addAll(initialSubjects)
}
}

Lưu ý code trong khối khởi tạo là một phần của hàm khởi tạo đầu tiên và nó sẽ thực hiện trước hàm khởi tạo số hai.

class Student(name: String) {
val subjects = mutableListOf<String>()
init {
println("Student name: $name")
}

constructor(name: String, initialSubjects: List<String>) : this(name){
subjects.addAll(initialSubjects)
println("Initial subjects: $initialSubjects")
}
}

fun main() {
val student = Student(name = "Van A", initialSubjects = listOf("Toan", "Van"))
}

Kết quả sẽ như sau:

Student name: Van A
Initial subjects: [Toan, Van]

Nếu một lớp không khai báo hàm khởi tạo thì trình biên dịch sẽ tự động tạo ra một hàm khởi tạo public không có tham số.

Các thông số trong hàm khởi tạo có thể có giá trị mặc định và khi dùng lớp không truyền vào thông số thì trình biên dịch sẽ dùng giá trị mặc định.

Cách khởi tạo một lớp.

Để tạo một đối tượng của lớp chúng ta gọi bất kì hàm khởi tạo nào của lớp ví dụ trên. Ngôn ngữ Kotlin không cần dùng từ khoá `new`

Cách khởi tạo lớp lồng, bên trong và nặc danh bên trong sẽ giới thiệu ở bài lớp bên trong (Inner class).

** Lớp bao gồm các thành phần chính sau.

  • Hàm khởi tạo và khối khởi tạo
  • Các hàm
  • Các thuộc tính
  • Lớp lồng và lớp bên trong
  • Các đối tượng

Tính kế thừa

Tất cả các lớp trong ngôn ngữ Kotlin có một lớp cha là Any, nó là lớp cha mặc định nếu một lớp không khai báo lớp cha.

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

Any có ba phương thức: equals(), hashCode(), và toString(). Vì vậy nó đã được định nghĩa cho tất cả các lớp của ngôn ngữ Kotlin.

Mặc định, lớp trong Kotlin là final (cuối) không thể kế thừa, để kế thừa phải khai báo với từ khoá open. Để khai báo kế thừa thì trong phần đầu của lớp theo sau bởi lớp cha.

open class Animal(name: String)
class Cat(name: String): Animal(name)

Nếu lớp cha có hàm khởi tạo đầu tiên thì phải khởi tạo theo sau, nếu lớp cha không có hàm khởi tạo đầu tiên thì tất cả các hàm khởi tạo thứ hai phải được gọi.

open class Animal {
constructor(name: String)

constructor(name: String, legs: Int)
}
class Cat: Animal {
constructor(name: String) : super(name)
constructor(name: String, color: String): super(name, 4)
}

Overriding methods (Hàm kế thừa)

Giống như lớp, hàm muốn được hiện thực lại ở lớp con phải được khai báo với từ khoá open. Ở lớp con phải có từ khoá override ở đầu hàm và nếu lớp con không hiện thực lại với hàm có từ khoá open thì sẽ báo lỗi.

Từ khoá open chỉ có tác dụng lên hàm nó được dùng. Từ khoá override mặc định là open và có thể kế thừa nếu lớp con có lớp con và có thể hiện thực nó. Để ngăn chặn việc này thì có thể dùng từ khóa final.

open class Animal {
open fun makeSound() {
}

fun fill(){}
}

open class Bird: Animal() {
override fun makeSound() {
super.makeSound()
}
}

class A: Bird() {
override fun makeSound() {
super.makeSound()
}
}

Overriding properties (Thuộc tính kế thừa)

Giống như hàm kế thừa, thuộc tính kế thừa cũng yêu cầu có từ khoá open ở lớp cha ở thuộc tính cần kế thừa và từ khoá override đối với thuộc tính kế thừa ở lớp con. Giá trị của thuộc tính kế thừa có thể được gán trực tiếp hoặc từ hàm get. Nếu thuộc tính kế thừa có giá trị mặc định chúng ta có thể không kế thừa ở lớp con.


open class Animal {
open val legs: Int = 0
open val wings: Int = 0
}

open class Bird : Animal() {
override val legs: Int
get() = 2

override val wings: Int = 2
}

Thuộc tính kế thừa có thể đặt trong hàm khởi tạo với từ khoá override

open class Cat(
override val legs: Int,
override val wings: Int
) : Animal()

Thuộc tính kế thừa ở lớp cha là không đổi nhưng có thể kế thừa bằng thuộc tính thay đổi ở lớp con. Ngược lại không chính xác

open class Cat(
override val legs: Int,
override var wings: Int
) : Animal()

Thứ tự khởi tạo trong lớp kế thừa

Trong khi khởi tạo lớp kế thừa thì thứ tự khởi tao trong lớp cha thực hiện trước và sau đó đến thứ tự trong lớp con.

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

open class Derived(
name: String,
lastName: String
) : Animal(name.capitalize().also { println("Argument for Animal: $it") }) {
init {
println("Initializing Derived")
}

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

fun main() {
val derived = Derived("Hello", "World")
}

Kết quả như sau:

Argument for Animal: Hello
Initializing Animal
Initializing size in Animal: 5
Initializing Derived
Initializing size in Derived: 10

Gọi hàm của lớp cha

Lớp con có thể gọi hàm hoặc thuộc tính của lớp cha với từ khoá super.

open class Animal(val name: String) {
val character: String = "Animal"
open fun greet() {
println("I am $name in Animal")
}
}
open class Cat(
name: String,
lastName: String
) : Animal(name) {
override fun greet() {
super.greet()
println("I am $name in Cat")
}
val run: String get() = "${super.character} can run"
}

Lớp con bên trong lớp cha có thể gọi lớp cha thông qua từ khoá super và tên lớp cha rồi đến hàm hoặc thuộc tính cần gọi. Ví dụ super@Parent.a

Quy luật override

Trong ngôn ngữ Kotlin nếu class vừa kế thừa từ lớp cha hoặc interface (giao diện) với cùng tên hàm hoặc thuộc tính thì nó phải hiện thực và chỉ rõ lớp hoặc interface được gọi trong kế thừa.

interface GreetInterface {
fun greet() {
println("Greet in interface")
}
}

open class Animal(val name: String) {
val character: String = "Animal"
open fun greet() {
println("I am $name in Animal")
}
}

open class Cat(
name: String,
val lastName: String
) : Animal(name), GreetInterface {
override fun greet() {
super<Animal>.greet()
super<GreetInterface>.greet()
println("I am $name $lastName in Cat")
}

val run: String get() = "${super.character} can run}"
}

fun main() {
val cat = Cat("Hello", "World")
cat.greet()
}

Lớp Abstract

Lớp và một số thành phần của nó có thể khai báo là abstract và không cần hiện thực nó. Với hàm có từ khoá open chúng ta không cần đánh dấu là abstract.

open class Shape {
open fun draw() {}
}
abstract class Square: Shape() {
abstract override fun draw()
}

Companion Object

Trong Kotlin, để dùng những hàm hoặc thuộc tính của lớp không cần khởi tạo lớp đó giống như static trong Java, chúng ta dùng Companion Objects để khai báo.

class Configuration private constructor(){
init {
INSTANCE = this
}

companion object {
private var INSTANCE: Configuration? = null
val instance: Configuration
get() = if (INSTANCE == null) {
Configuration().apply {
INSTANCE = this
}
} else {
INSTANCE!!
}
}
}

fun main() {
val configuration = Configuration.instance
}

Cảm ơn các bạn đã đọc bài. Đăng ký kênh chúng tôi để xem những bài học mới nhất.

Youtube kênh: https://bit.ly/2EFOOXs

Thảo luận bằng cách comment ở đây hoặc trong video của blog này.

--

--