Kotlin Bài 8: Kế thừa trong Kotlin

Xin chào, hôm nay mình sẽ trình bày về kế thừa lớp trong Kotlin. Đây là khái niệm rất quan trọng trong lập trình hướng đối tượng. Tại sao quan trọng thì mình sẽ trình bày trong xuyên suốt bài viết. Kế thừa là việc 1 lớp con thừa hưởng toàn bộ các thuộc tính và phương thức của lớp cha, đồng thời có thể có thêm những thuộc tính và phương thức mà lớp cha không có.

[toc]

Định nghĩa kế thừa

Các bạn vẫn giữ code của bài trước chứ, ta đã định nghĩa lớp Animal đặc trưng cho động vật. Bây giờ ta sẽ định nghĩa 1 lớp Cat kế thừa Animal để làm rõ hơn về khái niệm này nhé. Ta định nghĩa như sau:

class Cat(var color: String ,kind:String, name: String) : Animal(kind, name) {


}

Ta đã tạo ra 1 lớp Cat kế thừa lớp Animal đã tạo buổi trước bằng cách khai báo Animal sau dấu :. Lớp Cat vẫn có các thuộc tính kind, name như Animal. Nhưng đã có thêm 1 thuộc tính mới, là thuộc tính color. Và hàm khởi tạo của Cat sẽ gọi đến hàm khởi tạo Animal(kind, name) của Animal. Vì sao đối tượng Cat lại có kind name? Đơn giản vì Cat kế thừa từ Animal, nghĩa là mọi đối tượng Cat chính là Animal mở rộng, và đều có các thuộc tính của Animal. OK, còn color thì sao. Đây là thuộc tính mới của Cat Animal không có. Vì Cat Animal mở rộng mà :)) Cat có toàn bộ các thuộc tính và phương thức của Animal, thêm vào đó là các phương thức, đối tượng mới của riêng Cat.

Ủa nhưng mà sao lại có báo lỗi ở phần khai báo kế thừa: This type is final, so it cannot be inherited fromNguyên nhân là mặc định trong Kotlin, các class được khai báo được hiểu là final class, tức là không thể kế thừa. Vì vậy ta sẽ phải thêm từ khóa open trước class Animal để lớp này có thể được kế thừa bởi lớp Cat:

open class Animal(var kind: String, var name: String) {

}

OK, giờ đã hết lỗi. Ta sẽ thử tạo 1 đối tượng Cat và sử dụng các phương thức của Animal:

var cat = Cat("Red","Cat", "Lyly")
cat.getInfo() //Hello, i am a Cat, my name is Lyly

Như vậy là các đối tượng Cat hoàn toàn sử dụng được phương thức của Animal, do lớp Cat kế thừa lớp Animal. Ta thậm chí có thể khai báo như thế này:

var cat:Animal = Cat("Red", "Cat", "Lyly")

Ở đây ta khai báo 1 đối tượng có kiểu Animal và khởi tạo nó với Cat. Điều này hoàn toàn hợp lệ bởi khi Cat kế thừa Animal, thì Cat chính là Animal (mở rộng). Nhưng nếu ta khai báo thế này:

var cat:Cat = Animal("Cat", "Lyly")

Thì lập tức IDE sẽ báo lỗi. Bởi lẽ Animal là lớp cha của Cat, Cat chắc chắn là 1 Animal, nhưng 1 Animal có thể là Bird, Dog, Chicken, … chứ chưa chắc đã là Cat. Vì vậy ta khai báo đối tượng kiểu Cat mà khởi tạo kiểu Animal thì sẽ là không hợp lệ.

Override

