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:

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:

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:

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:

Đơ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:

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:

để 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:

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:

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:

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

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:

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:

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:

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:

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:

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:

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:

Đơ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:

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

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

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:

Ở đâ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:

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

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

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.

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

Kết quả:

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:

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:

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

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.

Hãy chia sẻ

Trả lời

Email của bạn sẽ không được hiển thị công khai. Các trường bắt buộc được đánh dấu *