1. Trước khi bắt đầu
Điều kiện tiên quyết
- Quen thuộc với việc sử dụng Kotlin Playground để chỉnh sửa chương trình Kotlin.
- Nắm được các khái niệm cơ bản về lập trình trong Kotlin dạy trong Bài 1 của khoá học này. Cụ thể là chương trình
main()
, hàm có đối số trả về giá trị, biến, kiểu dữ liệu và phép toán, cũng như câu lệnhif/else
. - Có thể định nghĩa một lớp (class) trong Kotlin, tạo một thực thể đối tượng từ lớp đó, cũng như truy cập vào các thuộc tính và phương thức của lớp đó.
Kiến thức bạn sẽ học được
- Tạo một chương trình Kotlin sử dụng tính kế thừa để triển khai một hệ phân cấp lớp (class hierarchy).
- Mở rộng một lớp (class), ghi đè chức năng hiện tại của lớp và thêm chức năng mới.
- Chọn đúng hệ số sửa đổi chế độ hiển thị (visibility modifier) cho biến.
Sản phẩm bạn sẽ tạo ra
- Một chương trình Kotlin với nhiều loại nhà ở (dwelling) được triển khai dưới dạng hệ phân cấp lớp.
Bạn cần có
- Máy tính có kết nối Internet để truy cập Kotlin Playground
2. Hệ phân cấp lớp là gì?
Con người có xu hướng phân loại các mục có thuộc tính và hành vi tương tự nhau thành các nhóm và thậm chí tạo thành một loại phân cấp nào đó giữa các mục. Ví dụ: bạn có thể có một danh mục rộng như rau củ và trong đó, bạn có thể có một loại cụ thể hơn như các loại đậu. Trong các loại đậu, bạn còn có thể có nhiều loại cụ thể hơn như đậu Hà Lan, đậu hạt, đậu lăng, đậu gà và đậu nành.
Bạn có thể biểu thị mối quan hệ này dưới dạng hệ phân cấp vì các loại đậu có chứa hoặc kế thừa (inherit) tất cả đặc tính của rau củ (ví dụ: chúng là thực vật và có thể ăn được). Tương tự, đậu Hà Lan, đậu hạt và đậu lăng đều có các đặc tính của cây họ đậu kèm theo đặc tính riêng.
Hãy xem bạn sẽ thể hiện mối quan hệ này như thế nào bằng thuật ngữ lập trình. Nếu đặt Vegetable
làm một lớp (class) trong Kotlin, bạn có thể tạo Legume
làm con (child) hoặc lớp con (subclass) của lớp Vegetable
. Tức là tất cả thuộc tính và phương thức của lớp Vegetable
đều được kế thừa (nghĩa là cũng có sẵn trong) trong lớp Legume
.
Bạn có thể trình bày thông tin này trong một sơ đồ hệ phân cấp lớp (class hierarchy) như minh hoạ dưới đây. Bạn có thể gọi Vegetable
là lớp cha (parent) hoặc siêu lớp (superclass) của lớp Legume
.
Bạn có thể tiếp tục và mở rộng hệ phân cấp lớp bằng cách tạo các lớp con của Legume
như Lentil
và Chickpea
. Điều này khiến Legume
vừa là con hay lớp con của Vegetable
, cũng vừa là lớp cha hoặc siêu lớp của Lentil
và Chickpea
. Vegetable
là lớp gốc hoặc cấp cao nhất (hoặc cơ sở) của hệ phân cấp này.
Tính kế thừa trong lớp Android
Mặc dù bạn có thể viết mã Kotlin mà không cần sử dụng các lớp và bạn cũng từng làm như vậy trong các lớp học lập trình trước đây, nhưng nhiều phần của Android được cung cấp cho bạn dưới dạng các lớp, trong đó có hoạt động (activity), khung hiển thị (view) và nhóm khung hiển thị (view group). Do đó, hệ phân cấp lớp là yếu tố cơ bản cho việc phát triển ứng dụng Android và cho phép bạn tận dụng các tính năng do khung Android cung cấp.
Ví dụ: lớp View
trong Android biểu thị một vùng hình chữ nhật trên màn hình, đồng thời chịu trách nhiệm vẽ và xử lý sự kiện. Lớp TextView
là lớp con của lớp View
, tức là TextView
kế thừa tất cả thuộc tính và chức năng của lớp View
, đồng thời bổ sung thêm logic cụ thể để hiển thị văn bản cho người dùng.
Nếu đi sâu hơn thì các lớp EditText
và Button
là con của lớp TextView
. Các lớp này kế thừa tất cả thuộc tính và phương thức của các lớp TextView
và View
, đồng thời thêm logic riêng của từng lớp. Ví dụ: EditText
thêm chức năng riêng để có thể chỉnh sửa văn bản trên màn hình.
Thay vì phải sao chép và dán tất cả logic từ các lớp View
và TextView
vào lớp EditText
, EditText
chỉ cần là lớp con của lớp TextView
(và lớp này lại là con của lớp View
). Sau đó, mã trong lớp EditText
có thể tập trung cụ thể vào những điểm khiến thành phần giao diện người dùng này khác với các khung hiển thị khác.
Ở đầu trang tài liệu dành cho một lớp Android trên trang web developer.android.com, bạn có thể thấy sơ đồ hệ phân cấp lớp. Nếu bạn thấy kotlin.Any
ở đầu hệ phân cấp, thì đó là vì trong Kotlin, mọi lớp đều có một siêu lớp chung là Any. Tìm hiểu thêm tại đây.
Như bạn có thể thấy, việc học cách khai thác tính kế thừa giữa các lớp có thể giúp bạn viết, sử dụng lại, đọc và kiểm thử mã dễ dàng hơn.
3. Tạo lớp cơ sở
Hệ phân cấp lớp nhà ở
Trong lớp học lập trình này, bạn sẽ xây dựng một chương trình Kotlin để trình bày cách hoạt động của hệ phân cấp lớp, sử dụng nhà ở (dwelling) – nơi mọi người cư trú và sinh hoạt và có không gian sàn, các tầng và cư dân để làm ví dụ.
Dưới đây là sơ đồ hệ phân cấp lớp mà bạn sẽ tạo. Ở gốc, bạn có một Dwelling
xác định các thuộc tính và chức năng đúng với mọi ngôi nhà, tương tự như bản thiết kế. Sau đó, bạn sẽ có các lớp của một nhà gỗ vuông (SquareCabin
), một lều tròn (RoundHut
) và một tháp tròn (RoundTower
) là một RoundHut
có nhiều tầng.
Các lớp mà bạn sẽ triển khai:
Dwelling
: một lớp cơ sở đại diện cho một chỗ ở không cụ thể, chứa thông tin chung của tất cả nhà ở.SquareCabin
: một nhà gỗ vuông có khu vực sàn hình vuông.RoundHut
: một lều tròn được làm bằng rơm rạ có khu vực sàn tròn và là lớp cha củaRoundTower
.RoundTower
: một tháp tròn được làm bằng đá, có khu vực sàn tròn và nhiều tầng.
Tạo lớp nhà ở trừu tượng
Lớp nào cũng có thể là lớp cơ sở của hệ phân cấp lớp hay lớp mẹ của các lớp khác.
Lớp "trừu tượng" (abstract) là một lớp không thể tạo thực thể vì không được triển khai đầy đủ. Bạn có thể xem đây là một bản phác thảo. Một bản phác thảo kết hợp các ý tưởng và kế hoạch cho điều gì đó, nhưng thường thì không đủ thông tin để xây dựng. Bạn sử dụng bản phác thảo (lớp trừu tượng) để tạo bản thiết kế (lớp) mà bạn dùng để tạo thực thể đối tượng thực tế.
Lợi ích chung của việc tạo siêu lớp là để chứa các thuộc tính và hàm chung cho tất cả lớp con. Nếu không xác định được giá trị của các thuộc tính và cách triển khai hàm, lớp đó trở thành trừu tượng. Ví dụ: Vegetables
có nhiều thuộc tính phổ biến đối với mọi loại rau củ, nhưng bạn không thể tạo một thực thể của một loại rau không cụ thể, vì bạn không biết hình dạng hay màu sắc của loại rau đó chẳng hạn. Vì vậy, Vegetable
là một lớp trừu tượng mà phải phụ thuộc vào các lớp con để xác định các chi tiết cụ thể về từng loại rau.
Phần khai báo của lớp trừu tượng bắt đầu bằng từ khoá abstract
.
Dwelling
sẽ là một lớp trừu tượng như Vegetable
. Lớp này sẽ chứa các thuộc tính và hàm phổ biến ở nhiều loại nhà ở, nhưng giá trị chính xác của các thuộc tính và chi tiết triển khai hàm thì không được xác định.
- Truy cập Kotlin Playground tại https://developer.android.com/training/kotlinplayground.
- Trong trình chỉnh sửa, hãy xoá
println("Hello, world!")
bên trong hàmmain()
. - Sau đó, hãy thêm mã này bên dưới hàm
main()
để tạo một lớpabstract
có tênDwelling
.
abstract class Dwelling(){
}
Thêm một thuộc tính cho vật liệu xây dựng
Trong lớp Dwelling
này, bạn định nghĩa những thứ có sẵn cho tất cả nhà ở, ngay cả khi những thứ này có đặc điểm riêng tuỳ theo nhà. Tất cả ngôi nhà đều được làm bằng một số vật liệu xây dựng.
- Bên trong
Dwelling
, hãy tạo biếnbuildingMaterial
thuộc kiểuString
để biểu thị vật liệu xây dựng. Vì vật liệu xây dựng sẽ không thay đổi, hãy sử dụngval
để biến này là một biến không thể thay đổi (immutable variable).
val buildingMaterial: String
- Hãy chạy chương trình và bạn sẽ gặp lỗi này.
Property must be initialized or be abstract
Thuộc tính buildingMaterial
không có giá trị. Trên thực tế, bạn KHÔNG THỂ cung cấp cho thuộc tính này một giá trị, bởi vì một toà nhà không cụ thể thì không được làm bằng bất cứ thứ gì cụ thể. Vì vậy, như thông báo lỗi đã chỉ ra, bạn có thể đặt tiền tố khai báo của buildingMaterial
bằng từ khoá abstract
để cho biết buildingMaterial sẽ không được định nghĩa ở đây.
- Hãy thêm từ khoá
abstract
vào phần định nghĩa biến.
abstract val buildingMaterial: String
- Chạy mã của bạn, và tuy không làm gì nhưng mã vẫn biên dịch mà không gặp lỗi.
- Tạo một thực thể của
Dwelling
trong hàmmain()
rồi chạy mã.
val dwelling = Dwelling()
- Bạn sẽ gặp lỗi vì không thể tạo một thực thể của lớp
Dwelling
trừu tượng.
Cannot create an instance of an abstract class
- Xoá mã không chính xác này.
Mã của bạn cho đến thời điểm này:
abstract class Dwelling(){
abstract val buildingMaterial: String
}
Thêm một thuộc tính cho sức chứa
Một thuộc tính khác của nhà ở là sức chứa (capacity), tức là số lượng người có thể ở trong đó.
Tất cả nhà ở đều có sức chứa không thay đổi. Tuy nhiên, sức chứa không thể được đặt trong siêu lớp Dwelling
. Sức chứa phải được khai báo trong các lớp con tương ứng với từng loại nhà ở cụ thể.
- Trong
Dwelling
, hãy thêm mộtabstract
số nguyênval
có têncapacity
.
abstract val capacity: Int
Thêm một thuộc tính riêng tư cho số lượng cư dân
Mọi ngôi nhà đều có một số residents
cư trú trong nhà (có thể nhỏ hơn hoặc bằng capacity
), vì vậy hãy định nghĩa thuộc tính residents
trong siêu lớp Dwelling
để tất cả lớp con kế thừa và sử dụng.
- Bạn có thể đặt
residents
làm một tham số được truyền vào hàm khởi tạo của lớpDwelling
. Thuộc tínhresidents
làvar
, vì số lượng cư dân có thể thay đổi sau khi thực thể được tạo.
abstract class Dwelling(private var residents: Int) {
Xin lưu ý rằng thuộc tính residents
được đánh dấu bằng từ khoá private
. Riêng tư (private) là một đối tượng sửa đổi chế độ hiển thị (visibility modifier) trong Kotlin, có nghĩa là thuộc tính residents
chỉ hiển thị (và dùng được) trong lớp này. Bạn không thể truy cập vào thuộc tính này ở nơi khác trong chương trình. Bạn có thể đánh dấu các thuộc tính hoặc phương thức bằng từ khoá private. Nếu bạn không chỉ định đối tượng sửa đổi chế độ hiển thị thì các thuộc tính và phương thức sẽ có giá trị là public
theo mặc định, đồng thời bạn có thể truy cập vào qua các phần khác trong chương trình. Vì số người sống trong nhà thường là thông tin riêng tư (so với thông tin về vật liệu xây dựng hoặc sức chứa của toà nhà), nên đây là một quyết định hợp lý.
Cả capacity
của nhà ở và số lượng residents
hiện tại đều đã được định nghĩa, nên bạn có thể tạo hàm hasRoom()
để xác định xem có còn chỗ ở cho người cư trú khác trong nhà hay không. Bạn có thể định nghĩa và triển khai hàm hasRoom()
trong lớp Dwelling
vì công thức tính xem có còn chỗ trống hay không có thể áp dụng cho tất cả nhà ở. Có chỗ trong Dwelling
nếu số residents
nhỏ hơn capacity
, và hàm phải trả về true
hoặc false
dựa trên phép so sánh này.
- Thêm hàm
hasRoom()
vào lớpDwelling
.
fun hasRoom(): Boolean {
return residents < capacity
}
- Bạn có thể chạy mã này và không gặp lỗi. Hiện bạn chưa thấy gì.
Mã hoàn tất của bạn hiện có dạng như sau:
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
4. Tạo lớp con
Tạo một lớp con SquareCabin
- Bên dưới lớp
Dwelling
, hãy tạo một lớp có tênSquareCabin
.
class SquareCabin
- Tiếp theo, bạn cần chỉ ra rằng
SquareCabin
có liên quan đếnDwelling
. Trong mã, bạn nên chỉ ra rằngSquareCabin
mở rộng từDwelling
(hoặc là lớp con củaDwelling)
vìSquareCabin
sẽ đưa ra phương thức triển khai cho các phần trừu tượng củaDwelling
.
Chỉ ra mối quan hệ kế thừa này bằng cách thêm dấu hai chấm (:
) sau tên lớp SquareCabin
, theo sau là một lệnh gọi để khởi tạo lớp cha Dwelling
. Đừng quên thêm các dấu ngoặc đơn sau tên lớp Dwelling
.
class SquareCabin : Dwelling()
- Khi mở rộng từ một siêu lớp, bạn phải truyền vào các tham số cần thiết mà siêu lớp đó dự kiến có.
Dwelling
cần có sốresidents
làm dữ liệu đầu vào. Bạn có thể truyền vào một số lượng cư dân cố định như3
.
class SquareCabin : Dwelling(3)
Tuy nhiên, chương trình của bạn nên linh hoạt hơn và cho phép thay đổi số lượng cư dân trong SquareCabins
. Do đó, hãy đặt residents
làm một tham số trong định nghĩa lớp SquareCabin
. Đừng khai báo residents
là val,
vì bạn đang sử dụng lại một thuộc tính đã được khai báo trong lớp cha Dwelling
.
class SquareCabin(residents: Int) : Dwelling(residents)
- Chạy mã.
- Việc này sẽ gây ra lỗi. Hãy xem:
Class 'SquareCabin' is not abstract and does not implement abstract base class member public abstract val buildingMaterial: String defined in Dwelling
Khi bạn khai báo các hàm và biến trừu tượng, điều này giống như hứa hẹn rằng sau này bạn sẽ cung cấp giá trị và phương thức triển khai cho những đối tượng này. Đối với một biến, như vậy tức là mọi lớp con của lớp trừu tượng đó đều phải cung cấp một giá trị cho biến đó. Đối với một hàm, như vậy tức là mọi lớp con đều phải triển khai nội dung của hàm.
Trong lớp Dwelling
, bạn đã khai báo một biến abstract
là buildingMaterial
. SquareCabin
là lớp con của Dwelling
, vì vậy, lớp này phải cung cấp giá trị cho buildingMaterial
. Hãy sử dụng từ khoá override
để cho biết thuộc tính này đã được định nghĩa trong một lớp cha và sẽ bị ghi đè trong lớp này.
- Bên trong lớp
SquareCabin
, hãyoverride
thuộc tínhbuildingMaterial
rồi gán cho thuộc tính này giá trị"Wood"
. - Làm tương tự với
capacity
, giả sử 6 cư dân có thể sống trongSquareCabin
.
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
Mã hoàn thiện của bạn sẽ có dạng như sau.
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
Để kiểm thử mã, hãy tạo một thực thể của SquareCabin
trong chương trình.
Sử dụng SquareCabin
- Chèn một hàm
main()
trống trước các định nghĩa lớpDwelling
vàSquareCabin
.
fun main() {
}
abstract class Dwelling(private var residents: Int) {
...
}
class SquareCabin(residents: Int) : Dwelling(residents) {
...
}
- Trong hàm
main()
, hãy tạo một thực thể củaSquareCabin
có tênsquareCabin
gồm 6 cư dân. Thêm các câu lệnh in (print statement) dành cho vật liệu xây dựng, sức chứa và hàmhasRoom()
.
fun main() {
val squareCabin = SquareCabin(6)
println("\nSquare Cabin\n============")
println("Capacity: ${squareCabin.capacity}")
println("Material: ${squareCabin.buildingMaterial}")
println("Has room? ${squareCabin.hasRoom()}")
}
Xin lưu ý rằng hàm hasRoom()
không được định nghĩa trong lớp SquareCabin
nhưng đã được định nghĩa trong lớp Dwelling
. Vì SquareCabin
là lớp con của lớp Dwelling
, nên hàm hasRoom()
được kế thừa tự do. Hàm hasRoom()
nay có thể được gọi trên tất cả thực thể của SquareCabin
, như đã thấy trong đoạn mã dưới dạng squareCabin.hasRoom()
.
- Chạy mã của bạn và mã sẽ in nội dung sau:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false
Bạn đã tạo squareCabin
với 6
cư dân, bằng với capacity
, vì vậy hasRoom()
sẽ trả về false
. Bạn có thể thử nghiệm việc khởi tạo SquareCabin
với số lượng residents
ít hơn. Khi bạn chạy lại chương trình, hasRoom()
sẽ trả về true
.
Sử dụng with để đơn giản hoá mã
Trong các câu lệnh println()
, mỗi khi bạn tham chiếu đến một thuộc tính hoặc hàm của squareCabin
, hãy lưu ý cách bạn phải lặp lại squareCabin.
Việc này có tính lặp đi lặp lại và có thể là nguồn lỗi khi bạn sao chép và dán các câu lệnh in.
Khi đang xử lý một thực thể cụ thể của một lớp và cần truy cập vào nhiều thuộc tính và chức năng của thực thể đó, bạn có thể sử dụng câu lệnh with
để nói rằng "thực hiện tất cả thao tác sau đây cho đối tượng thực thể này". Bắt đầu bằng từ khoá with
, theo sau là tên thực thể trong các dấu ngoặc đơn, sau đó là các dấu ngoặc nhọn chứa các thao tác bạn muốn thực hiện.
with (instanceName) {
// all operations to do with instanceName
}
- Trong hàm
main()
, hãy thay đổi các câu lệnh in để sử dụngwith
. - Xoá
squareCabin.
trong các câu lệnh in.
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
- Chạy lại mã của bạn để đảm bảo mã đó chạy không có lỗi và cho thấy cùng một kết quả.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false
Đây là mã đã hoàn tất của bạn:
fun main() {
val squareCabin = SquareCabin(6)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
Tạo một lớp con RoundHut
- Tương tự như
SquareCabin
, hãy thêm một lớp con khác (RoundHut
) vàoDwelling
. - Ghi đè
buildingMaterial
rồi đặt giá trị"Straw"
. - Ghi đè
capacity
rồi đặt thành 4.
class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- Trong
main()
, hãy tạo một thực thể củaRoundHut
có 3 cư dân.
val roundHut = RoundHut(3)
- Thêm mã dưới đây để in thông tin về
roundHut
.
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
- Chạy mã của bạn. Kết quả đầu ra cho toàn bộ chương trình sẽ là:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true
Bây giờ, bạn sẽ có một hệ phân cấp lớp có dạng thế này, với Dwelling
là lớp gốc còn SquareCabin
và RoundHut
là lớp con của Dwelling
.
Tạo một lớp con RoundTower
Lớp cuối cùng trong hệ phân cấp lớp này là một toà tháp hình tròn. Bạn có thể coi tháp tròn là một túp lều tròn bằng đá, có nhiều tầng. Do đó, bạn có thể đặt RoundTower
làm lớp con của RoundHut
.
- Tạo một lớp
RoundTower
là lớp con củaRoundHut
. Thêm tham sốresidents
vào hàm khởi tạo củaRoundTower
, sau đó truyền tham số đó vào hàm khởi tạo của siêu lớpRoundHut
. - Ghi đè
buildingMaterial
thành"Stone"
. - Đặt
capacity
thành4
.
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- Chạy mã này và bạn sẽ gặp lỗi.
This type is final, so it cannot be inherited from
Lỗi này nghĩa là không thể tạo lớp con của (hoặc kế thừa từ) lớp RoundHut
. Theo mặc định, trong Kotlin, các lớp đều là lớp cuối cùng và không thể tạo lớp con được. Bạn chỉ được phép kế thừa từ các lớp abstract
hoặc các lớp được đánh dấu bằng từ khoá open
. Do đó, bạn cần đánh dấu lớp RoundHut
bằng từ khoá open
để cho phép các lớp khác có thể kế thừa từ lớp này.
- Thêm từ khoá
open
vào đầu phần khai báoRoundHut
.
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
- Trong
main()
, hãy tạo một thực thể củaroundTower
và in thông tin về thực thể đó.
val roundTower = RoundTower(4)
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
Sau đây là mã hoàn chỉnh.
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(3)
val roundTower = RoundTower(4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(residents: Int) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4
}
- Chạy mã. Mã này đã hoạt động được mà không xảy ra lỗi và tạo ra kết quả sau đây.
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Round Hut ========= Material: Straw Capacity: 4 Has room? true Round Tower ========== Material: Stone Capacity: 4 Has room? false
Thêm nhiều tầng vào RoundTower
RoundHut
ám chỉ một toà nhà một tầng. Các toà tháp thường có nhiều tầng.
Khi nói đến sức chứa, toà tháp càng có nhiều tầng thì sức chứa càng lớn.
Bạn có thể sửa đổi RoundTower
để có nhiều tầng và điều chỉnh sức chứa dựa trên số tầng.
- Cập nhật hàm khởi tạo
RoundTower
để lấy thêm một tham số dạng số nguyênval floors
cho số tầng. Đặt sauresidents
. Lưu ý rằng bạn không cần truyền giá trị này cho hàm khởi tạoRoundHut
của lớp cha vìfloors
được định nghĩa ở đây trongRoundTower
cònRoundHut
không cófloors
.
class RoundTower(
residents: Int,
val floors: Int) : RoundHut(residents) {
...
}
- Chạy mã. Đã xảy ra lỗi khi tạo
roundTower
trong phương thứcmain()
vì bạn chưa cung cấp một con số cho đối sốfloors
. Bạn có thể thêm đối số bị thiếu.
Ngoài ra, trong phần định nghĩa lớp RoundTower
, bạn có thể thêm giá trị mặc định cho floors
như minh hoạ dưới đây. Sau đó, khi không có giá trị nào cho floors
được truyền vào hàm khởi tạo, hệ thống có thể sử dụng giá trị mặc định để tạo thực thể đối tượng.
- Trong mã, hãy thêm
= 2
sau phần khai báo củafloors
để gán cho đối tượng này một giá trị mặc định là 2.
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
...
}
- Chạy mã. Mã này phải biên dịch được vì
RoundTower(4)
hiện tạo một thực thể đối tượngRoundTower
có giá trị mặc định là 2 tầng. - Trong lớp
RoundTower
, hãy cập nhậtcapacity
để nhân với số tầng.
override val capacity = 4 * floors
- Chạy mã và nhận thấy sức chứa của
RoundTower
giờ đây là 8 cho 2 tầng.
Sau đây là mã đã hoàn tất.
fun main() {
val squareCabin = SquareCabin(6)
val roundHut = RoundHut(3)
val roundTower = RoundTower(4)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
}
class SquareCabin(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
}
open class RoundHut(residents: Int) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
}
class RoundTower(
residents: Int,
val floors: Int = 2) : RoundHut(residents) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
}
5. Sửa đổi lớp trong hệ phân cấp
Tính diện tích sàn
Trong bài tập này, bạn sẽ tìm hiểu cách khai báo một hàm trừu tượng trong một lớp trừu tượng và sau đó triển khai chức năng của hàm đó trong các lớp con.
Mọi ngôi nhà đều có diện tích sàn (floor area), tuy nhiên cách tính diện tích sàn thì còn tuỳ thuộc vào hình dạng nhà ở.
Khai báo floorArea() trong lớp Dwelling
- Trước tiên, hãy thêm hàm
abstract
floorArea()
vào lớpDwelling
. Trả về mộtDouble
. Double là một kiểu dữ liệu, nhưString
vàInt
; kiểu dữ liệu này được sử dụng cho số thập phân, tức là các số có dấu thập phân theo sau là phần thập phân, chẳng hạn như 5,8793.
abstract fun floorArea(): Double
Tất cả phương thức trừu tượng được khai báo trong một lớp trừu tượng đều phải được triển khai trong mọi lớp con của lớp đó. Trước khi có thể chạy mã, bạn cần triển khai floorArea()
trong các lớp con.
Triển khai floorArea() cho SquareCabin
Giống như buildingMaterial
và capacity
, vì bạn đang triển khai hàm abstract
được định nghĩa trong lớp cha, nên bạn cần sử dụng từ khoá override
.
- Trong lớp
SquareCabin
, hãy bắt đầu bằng từ khoáoverride
, sau đó triển khai thực tế hàmfloorArea()
như minh hoạ dưới đây.
override fun floorArea(): Double {
}
- Trả về diện tích sàn đã tính. Diện tích của một hình chữ nhật hoặc hình vuông là chiều dài của một cạnh nhân với chiều dài của cạnh còn lại. Nội dung của hàm sẽ
return length * length
.
override fun floorArea(): Double {
return length * length
}
Chiều dài (length) không phải là biến trong lớp và còn tuỳ theo thực thể, vì vậy bạn có thể thêm chiều dài làm tham số hàm khởi tạo cho lớp SquareCabin
.
- Thay đổi định nghĩa lớp
SquareCabin
để thêm tham sốlength
thuộc kiểuDouble
. Khai báo thuộc tính dưới dạngval
vì chiều dài của một toà nhà không thay đổi.
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
Dwelling
và tất cả lớp con của lớp này có residents
làm đối số hàm khởi tạo. Vì đây là đối số đầu tiên trong hàm khởi tạo Dwelling
, nên phương pháp hay nhất là đặt đối số này làm đối số đầu tiên trong mọi hàm khởi tạo của các lớp con, đồng thời đặt các đối số theo cùng thứ tự trong mọi định nghĩa lớp. Do đó, hãy chèn tham số length
mới sau tham số residents
.
- Trong
main()
, hãy cập nhật việc tạo thực thể chosquareCabin
. Truyền50.0
vào hàm khởi tạoSquareCabin
làmlength
.
val squareCabin = SquareCabin(6, 50.0)
- Bên trong câu lệnh
with
chosquareCabin
, hãy thêm câu lệnh in diện tích sàn.
println("Floor area: ${floorArea()}")
Mã của bạn sẽ không chạy bởi vì bạn còn phải triển khai floorArea()
trong RoundHut
.
Triển khai floorArea() cho RoundHut
Tương tự như vậy, hãy triển khai diện tích sàn (floor area) cho RoundHut
. RoundHut
cũng là lớp con trực tiếp của Dwelling
, vì vậy, bạn cần sử dụng từ khoá override
.
Diện tích sàn của một ngôi nhà tròn là PI * bán kính * bán kính.
PI
là một giá trị toán học. Giá trị này được xác định trong một thư viện toán học (math library). Thư viện (library) là một tập hợp hàm và giá trị xác định trước mà chương trình có thể sử dụng. Các hàm và giá trị này được định nghĩa bên ngoài chương trình. Để sử dụng hàm hoặc giá trị thư viện, bạn cần cho trình biên dịch (compiler) biết rằng bạn sẽ sử dụng hàm hoặc giá trị đó. Bạn thực hiện việc này bằng cách nhập hàm hoặc giá trị vào chương trình của mình. Để sử dụng PI
trong chương trình, bạn cần nhập kotlin.math.PI
.
- Nhập
PI
từ thư viện toán học Kotlin Đặt phần này ở đầu tệp, trướcmain()
.
import kotlin.math.PI
- Triển khai hàm
floorArea()
choRoundHut
.
override fun floorArea(): Double {
return PI * radius * radius
}
Cảnh báo: Nếu bạn không nhập kotlin.math.PI, bạn sẽ gặp lỗi, vì vậy, hãy nhập thư viện này trước khi sử dụng. Ngoài ra, bạn có thể viết ra phiên bản PI đủ điều kiện, như trong kotlin.math.PI * bán kính * bán kính, sau đó không cần câu lệnh nhập.
- Cập nhật hàm khởi tạo
RoundHut
để truyềnradius
.
open class RoundHut(
residents: Int,
val radius: Double) : Dwelling(residents) {
- Trong
main()
, hãy cập nhật quá trình khởi tạoroundHut
bằng cách truyềnradius
là10.0
đến hàm khởi tạoRoundHut
.
val roundHut = RoundHut(3, 10.0)
- Thêm một câu lệnh in bên trong câu lệnh
with
choroundHut
.
println("Floor area: ${floorArea()}")
Triển khai floorArea() cho RoundTower
Mã của bạn chưa chạy và gặp lỗi này:
Error: No value passed for parameter 'radius'
Trong RoundTower
, để chương trình của bạn biên dịch được, bạn không cần triển khai floorArea()
vì floorArea() được kế thừa từ RoundHut
, nhưng bạn cần cập nhật định nghĩa lớp RoundTower
để có cùng đối số radius
với lớp cha RoundHut
.
- Thay đổi hàm khởi tạo của RoundTower để lấy cả
radius
. Đặtradius
sauresidents
và trướcfloors
. Bạn nên liệt kê các biến có giá trị mặc định ở cuối. Đừng quên truyềnradius
đến hàm khởi tạo lớp mẹ.
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
- Cập nhật quá trình khởi tạo của
roundTower
trongmain()
.
val roundTower = RoundTower(4, 15.5)
- Và thêm câu lệnh in để gọi
floorArea()
.
println("Floor area: ${floorArea()}")
- Bây giờ, bạn có thể chạy mã!
- Xin lưu ý rằng cách tính cho
RoundTower
là không chính xác vì được kế thừa từRoundHut
và không tính đến sốfloors
. - Trong
RoundTower
, hãyoverride floorArea()
để có thể triển khai theo một cách khác: nhân diện tích sàn với số tầng. Hãy lưu ý cách bạn có thể định nghĩa hàm trong một lớp trừu tượng (Dwelling
), triển khai hàm này trong lớp con (RoundHut
) rồi ghi đè lại trong lớp con của lớp con (RoundTower
). Đây là điểm ưu việt nhất của cả hai phương thức – bạn kế thừa chức năng mà bạn muốn đồng thời có thể ghi đè chức năng mà bạn không muốn.
override fun floorArea(): Double {
return PI * radius * radius * floors
}
Mã này hoạt động, nhưng có một cách để tránh việc lặp lại mã đã có trong lớp mẹ RoundHut
. Bạn có thể gọi hàm floorArea()
từ lớp cha RoundHut
để trả về PI * radius * radius
. Sau đó, nhân kết quả đó với số floors
.
- Trong
RoundTower
, hãy cập nhậtfloorArea()
để sử dụng phương thức triển khai siêu lớpfloorArea()
. Sử dụng từ khoásuper
để gọi hàm được khai báo trong lớp mẹ.
override fun floorArea(): Double {
return super.floorArea() * floors
}
- Chạy lại mã và
RoundTower
tính ra diện tích sàn chính xác cho nhiều tầng.
Sau đây là mã hoàn tất của bạn:
import kotlin.math.PI
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(3, 10.0)
val roundTower = RoundTower(4, 15.5)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Has room? ${hasRoom()}")
println("Floor area: ${floorArea()}")
}
}
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
fun hasRoom(): Boolean {
return residents < capacity
}
abstract fun floorArea(): Double
}
class SquareCabin(residents: Int,
val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
override fun floorArea(): Double {
return length * length
}
}
open class RoundHut(residents: Int,
val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
override fun floorArea(): Double {
return PI * radius * radius
}
}
class RoundTower(residents: Int, radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
override val capacity = 4 * floors
override fun floorArea(): Double {
return super.floorArea() * floors
}
}
Kết quả đầu ra phải là:
Square Cabin ============ Capacity: 6 Material: Wood Has room? false Floor area: 2500.0 Round Hut ========= Material: Straw Capacity: 4 Has room? true Floor area: 314.1592653589793 Round Tower ========== Material: Stone Capacity: 8 Has room? true Floor area: 1509.5352700498956
Cho phép người cư trú mới nhận phòng
Thêm khả năng cho một cư dân mới nhận phòng bằng hàm getRoom()
làm tăng số lượng cư dân thêm một. Vì logic này là như nhau đối với tất cả nhà ở, nên bạn có thể triển khai hàm này trong Dwelling
. Điều này làm cho hàm getRoom() được cung cấp cho tất cả lớp con và lớp con của lớp con. Tuyệt!
Lưu ý:
- Sử dụng một câu lệnh
if
chỉ thêm cư dân nếu còn đủ sức chứa. - In một thông điệp cho kết quả.
- Bạn có thể sử dụng
residents++
(là viết tắt củaresidents = residents + 1
) để cộng 1 vào biếnresidents
.
- Triển khai hàm
getRoom()
trong lớpDwelling
.
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
- Thêm một số câu lệnh in vào khối câu lệnh
with
choroundHut
để quan sát xem điều gì xảy ra chogetRoom()
vàhasRoom()
khi được sử dụng cùng nhau.
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()
Kết quả đầu ra cho các câu lệnh in như sau:
Has room? true You got a room! Has room? false Sorry, at capacity and no rooms left.
Hãy xem mã giải pháp để biết thông tin chi tiết.
Đặt một tấm thảm vừa với một ngôi nhà tròn
Giả sử bạn cần biết chiều dài một cạnh của thảm cho RoundHut
hoặc RoundTower
. Đặt hàm này vào RoundHut
để cung cấp cho tất cả ngôi nhà tròn.
- Trước tiên, hãy nhập hàm
sqrt()
từ thư việnkotlin.math
.
import kotlin.math.sqrt
- Triển khai hàm
calculateMaxCarpetLength()
trong lớpRoundHut
. Công thức tính chiều dài của hình vuông có thể nằm trong hình tròn làsqrt(2) * radius
. Điều này được giải thích trong sơ đồ trên.
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
Truyền giá trị Double
, 2.0
đến hàm toán học sqrt(2.0)
, vì loại trả về của hàm là Double
không phải Integer
.
- Phương thức
calculateMaxCarpetLength()
hiện có thể được gọi trên các thực thểRoundHut
vàRoundTower
. Thêm các câu lệnh in vàoroundHut
vàroundTower
trong hàmmain()
.
println("Carpet Length: ${calculateMaxCarpetLength()}")
Hãy xem mã giải pháp để biết thông tin chi tiết.
Xin chúc mừng! Bạn đã tạo được một hệ phân cấp lớp hoàn chỉnh với các thuộc tính và hàm, giờ hãy tìm hiểu mọi thứ bạn cần để tạo những lớp hữu ích hơn!
6. Mã giải pháp
Đây là mã giải pháp hoàn chỉnh cho lớp học lập trình này, bao gồm cả nhận xét.
/**
* Program that implements classes for different kinds of dwellings.
* Shows how to:
* Create class hierarchy, variables and functions with inheritance,
* abstract class, overriding, and private vs. public variables.
*/
import kotlin.math.PI
import kotlin.math.sqrt
fun main() {
val squareCabin = SquareCabin(6, 50.0)
val roundHut = RoundHut(3, 10.0)
val roundTower = RoundTower(4, 15.5)
with(squareCabin) {
println("\nSquare Cabin\n============")
println("Capacity: ${capacity}")
println("Material: ${buildingMaterial}")
println("Floor area: ${floorArea()}")
}
with(roundHut) {
println("\nRound Hut\n=========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Floor area: ${floorArea()}")
println("Has room? ${hasRoom()}")
getRoom()
println("Has room? ${hasRoom()}")
getRoom()
println("Carpet size: ${calculateMaxCarpetLength()}")
}
with(roundTower) {
println("\nRound Tower\n==========")
println("Material: ${buildingMaterial}")
println("Capacity: ${capacity}")
println("Floor area: ${floorArea()}")
println("Carpet Length: ${calculateMaxCarpetLength()}")
}
}
/**
* Defines properties common to all dwellings.
* All dwellings have floorspace,
* but its calculation is specific to the subclass.
* Checking and getting a room are implemented here
* because they are the same for all Dwelling subclasses.
*
* @param residents Current number of residents
*/
abstract class Dwelling(private var residents: Int) {
abstract val buildingMaterial: String
abstract val capacity: Int
/**
* Calculates the floor area of the dwelling.
* Implemented by subclasses where shape is determined.
*
* @return floor area
*/
abstract fun floorArea(): Double
/**
* Checks whether there is room for another resident.
*
* @return true if room available, false otherwise
*/
fun hasRoom(): Boolean {
return residents < capacity
}
/**
* Compares the capacity to the number of residents and
* if capacity is larger than number of residents,
* add resident by increasing the number of residents.
* Print the result.
*/
fun getRoom() {
if (capacity > residents) {
residents++
println("You got a room!")
} else {
println("Sorry, at capacity and no rooms left.")
}
}
}
/**
* A square cabin dwelling.
*
* @param residents Current number of residents
* @param length Length
*/
class SquareCabin(residents: Int, val length: Double) : Dwelling(residents) {
override val buildingMaterial = "Wood"
override val capacity = 6
/**
* Calculates floor area for a square dwelling.
*
* @return floor area
*/
override fun floorArea(): Double {
return length * length
}
}
/**
* Dwelling with a circular floorspace
*
* @param residents Current number of residents
* @param radius Radius
*/
open class RoundHut(
residents: Int, val radius: Double) : Dwelling(residents) {
override val buildingMaterial = "Straw"
override val capacity = 4
/**
* Calculates floor area for a round dwelling.
*
* @return floor area
*/
override fun floorArea(): Double {
return PI * radius * radius
}
/**
* Calculates the max length for a square carpet
* that fits the circular floor.
*
* @return length of square carpet
*/
fun calculateMaxCarpetLength(): Double {
return sqrt(2.0) * radius
}
}
/**
* Round tower with multiple stories.
*
* @param residents Current number of residents
* @param radius Radius
* @param floors Number of stories
*/
class RoundTower(
residents: Int,
radius: Double,
val floors: Int = 2) : RoundHut(residents, radius) {
override val buildingMaterial = "Stone"
// Capacity depends on the number of floors.
override val capacity = floors * 4
/**
* Calculates the total floor area for a tower dwelling
* with multiple stories.
*
* @return floor area
*/
override fun floorArea(): Double {
return super.floorArea() * floors
}
}
7. Tóm tắt
Trong lớp học lập trình này, bạn đã tìm hiểu cách:
- Tạo một hệ phân cấp lớp, còn gọi là cây phả hệ lớp, nơi lớp con kế thừa chức năng của lớp cha. Các thuộc tính và hàm được kế thừa trong các lớp con.
- Tạo một lớp
abstract
mà trong đó một số chức năng còn lại sẽ được các lớp con của lớp đó thực hiện. Do đó, bạn không thể tạo thực thể cho một lớpabstract
. - Tạo lớp con của lớp
abstract
. - Sử dụng từ khoá
override
để ghi đè thuộc tính và hàm trong lớp con. - Sử dụng từ khoá
super
để tham chiếu các hàm và thuộc tính trong lớp cha. - Làm cho một lớp trở thành
open
để có thể tạo lớp con. - Làm cho một thuộc tính trở thành
private
để chỉ có thể được sử dụng bên trong lớp đó. - Sử dụng cấu trúc
with
để thực hiện nhiều lệnh gọi trên cùng một thực thể đối tượng. - Nhập chức năng từ thư viện
kotlin.math
8. Tìm hiểu thêm
- Tính kế thừa
- Lớp và tính kế thừa
- Hàm dựng
- Tính kế thừa (trừu tượng, mở, ghi đè, riêng tư)
with
(định nghĩa chính thức)