Ye, như ở phần trên ta đã biết, 1 đối tượng của lớp con (Cat) có thể gọi được phương thức của lớp cha (Animal). Ta còn có thể làm được nhiều hơn thế: ta có thể mở rộng phương thức đã định nghĩa ở lớp cha. Mình sẽ trình bày ngay đây: ở lớp Animal ta đã khai báo phương thức getInfo, và phương thức này hoàn toàn sử dụng được với các đối tượng Cat. Nhưng giờ đây ta muốn getInfo trả về thêm màu lông của đối tượng Cat. Sau đây là cách làm. Ta định nghĩa hàm getInfo ở lớp Cat như sau:

override  fun getInfo() {

}

Và thêm từ khóa open vào dòng định nghĩa phương thức getInfo ở lớp Animal:

open fun getInfo()

Ở đây, trong lớp Cat ta đã viết lại phương thức getInfo của lớp Animal, nên ta phải dùng từ khóa override. Tuy nhiên, chỉ có những phương thức open mới cho phép ta định nghĩa lại, thành ra ta phải thêm từ khóa open khi định nghĩa getInfo ở lớp Animal, dễ hiểu đúng không? Lúc này ta chạy lại chương trình. Kết quả console sẽ không in ra dòng log nào. Vì sao? Bởi với từ khóa override, phương thức getInfo ở class Animal đã bị ghi đè hoàn toàn bởi phương thức getInfo ở class Cat. Mà như ta thấy trong class Cat, phương thức getInfo không thực hiện bất cứ điều gì, dẫn đến không có dòng log nào được in ra ở console. Giờ ta sẽ thử in ra các thuộc tính của Cat xem sao:

override fun getInfo() {
   print("Hello, i am a $kind, my name is $name")
   age?.let {
       println(", i am $age years old")
   }
}

Kết quả:

Hello, i am a Cat, my name is Lyly

Ồ, vậy là ổn rồi. Nhưng như bạn thấy đấy, mình đã phải copy phương thức getInfo Animal để cho vào Cat. Thế thì sinh ra hướng đối tượng với cả kế thừa để làm gì, đúng không. Ta có cách khác để làm điều này, đơn giản chỉ cần gọi:

super.getInfo()

Là phương thức getInfo ở lớp cha (Animal) sẽ được thực thi. Ta muốn in ra màu lông của Cat nữa thì sao, đơn giản là thêm dòng lệnh:

println("I am $color")

Như vậy phương thức getInfo ở Cat sẽ như sau:

override fun getInfo() {
   super.getInfo()
   println("I am $color")
}

Chạy lại chương trình ta thu được:

Hello, i am a Cat, my name is Lyly
I am Red

Lưu ý: Bạn có thể định nghĩa thêm bất kỳ các phương thức mới nào ở class Cat, nhưng chúng sẽ chỉ được gọi bởi các đối tượng Cat, chứ ko thể gọi bởi các đối tượng Animal.

Sử dụng interface

Interface trong Kotlin cũng khá giống với bên Java. Nó chứa các phương thức mà các lớp kế thừa nó sẽ thực hiện. Chúng ta cũng có thể định nghĩa các thuộc tính trong interface. Thôi ta sẽ đi vào ví dụ cho dễ hiểu nhé. Ta định nghĩa interface IAction như sau:

interface IAction {
   fun makeSound()
   fun eat(food: String)
}

Như các bạn thấy, interface trên có 2 phương thức makeSound và eat. Ta sẽ cho lớp Cat kế thừa interface này:

class Cat(var color: String, kind: String, name: String) : Animal(kind, name), IAction

Lưu ý: khi muốn kế thừa thêm bất kỳ class hay interface nào, ta chỉ cần điền thêm tên class hay interface đó, ngăn cách nhau bới dấu

Giờ IDE sẽ thông báo lỗi cho ta biết rằng, ta cần định nghĩa các phương thức có ở trong interface IAction vào lớp Cat. Okie, định nghĩa thôi, và ta viết luôn nội dung cho các phương thức đó (nên nhớ ở trong IAction ta mới định nghĩa tên phương thức mà chưa viết nội dung):

override fun makeSound() {
   println("Meo meo meo")
}

