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 và 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 mà Animal không có. Vì Cat là 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 from. Nguyê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 numberOfFoot. Ta đị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 (IAction và AbstractAction). 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.