override fun eat(food: String) {
   println("I am eating $food")
}

Sau đó thử gọi :

var cat = Cat("Red", "Cat", "Lyly")
cat.makeSound()
cat.eat("Rat")

Kết quả:

Meo meo meo
I am eating Rat

Vậy là đối tượng Cat đã có thể sử dụng các phương thức khai báo trong IAction nhờ kế thừa interface này. Ta cũng có thể viết nội dung phương thức ngay trong interface, đây là 1 cải tiến lớn của Kotlin so với Java. Ta sử lại nội dung phương thức eat trong IAction:

fun eat(food: String){
   println("Yeah")
}

và sửa lại trong class Cat như sau:

override fun eat(food: String) {
   super.eat(food)
}

Lại là từ khóa super, từ khóa này dùng để gọi tới phương thức tương ứng của cha (trường hợp này “cha” là IAction).

Thuộc tính trong interface

Cũng giống như phương thức, ta có thể định nghĩa các thuộc tính trong interface và sử dụng chúng ở trong class kế thừa interface đó. Ví dụ ta thêm thuộc tính numberOfFoot vào interface IAction:

override var numberOfFoot: Int = 4

Lúc này, khi khai báo numberOfFoot trong class, ta đã có thể (và bắt buộc )khởi tạo giá trị cho nó. Hoặc ta có 1 cách khác:

override var numberOfFoot: Int
   get() = 4
   set(value) {

   }

Vậy là ta đã set được giá trị cho numberOfFootTa định nghĩa thêm 1 phương thức move trong IAction thế này:

fun move(){
   println("I move with $numberOfFoot legs")
}

Lưu ý: Các phương thức được viết nội dung trong interface thì không bắt buộc phải khai báo trong class kế thừa interface đó.

Giờ ta thử sử dụng phương thức move mới khởi tạo:

var cat = Cat("white", "cat", "Lyly")
cat.move() // In ra: I move with 4 legs

Vậy là giá trị của thuộc tính numberOfFoot đúng là 4 như ta đã khai báo trong class Cat.

Abstract class (lớp trừu tượng)

Về cơ bản abstract class cũng tương tự như interface, nó cho phép ta định nghĩa các thuộc tính, phương thức để sử dụng trong class kế thừa nó. Tuy nhiên abstract class có 1 nhược điểm so với interface là: 1 class chỉ được phép kế thừa từ 1 class khác, trong khi đó có thể kế thừa tùy ý số interface. Vì vậy khi class của bạn đã kế thừa 1 class rồi thì sẽ không thể sử dụng abstract class được nữa. Dù sao mình cũng sẽ demo 1 ví dụ về abstract class:

open abstract class AbstractAction {

   abstract fun makeSoundAbstract()

   fun eatFood(){
   println("I am eating")
   }
 
}

Ta tạo ra 1 abstract class AbstractAction. Trong này ta định nghĩa 2 phương thức makeSoundAbstract và eat. Phương thức makeSoundAbstract ta chưa viết luôn thân hàm nên phải thêm từ khóa abstract ở đầu, còn đối với eat thì không cần. Ok, giờ ta sẽ cho class Cat của ta kế thừa class AbstractAction này:

class Cat(var color: String, kind: String, name: String) : Animal(kind, name), IAction, AbstractAction() {

Ngay lập tức IDE sẽ báo cho ta 1 lỗi: Only one class may appear in supertype list

Tức là chỉ 1 class được xuất hiện ở danh sách kế thừa. Class Cat của ta đã kế thừa Animal, nên không thể kế thừa thêm 1 class khác (AbstractClass). Ta thử xóa bỏ phần kế thừa Animal và các phương thức liên quan đến Animal đi xem sao. Sau đó override lại phương thức makeSoundAbstract (Phương thức eatFood không bắt buộc override vì nó đã được viết thân hàm).

override fun makeSoundAbstract() {
   println("Meo meo")
}

Rồi, thử các phương thức mới xem nào:

var cat = Cat("Red", "Cat", "Lyly")
cat.makeSoundAbstract()
cat.eatFood()

/*Kết quả: 
Meo meo
I am eating*/

Trùng lặp tên phương thức khi kế thừa nhiều interface

Đôi khi trong các class hay interface mà ta kế thừa, có những phương thức trùng tên với nhau. Vậy, chỉ với từ khóa super, làm sao để gọi chính xác phương thức ứng với interface (class) mà ta mong muốn. Ví dụ mình sửa phương thức eatFood trong AbstractAction thành như sau:

open fun eat(food: String) {
   println("[AbstractAction]I am eating $food")
}

và sửa phương thức eat trong IAction thành như sau:

fun eat(food: String) {
   println("[IAction]I am eating $food")
}

Vậy là 2 phương thức đã giống nhau từ tên cho đến tham số đầu vào, giờ hãy để ý trong phương thức override fun eat(food: String) của class Cat: Many supertypes available, please specify the one you mean in angle bracket, egg ‘super<Foo>’Nghĩa là: phương thức này đã được định nghĩa ở nhiều lớp hoặc interface cha (IActionAbstractAction). Ta cần chỉ rõ muốn sử dụng phương thức ở lớp cha nào. Cách làm như sau:

super<IAction>.eat(food) // Sử dụng phương thức eat ở interface IAction
super<AbstractAction>.eat(food)// Sử dụng phương thức eat ở class AbstractAction

Thử sử dụng phương thức eat này xem:

var cat = Cat("Red", "Cat", "Lyly")
cat.eat("Rat")

Kết quả:

[IAction]I am eating Rat
[AbstractAction]I am eating Rat

Vậy là ta đã định rõ được phương thức ta muốn sử dụng là tới từ lớp (interface) cha nào.

Tổng kết

Vậy là bài này mình đã trình bày xong những khái niệm cơ bản về kế thừa trong lập trình hướng đối tượng với Kotlin. Các kiến thức về interface, abstract class, override mình đã đưa ra kèm các ví dụ cụ thể. Hy vọng các bạn có thể nắm rõ nội dung bài học.

 

Kotlin Bài 7: Lớp, thuộc tính và phương thức

Lớp (class) là khái niệm cơ bản nhất trong lập trình hướng đối tượng. Nó đặc trưng cho 1 loại đối tượng xác định, với những thuộc tính và hành động xác định. Lớp bao gồm các thuộc tính và các phương thức. Hôm nay, hãy cùng mình tìm hiểu về khái niệm cơ bản nhưng cũng là quan trọng nhất của lập trình hướng đối tượng này nhé.

Định nghĩa lớp

Ta định nghĩa lớp qua từ khóa class, điều này khá giống với Java:

class Human

Dòng code trên vừa khai báo 1 lớp rỗng, lớp đơn giản nhất trong lập trình hướng đối tượng. Nó không có thuộc tính cũng chẳng có thân hàm, tuy nhiên ta vẫn có thể khởi tạo 1 đối tượng của lớp Human trên như sau:

val human = Human()

Như vậy là không giống với Java, ta không cần từ khóa new để khởi tạo 1 đối tượng mới.

Thuộc tính và hàm khởi tạo của lớp

Lớp ta vừa mới khởi tạo ở phần trước còn khá đơn giản, giờ ta sẽ thêm vài thuộc tính cho nó, đồng thời sẽ thêm cách khởi tạo mới cho class này:

class Human(var name: String, var age: Int) {

}

Lớp Human đã được bổ sung 2 thuộc tính là name age. Và nó có hàm khởi tạo gồm 2 tham số đầu vào kiểu Stringint(var name: String, var age: Int) được gọi là primary constructor (hàm khởi tạo chính).

Vậy là với đúng 1 dòng, ta đã vừa khai báo được các thuộc tính của class Human, vừa định nghĩa được hàm khởi tạo ứng với 2 thuộc tính đó. Rất ngắn gọn và dễ hiểu đúng không. Giờ ta sẽ thử khởi tạo 1 đối tượng Human bằng hàm khởi tạo ta vừa định nghĩa:

var me: Human = Human("Dương Vũ", 25)

Đơn giản là khai báo biến có kiểu dữ liệu là Human và khởi tạo cho nó! Theo đúng định nghĩa lớp viết ra ở trên, mình dùng hàm khởi tạo có các tham số đầu vào là StringInt, tương ứng với tên và tuổi. Giờ mình sẽ lấy ra giá trị các thuộc tính của thằng me mà mình vừa khởi tạo:

print("name = ${me.name}, age = ${me.age}") // In ra: name = Dương Vũ, age = 25

Cách truy xuất đến thuộc tính của đối tượng rất đơn giản dễ dàng đúng ko. Ta sử dụng cú pháp:

<tên_đối_tượng>.<tên_thuộc_tính>

để truy xuất đến thuộc tính của đối tượng me.

Định nghĩa phương thức cho lớp

Từ đầu đến giờ ta đã định nghĩa class Human, có các thuộc tính của riêng nó, nhưng chưa định nghĩa các hành động cho nó thực hiện. Ta sẽ viết định nghĩa 1 phương thức nằm trong class Human như thế này:

fun introduce() {
    print("Hello, I am $name, I am $age years old")
}

Như vậy việc định nghĩa phương thức trong class đơn giản chỉ là định nghĩa hàm để xử lý các thuộc tính trong class đó. Như ở trên ta đã xây dựng 1 phương thức cho phép Human giới thiệu về bản thân mình. Phương thức này sẽ log ra tên, tuổi của đối tượng gọi phương thức:

var me: Human = Human("Dương Vũ", 25)
me.introduce() // in ra: Hello, I am Dương Vũ, I am 25 years old

Như vậy là phương thức introduce đã hoạt động. Ta thấy, khi ở bên trong thân class, thì truy xuất đến các thuộc tính đơn giản là gọi thẳng tên thuộc tính đó ra: $name, $age. Còn ở bên ngoài class ta cần sử dụng cú pháp <tên_đối_tượng>.<tên thuộc tính>. Mình sẽ có 1 bài viết riêng về các quy tắc truy xuất đến thuộc tính và phương thức của 1 lớp để các bạn hiểu rõ hơn. Còn giờ thì ta cứ biết cơ bản là vậy đã, ok 😉

Ok, giờ ta sẽ tạo 1 phương thức khác cho quen tay. Thêm hàm sau vào lớp Human:

fun greeting(){
   print("From $name with love")
}

Ta sẽ gọi hàm greeting này:

var harry = Human("Harry", 15) // Khởi tạo đối tượng harry
harry.greeting() //In ra: From Harry with love

Block init

Trong Kotlin class, ta có thể định nghĩa ra 1 block init, bao gồm các câu lệnh được thực hiện ngay sau khi đối tượng của lớp đó được khởi tạo. Ví dụ nhé, ta thêm khối lệnh sau vào lớp Human:

init {
   println("$name has been born")
}

Block init này thực hiện việc log ra dòng thông báo ngay sau khi mỗi đối tượng của class Human được khởi tạo. Thử khởi tạo 1 đối tượng mới:

var peter = Human("Peter", 15) // In ra: Peter has been born
peter.introduce() // in ra: Hello, I am Peter, I am 15 years old

Như ta thấy các câu lệnh trong block init đã được gọi ngay sau khi đối tượng peter được tạo ra. Và tất nhiên là trước khi phương thức introduce được thực hiện.

Từ khóa this

Trong 1 lớp , từ khoá this trỏ tới đối tượng hiện tại của lớp đó. Nghe hơi trúc trắc nhỉ, mình sẽ demo ngay để các bạn dễ hình dung. Ta viết 1 phương thức như sau:

fun increaseAge() {
   this.age += 1
}

Phương thức này tăng giá trị của thuộc tính age lên 1 đơn vị. Ở đây, từ khoá this trỏ đến đối tượng hiện tại, tức là đối tượng đang gọi phương thức increaseAge này, và thay đổi thuộc tính age của đối tượng đó. Ta thử gọi phương thức:

var human = Human("Nam", 20)
human.increaseAge() 
human.introduce() //In ra: Hello, I am Nam, I am 21 years old

Secondary constructor

Một lớp có thể có nhiều hàm khởi tạo khác nhau. Secondary constructor là các hàm khởi tạo khác với hàm khởi tạo chính (primary constructor). Mình sẽ lấy thêm 1 ví dụ khác cho các bạn hiểu rõ hơn nữa nhé. Ta sẽ tạo 1 lớp Animal. Đầu tiên, khai báo lớp và hàm khởi tạo:

class Animal(var kind:String, var name: String){

}

Animal là lớp đặc trưng cho động vật, có các thuộc tính là kind (loài) name (tên)Giờ mình sẽ thêm 1 thuộc tính nữa:

var age:Int?=null

Thuộc tính age có kiểu Int. Thuộc tính này có thể có giá trị hoặc null. Nếu bạn còn chưa rõ các khái niệm về nullable type, hãy đọc lại bài hướng dẫn về Nullable type – kiểu dữ liệu có thể null của mình nhé.

Thêm 1 phương thức:

fun getInfo() {
   print("Hello, i am a $kind, my name is $name")
   age?.let {
       print(", i am $age years old")
   }
}

Đơn giản là log ra câu chào ứng với các giá trị của kind name. Nếu age được gán giá trị (khác null) thì log thêm cả giá trị của age. Toàn bộ lớp Animal của ta sẽ như thế này:

class Animal(var kind: String, var name: String) {
    
    var age: Int? = null
    
    fun getInfo() {
        print("Hello, i am a $kind, my name is $name")
        age?.let {
            print(", i am $age years old")
        }
    }
}

Thử khởi tạo 1 đối tượng, và gọi phương thức getInfo:

var cat = Animal("cat", "Miu Miu")
cat.age = 1
cat.getInfo() //In ra: Hello, i am a cat, my name is Miu Miu, i am 1 years old

Tạo 1 đối tượng Animal khác:

var dog = Animal("dog", "Milu")
dog.getInfo() // In ra: Hello, i am a dog, my name is Milu

Lần này, dòng log ko có thông báo age vì age đang có giá trị nullCác bạn có thể thấy rằng mỗi khi khởi tạo 1 đối tượng Animal, ngoài kind name là các thuộc tính bắt buộc phải có trong hàm khởi tạo. Thì thuộc tính age là không bắt buộc. Nếu ta quên không set giá trị cho age thì đương nhiên nó sẽ null. Giờ ta muốn đưa age vào 1 hàm khởi tạo khác mà vẫn muốn giữ hàm khởi tạo ta định nghĩa lúc đầu, phải làm thế nào. Ta sẽ định nghĩa thêm 1 hàm khởi tạo nữa, tất cả (những) hàm khởi tạo được định nghĩa thêm được gọi là Secondary constructor:

constructor ( kind: String, name: String,age: Int) : this(kind, name) {
   this.age = age
}

Ở đây có 2 điều cần nói:

  • Secondary constructor phải gọi đến primary constructor: this(kind, name) hoặc gọi đến Secondary constructor nào có gọi primary constructor. Tóm lại bằng mọi giá phải tham chiếu đến primary constructor.
  • Secondary constructor không cho phép định nghĩa các thuộc tính, tức là ta không thể sử dụng các từ khóa var, val khi định nghĩa Secondary constructor.

Ta thử khởi tạo 1 phương thức dùng hàm Secondary constructor mới được viết xem sao:

var bird  = Animal("bird", "Kiki", 3)
bird.getInfo() // In ra: Hello, i am a bird, my name is Kiki, i am 3 years old

Ta sẽ thêm 1 thuộc tính nữa:

var favorite: String? = null

Và thêm 1 hàm khởi tạo nữa:

constructor(kind: String, name: String, age: Int, favorite: String) : this(kind, name, age) {
   this.favorite = favorite
}

Hàm khởi tạo này gọi đến hàm khởi tạo thứ 2 ta định nghĩa ở trên, do hàm khởi tạo thứ 2 đã tham chiếu đến primary constructor, nên hàm khởi tạo thứ 3 này hoàn toàn hợp lệ. Ok, hi vọng các bạn đã hiểu được về secondary constructor.

Getter và setter

Khác với java, mỗi thuộc tính trong các Kotlin class đều được khởi tạo 1 hàm getter và 1 hàm setter mặc định. Hàm getter của 1 thuộc tính được gọi đến khi ta truy xuất đến giá trị của thuộc tính đó. Hàm setter thì được gọi khi ta thay đổi giá trị của thuộc tính. Để ví dụ mình sẽ định nghĩa thêm 1 thuộc tính trong lớp Animal trên và viết getter setter cho nó. Các bạn hãy nhớ kỹ rằng, nếu ta ko viết getter setter cho 1 thuộc tính thì thuộc tính đó sẽ có các hàm getter setter mặc định, các hàm đó đơn giản chỉ lấy và gán giá trị cho thuộc tính của chúng ta.

var isSafe: Boolean
   get() {
       println("Getter of property isSafe called")
       if (kind.equals("tiger", true) || kind.equals("puma", true)) {
           return false
       } else {
           return true
       }
   }
   set(value) {
       println("Setter of property isSafe called")
   }

Giờ thử tạo 1 đối tượng và lấy giá trị của thuộc tính isSafe:

var dog = Animal("dog", "Lili")
println("${dog.isSafe}") 

Kết quả:

Getter of property isSafe called
true

Như vậy khi lấy giá trị của isSafe thì hàm getter ứng với thuộc tính isSafe đã được gọi. Ta sẽ thử thay đổi giá trị của isSafe xem sao:

dog.isSafe = false // In ra: Setter of property isSafe called

Vậy khi thay đổi giá trị của isSafe thì hàm setter đã được gọi.

Lưu ý: Hàm getter và setter thường được viết lại cho các thuộc tính mà giá trị của nó phụ thuộc vào gía trị của các thuộc tính khác. Như ví dụ trên, giá trị của thuộc tính isSafe phụ thuộc vào gía trị của kind. Nếu trong hàm getter hay setter mà các bạn truy xuất đến thuộc tính tương ứng của nó thì sẽ xảy ra lỗi. Mình ví dụ như sau, ta sẽ thay đổi lại nội dung hàm getter ở trên:

get() {
   return isSafe
}

Hàm getter của isSafe trả về đúng giá trị của isSafe nghe hợp lý đúng ko. Thử xem:

var dog = Animal("dog", "Lili")
println("${dog.isSafe}")

Kết quả là dòng log đỏ chói : Exception in thread “main” java.lang.StackOverflowError 😀 . Lý do vì bản thân hàm getter ở trên đã return về giá trị của isSafe, mà mỗi khi truy xuất đến isSafe thì hàm getter lại được gọi, điều đó tạo ra 1 vòng lặp không có điểm dừng, đây chính là nguyên nhân exception java.lang.StackOverflowError bị văng ra. Vì vậy, hãy luôn nhớ, chỉ viết getter setter cho các thuộc tính mà giá trị của nó phụ thuộc vào các thuộc tính khác. Còn nếu không, setter getter đã được mặc định viết cho chúng ta rồi, chúng ta ko nên chọc vào chúng nữa.