Buổi 5: Lập trình hướng đối tượng (1): lớp và đối tượng
Hãy tưởng tượng bạn được giao viết phần mềm cho một thư viện trường. Có hàng nghìn đầu sách, mỗi cuốn có tên, tác giả, năm xuất bản, trạng thái đang cho mượn hay còn trên kệ. Nếu chỉ dùng những gì đã học đến giờ, bạn sẽ phải khai báo hàng loạt mảng song song: một mảng tên sách, một mảng tác giả, một mảng năm, một mảng trạng thái… và mỗi lần thêm một thuộc tính mới (ví dụ mã vạch) bạn lại phải sửa mọi hàm đang dùng các mảng đó. Chỉ cần lệch một chỉ số, dữ liệu của cuốn sách này sẽ dính sang cuốn sách khác.
Lập trình hướng đối tượng (Object-Oriented Programming, OOP) giải quyết đúng nỗi đau này: thay vì rải dữ liệu thành nhiều mảng rời rạc, bạn gói dữ liệu và các thao tác trên dữ liệu đó vào cùng một "vật thể" — một cuốn sách biết tên của nó, biết trả lời "tôi có đang được mượn không", biết tự "đổi trạng thái" khi có người mượn. Buổi này bạn học cách mô tả những vật thể như vậy bằng lớp (class), tạo ra chúng bằng đối tượng (object), bảo vệ dữ liệu bên trong bằng đóng gói (encapsulation), và làm cho chúng "nói chuyện" được với phần còn lại của chương trình qua toString, equals, hashCode.
Mục tiêu buổi học
- Sau buổi này bạn có thể giải thích được vì sao cần OOP và kể tên bốn tính chất của nó (đóng gói, kế thừa, đa hình, trừu tượng).
- Sau buổi này bạn có thể khai báo một lớp với thuộc tính, phương thức, nhiều hàm tạo (có dùng
this) và tạo, sử dụng các đối tượng của lớp đó. - Sau buổi này bạn có thể áp dụng đóng gói: đặt thuộc tính
private, viết getter/setter có kiểm tra hợp lệ, chọn đúng phạm vi truy cập. - Sau buổi này bạn có thể phân biệt thành viên thuộc đối tượng với thành viên
static(thuộc lớp) và dùngstaticđúng chỗ (bộ đếm, hằng số, hàm tiện ích). - Sau buổi này bạn có thể ghi đè
toString,equals,hashCodeđúng quy ước và hiểu vì sao mọi lớp đều "thừa hưởng" từObject. - Sau buổi này bạn có thể thiết kế một chương trình nhỏ gồm vài lớp phối hợp với nhau (quan hệ "có một").
Bản đồ buổi học
| Phần | Nội dung | Thời lượng gợi ý |
|---|---|---|
| 1 | Từ lập trình thủ tục đến OOP, bốn tính chất | 15 phút |
| 2 | Lớp và đối tượng: thuộc tính, phương thức, tham chiếu | 25 phút |
| 3 | Hàm tạo: chồng hàm tạo, this |
25 phút (gồm thực hành) |
| 4 | Đóng gói: private, getter/setter, phạm vi truy cập |
30 phút (gồm thực hành) |
| 5 | Thành viên static |
25 phút (gồm thực hành) |
| 6 | Lớp Object: toString, equals, hashCode, Comparable |
30 phút (gồm thực hành) |
| 7 | Nhiều lớp phối hợp, nhìn trước kế thừa | 30 phút (thực hành tổng hợp) |
Khoảng 55–60% thời gian là thực hành: bạn gõ lại và sửa các ví dụ có nút Chạy thử, rồi làm các nhóm bài tập chấm tự động sau mỗi phần.
1. Từ lập trình thủ tục đến hướng đối tượng
1.1. Lập trình thủ tục: dữ liệu và hàm tách rời
Ở các buổi trước, chương trình của bạn gồm các biến và các hàm. Dữ liệu nằm ở một nơi, các hàm xử lý nằm ở nơi khác; hàm nhận dữ liệu qua tham số rồi trả kết quả. Cách này gọi là lập trình thủ tục (procedural programming). Nó rất hợp với bài toán nhỏ, nhưng khi chương trình lớn lên sẽ nảy sinh ba vấn đề:
- Dữ liệu liên quan bị tách rời. Thông tin một sinh viên (mã, tên, điểm) nằm trong ba mảng khác nhau; chỉ cần sắp xếp mảng điểm mà quên sắp xếp hai mảng kia là dữ liệu sai lệch ngầm, không báo lỗi.
- Ai cũng sửa được dữ liệu. Mọi hàm đều thấy mảng điểm và có thể ghi vào đó giá trị vô lý như
-5hay1000. - Khó mở rộng. Thêm một loại tài khoản ngân hàng mới nghĩa là sửa hàng chục chỗ dùng
if/elsekiểm tra loại.
Hãy xem một đoạn mã thủ tục điển hình để thấy "mùi" của vấn đề.
// Cách thủ tục: ba mảng song song cho ba sinh viên
String[] names = {"An", "Binh", "Chi"};
double[] scores = {8.5, 7.0, 9.0};
int[] ages = {19, 20, 19};
// Muốn in thông tin sinh viên thứ i, phải nhớ lấy đủ ba mảng cùng chỉ số i1.2. Lập trình hướng đối tượng: gói dữ liệu và hành vi
Trong OOP, bạn mô hình hóa bài toán bằng các đối tượng — mỗi đối tượng đại diện cho một thực thể của thế giới thực (sinh viên, tài khoản, hóa đơn) hoặc một khái niệm (phân số, số phức, ngày tháng). Một đối tượng có hai phần:
- Trạng thái (state): dữ liệu mà đối tượng đang giữ — gọi là thuộc tính (field/attribute). Ví dụ một tài khoản có số dư
1_500_000. - Hành vi (behavior): những việc đối tượng làm được — gọi là phương thức (method). Ví dụ tài khoản biết
deposit(amount)(nạp tiền) vàwithdraw(amount)(rút tiền).
Điểm cốt lõi: dữ liệu và các thao tác lên dữ liệu sống cùng nhau. Muốn rút tiền, bạn không "thò tay" sửa số dư, mà nhờ tài khoản làm việc đó bằng cách gọi phương thức; chính tài khoản kiểm tra số dư có đủ không.
Ví dụ thực tế: Khi bạn rút tiền ở cây ATM, bạn không trực tiếp sửa số dư trong cơ sở dữ liệu ngân hàng. Bạn gửi yêu cầu "rút 500 nghìn" và hệ thống tự kiểm tra, trừ tiền, ghi nhật ký. Đối tượng tài khoản trong OOP hoạt động đúng như vậy.
1.3. Bốn tính chất của OOP
OOP thường được tóm tắt bằng bốn tính chất. Buổi này tập trung vào tính chất đầu tiên, buổi sau sẽ học ba tính chất còn lại.
Đóng gói
EncapsulationGiấu dữ liệu bên trong, chỉ cho truy cập qua phương thức công khai có kiểm soát.
private double balance; public void deposit(double x)
Kế thừa
InheritanceLớp con dùng lại thuộc tính và phương thức của lớp cha, rồi bổ sung hoặc đổi hành vi.
class Dog extends Animal { void bark() { … } }Đa hình
PolymorphismCùng một lời gọi, hành vi tùy đối tượng thật khi chạy (ghi đè @Override).
Animal a = new Dog(); a.speak(); // "Woof!"
Trừu tượng
AbstractionChỉ lộ ra "làm gì", ẩn chi tiết "làm thế nào" qua abstract class và interface.
abstract class Shape { abstract double area(); }
Đóng gói bảo vệ dữ liệu, kế thừa tái sử dụng mã, đa hình cho phép mở rộng mà không sửa nơi gọi, trừu tượng giúp thiết kế theo hợp đồng.
Hình trên liệt kê bốn "trụ cột" của OOP; hãy đọc từ trái sang phải và để ý trụ cột đóng gói được đánh dấu là nền móng của các trụ còn lại.
| Tính chất | Ý nghĩa ngắn gọn | Học ở |
|---|---|---|
| Đóng gói (encapsulation) | Giấu dữ liệu bên trong, chỉ cho truy cập qua phương thức có kiểm soát | Buổi 5 (hôm nay) |
| Kế thừa (inheritance) | Lớp con dùng lại và mở rộng lớp cha | Buổi 6 |
| Đa hình (polymorphism) | Cùng một lời gọi, đối tượng khác nhau xử lý khác nhau | Buổi 6 |
| Trừu tượng (abstraction) | Chỉ nêu "làm gì", che giấu "làm thế nào" (lớp trừu tượng, interface) | Buổi 6 |
Lưu ý: Bốn tính chất không tách rời nhau. Đóng gói giúp mỗi lớp là một "hộp đen" đáng tin cậy; nhờ đó kế thừa và đa hình mới an toàn để dùng. Vì vậy hôm nay hãy học thật chắc phần đóng gói.
2. Lớp và đối tượng
2.1. Lớp là bản thiết kế, đối tượng là thực thể
Lớp (class) là bản thiết kế mô tả một loại đối tượng: nó có những thuộc tính nào, làm được những phương thức nào. Đối tượng (object) là một thực thể cụ thể được "đúc" ra từ bản thiết kế đó, có giá trị thuộc tính riêng của mình. Từ một lớp BankAccount bạn tạo được vô số đối tượng: tài khoản của An, tài khoản của Bình, tài khoản của Chi… Mỗi đối tượng có số dư riêng nhưng cùng biết nạp và rút tiền theo một cách.
Mỗi lần new tạo một đối tượng với bộ thuộc tính riêng (color, speed). Thuộc tính static chỉ có một bản của cả lớp, mọi đối tượng cùng đọc/ghi chung.
Hình mô tả một lớp (khung bản thiết kế) và ba đối tượng sinh ra từ nó; hãy chú ý các đối tượng cùng cấu trúc nhưng khác giá trị thuộc tính. Cách gọi trong chuyên ngành: đối tượng còn gọi là instance (thể hiện), việc tạo đối tượng gọi là khởi tạo (instantiation).
| Lớp (class) | Đối tượng (object) |
|---|---|
| Bản thiết kế, khai báo một lần trong mã nguồn | Thực thể cụ thể, tạo ra khi chạy chương trình |
| Không chiếm bộ nhớ dữ liệu của từng thực thể | Mỗi đối tượng chiếm một vùng nhớ trên heap |
Ví dụ: Student |
Ví dụ: sinh viên An 8.5 điểm, sinh viên Bình 7.0 điểm |
2.2. Khai báo lớp: thuộc tính và phương thức
Khung của một lớp gồm tên lớp (viết hoa chữ cái đầu theo kiểu PascalCase), các thuộc tính ở đầu, rồi các phương thức. Ví dụ dưới đây là lớp Student đơn giản nhất, chưa có đóng gói (ta cố tình để vậy để buổi sau thấy vì sao cần đóng gói).
class Student {
String name; // thuộc tính (field)
double score;
void print() { // phương thức (method)
System.out.println(name + " - " + score);
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student(); // tạo đối tượng
s.name = "An";
s.score = 8.5;
s.print();
}
}Một vài điểm cần nhớ từ ví dụ này:
Student s = new Student();làm hai việc: toán tửnewtạo đối tượng trên heap, còn biếnsgiữ tham chiếu (địa chỉ) tới đối tượng ấy.- Truy cập thành viên của đối tượng bằng toán tử chấm:
s.name,s.print(). - Phương thức
print()ở đây không có từ khóastaticvì nó thuộc về từng đối tượng: nó dùngnamevàscorecủa chính đối tượng gọi nó. - Trong một tệp có thể khai báo nhiều lớp, nhưng chỉ lớp
publicmới phải trùng tên tệp. Quy ước thực tế: mỗi lớp một tệp.java.
Mẹo: Đặt tên lớp là danh từ số ít (
Student,Invoice), tên phương thức là động từ (deposit,calculateTotal), tên thuộc tính là danh từ (balance). Tên trong code dùng tiếng Anh để thống nhất với thư viện chuẩn.
2.3. Phương thức tác động lên thuộc tính của đối tượng
Phương thức của đối tượng có thể đọc và thay đổi thuộc tính của chính đối tượng đó, đồng thời nhận tham số và trả giá trị như hàm bạn đã học ở buổi 4. Hãy mô phỏng một tài khoản ngân hàng đơn giản.
class Account {
String owner;
double balance;
void deposit(double amount) {
balance += amount;
}
boolean withdraw(double amount) {
if (amount > balance) {
return false; // không đủ tiền
}
balance -= amount;
return true;
}
}
public class Main {
public static void main(String[] args) {
Account a = new Account();
a.owner = "Lan";
a.deposit(1000);
System.out.println(a.withdraw(300)); // true
System.out.println(a.withdraw(900)); // false
System.out.println(a.balance); // 700.0
}
}Đoạn mã cho thấy phương thức withdraw tự quyết định có cho rút hay không. Tuy vậy, vì balance đang để công khai, bất kỳ đoạn mã nào cũng có thể viết a.balance = -999; và phá vỡ quy tắc. Phần 4 sẽ khắc phục chuyện này.
2.4. Biến tham chiếu, null và giá trị mặc định
Khi một biến có kiểu lớp, nó không chứa đối tượng, mà chỉ chứa tham chiếu tới đối tượng nằm trên heap. Hệ quả quan trọng: gán b = a không sao chép đối tượng, mà làm cho hai biến cùng trỏ vào một đối tượng.
class Counter {
int value;
}
public class Main {
public static void main(String[] args) {
Counter a = new Counter();
Counter b = a; // b và a cùng trỏ một đối tượng
b.value = 5;
System.out.println(a.value); // 5, vì cùng một đối tượng
Counter c = new Counter(); // đối tượng khác
System.out.println(c.value); // 0, giá trị mặc định
System.out.println(a == c); // false
}
}Những điều cần nhớ về tham chiếu:
- Thuộc tính chưa gán giá trị được Java tự khởi tạo mặc định: số là
0(hoặc0.0),booleanlàfalse, ký tự là'\u0000', còn kiểu tham chiếu (kể cảString) lànull. Biến cục bộ thì không có giá trị mặc định và phải gán trước khi dùng. nullnghĩa là "chưa trỏ tới đối tượng nào". Gọi phương thức trênnullnémNullPointerException.- Truyền đối tượng cho hàm là truyền bản sao của tham chiếu (vẫn là truyền giá trị): hàm có thể sửa nội dung đối tượng nhưng không đổi được biến tham chiếu của người gọi.
class Box {
String label;
}
public class Main {
public static void main(String[] args) {
Box x = new Box();
System.out.println(x.label); // null
try {
System.out.println(x.label.length());
} catch (NullPointerException e) {
System.out.println("Lỗi: label đang là null");
}
x.label = "sách";
System.out.println(x.label.length()); // 4
}
}Cảnh báo:
NullPointerExceptionlà ngoại lệ gặp nhiều nhất của người mới học Java. Mỗi khi thấy nó, hãy hỏi: "biến nào đang lànull, và vì sao nó chưa được gán đối tượng?"
Về bộ nhớ: các biến tham chiếu nằm trên stack (khi là biến cục bộ), còn bản thân đối tượng nằm trên heap. Bộ thu gom rác (garbage collector) tự giải phóng đối tượng khi không còn tham chiếu nào trỏ tới, nên Java không có thao tác delete như C++.
3. Hàm tạo (constructor)
3.1. Hàm tạo là gì?
Ở ví dụ trước, bạn tạo new Student() rồi gán từng thuộc tính bằng tay. Cách đó dễ quên: nếu bạn bỏ sót dòng s.score = ..., đối tượng sẽ mang điểm 0.0 mà không ai báo lỗi. Hàm tạo (constructor) là một khối mã đặc biệt chạy ngay lúc đối tượng được tạo để đưa nó vào trạng thái hợp lệ. Hàm tạo có hai đặc điểm nhận dạng:
- Tên trùng đúng tên lớp.
- Không khai báo kiểu trả về (kể cả
void).
class Student {
String name;
double score;
Student(String name, double score) {
this.name = name; // this.name là thuộc tính, name là tham số
this.score = score;
}
void print() {
System.out.println(name + " - " + score);
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student("An", 8.5);
s.print();
}
}Từ khóa this là tham chiếu tới chính đối tượng đang chạy phương thức/hàm tạo. Khi tham số trùng tên thuộc tính (cách đặt tên rất phổ biến), this.name chỉ thuộc tính còn name chỉ tham số. Nếu bạn viết name = name; thì chỉ gán tham số cho chính nó và thuộc tính vẫn giữ giá trị mặc định — một lỗi cực kỳ hay gặp.
3.2. Hàm tạo mặc định
Nếu bạn không viết hàm tạo nào, trình biên dịch tự thêm một hàm tạo mặc định không tham số (Student() {}), vì vậy new Student() ở phần 2 chạy được. Nhưng ngay khi bạn viết bất kỳ hàm tạo nào, Java không thêm hàm tạo mặc định nữa. Lúc đó muốn dùng new Student() bạn phải tự viết hàm tạo không tham số.
Lưu ý: Gặp lỗi biên dịch "constructor Student in class Student cannot be applied to given types" nghĩa là bạn gọi
newvới danh sách tham số mà lớp không có hàm tạo tương ứng. Hãy đối chiếu số lượng và kiểu tham số.
3.3. Chồng hàm tạo (constructor overloading)
Giống chồng hàm ở buổi 4, một lớp có thể có nhiều hàm tạo với danh sách tham số khác nhau. Điều này cho phép tạo đối tượng theo nhiều cách: đầy đủ thông tin, hoặc chỉ một phần với giá trị mặc định hợp lý.
Để tránh lặp mã giữa các hàm tạo, dùng this(...) ở dòng đầu tiên để gọi hàm tạo khác của cùng lớp.
class Book {
String title;
String author;
int year;
Book(String title, String author, int year) {
this.title = title;
this.author = author;
this.year = year;
}
Book(String title, String author) {
this(title, author, 2024); // gọi hàm tạo ba tham số
}
Book(String title) {
this(title, "Khuyết danh"); // gọi hàm tạo hai tham số
}
void print() {
System.out.println(title + " | " + author + " | " + year);
}
}
public class Main {
public static void main(String[] args) {
new Book("Java căn bản", "Lam", 2023).print();
new Book("Cấu trúc dữ liệu", "Minh").print();
new Book("Sách cũ").print();
}
}Ví dụ trên dùng chuỗi hàm tạo: hàm tạo ngắn nhất gọi hàm tạo dài hơn, cuối cùng chỉ một hàm tạo "thật" gán thuộc tính. Cách làm này giúp mọi quy tắc kiểm tra hợp lệ (sẽ học ở phần 4) nằm ở đúng một chỗ.
3.4. Thứ tự khởi tạo trong một đối tượng
Khi bạn gọi new, Java thực hiện theo thứ tự: cấp vùng nhớ và đặt mọi thuộc tính về giá trị mặc định; chạy phần khởi tạo thuộc tính tại chỗ (ví dụ int count = 10;); rồi mới chạy thân hàm tạo. Vì vậy bạn có thể cho thuộc tính một giá trị ban đầu ngay tại khai báo, hàm tạo sau đó có thể ghi đè.
class Ticket {
String status = "NEW"; // khởi tạo tại chỗ
int seats;
Ticket(int seats) {
System.out.println("Trước hàm tạo, status = " + status);
this.seats = seats;
status = "BOOKED";
}
}
public class Main {
public static void main(String[] args) {
Ticket t = new Ticket(2);
System.out.println(t.status + " " + t.seats);
}
}Bảng sau tổng hợp cách khởi tạo đối tượng:
| Tình huống | Kết quả |
|---|---|
| Không viết hàm tạo | Java thêm hàm tạo mặc định, thuộc tính nhận giá trị mặc định |
| Có viết hàm tạo có tham số | Không còn hàm tạo mặc định; phải tự viết nếu cần |
| Nhiều hàm tạo | Chọn theo số lượng và kiểu tham số lúc new |
this(...) |
Gọi hàm tạo khác của cùng lớp, chỉ được ở dòng đầu |
3.5. this làm gì ngoài việc phân biệt tên?
Ngoài this.field và this(...), bạn còn có thể dùng this như một giá trị: truyền chính đối tượng cho hàm khác, hoặc trả về this để gọi nối tiếp (method chaining). Kỹ thuật này gặp nhiều trong StringBuilder (sb.append("a").append("b")).
class Vec {
double x, y;
Vec(double x, double y) {
this.x = x;
this.y = y;
}
Vec add(Vec o) {
return new Vec(x + o.x, y + o.y); // tạo đối tượng mới, không sửa đối tượng cũ
}
Vec scale(double k) {
x *= k;
y *= k;
return this; // trả về chính nó để nối tiếp
}
void print() {
System.out.println("(" + x + ", " + y + ")");
}
}
public class Main {
public static void main(String[] args) {
Vec a = new Vec(1, 2);
Vec b = new Vec(3, 4);
a.add(b).print(); // (4.0, 6.0)
a.scale(2).scale(3).print(); // (6.0, 12.0)
}
}Chú ý sự khác biệt thiết kế: add trả về đối tượng mới và giữ nguyên a, b (kiểu "bất biến"), còn scale sửa trực tiếp đối tượng và trả về chính nó. Đề bài thường nói rõ muốn kiểu nào; hãy đọc kỹ để chọn đúng.
Mẹo: Khi lớp biểu diễn một giá trị toán học (vector, phân số, số phức, ngày tháng), nên cho các phép toán trả về đối tượng mới để tránh sửa nhầm dữ liệu ở nơi khác.
Vừa học xong lớp, đối tượng và hàm tạo, hãy làm hai bài này: bài hóa đơn tiền điện giúp bạn gói dữ liệu và phép tính vào một lớp, bài vector 2D luyện thiết kế phương thức nhận và trả về đối tượng cùng kiểu.
4. Đóng gói (encapsulation)
4.1. Vấn đề: dữ liệu công khai bị phá dễ dàng
Quay lại lớp Account ở phần 2. Vì thuộc tính balance là công khai, đoạn mã dưới đây vô tình (hoặc cố ý) làm hỏng quy tắc "số dư không âm".
class Account {
double balance;
}
public class Main {
public static void main(String[] args) {
Account a = new Account();
a.balance = -5000; // không ai ngăn được
System.out.println("Số dư: " + a.balance);
}
}Đóng gói là nguyên tắc giấu trạng thái bên trong đối tượng và chỉ cho truy cập qua các phương thức công khai mà lớp kiểm soát. Nhờ đó lớp luôn giữ được bất biến (invariant) của mình — ví dụ "số dư không âm", "điểm trong khoảng 0 đến 10", "tuổi không âm".
4.2. Phạm vi truy cập (access modifier)
Java có bốn mức truy cập, áp dụng cho thuộc tính, phương thức, hàm tạo và (một phần) lớp:
| Từ khóa | Truy cập được từ đâu | Dùng khi |
|---|---|---|
private |
Chỉ bên trong chính lớp đó | Mặc định cho thuộc tính |
| (không ghi — package-private) | Mọi lớp cùng package | Hỗ trợ nội bộ trong một package |
protected |
Cùng package và các lớp con (buổi 6) | Thành viên dành cho lớp kế thừa |
public |
Mọi nơi | Giao diện công khai của lớp |
Quy tắc vàng: thuộc tính private, phương thức mà bên ngoài cần dùng thì public. Bên ngoài chỉ nhìn thấy "giao diện" (các phương thức public), còn cách lưu trữ bên trong có thể thay đổi mà không làm hỏng người dùng lớp.
Lưu ý: Nếu bạn thử truy cập thuộc tính
privatetừ lớp khác, trình biên dịch báo lỗi "has private access". Đây là lỗi biên dịch, tức là Java chặn ngay từ đầu chứ không đợi đến lúc chạy.
4.3. Getter và setter
Để bên ngoài đọc hoặc đổi dữ liệu có kiểm soát, ta viết cặp phương thức: getter (đọc) tên getXxx, setter (ghi) tên setXxx. Với thuộc tính boolean getter thường đặt tên isXxx. Setter là nơi chốt chặn kiểm tra hợp lệ.
class Account {
private String owner;
private double balance;
Account(String owner, double balance) {
this.owner = owner;
setBalance(balance); // dùng setter để tái sử dụng kiểm tra
}
public String getOwner() {
return owner;
}
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
if (balance < 0) {
System.out.println("Số dư không hợp lệ, bỏ qua");
return;
}
this.balance = balance;
}
public void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
public class Main {
public static void main(String[] args) {
Account a = new Account("Lan", 100);
a.setBalance(-5000); // bị từ chối
a.deposit(50);
System.out.println(a.getOwner() + ": " + a.getBalance()); // Lan: 150.0
}
}Điểm đáng chú ý: hàm tạo cũng gọi setBalance để dữ liệu ngay từ lúc tạo cũng phải qua kiểm tra; không có "cửa sau" nào để đưa dữ liệu sai vào đối tượng.
4.4. Khi nào không cần setter?
Đừng máy móc tạo getter/setter cho mọi thuộc tính. Một số thuộc tính nên chỉ đọc hoặc không lộ ra:
- Chỉ có getter (không setter): mã sinh viên đã đặt khi tạo thì không đổi — thuộc tính khai báo
finalvà gán đúng một lần trong hàm tạo. - Thay setter bằng phương thức nghiệp vụ: thay vì
setBalance, chỉ cung cấpdepositvàwithdrawđể mọi thay đổi đều có ý nghĩa nghiệp vụ. - Thuộc tính suy ra:
getArea()tính từ chiều dài, chiều rộng; không cần lưu thuộc tínharea.
class Student {
private final String id; // không đổi sau khi tạo
private double score;
Student(String id, double score) {
this.id = id;
setScore(score);
}
public String getId() {
return id;
}
public double getScore() {
return score;
}
public void setScore(double score) {
if (score < 0 || score > 10) {
throw new IllegalArgumentException("Điểm phải trong [0, 10]: " + score);
}
this.score = score;
}
public String getGrade() { // thuộc tính suy ra
if (score >= 8.5) return "A";
if (score >= 7.0) return "B";
if (score >= 5.0) return "C";
return "F";
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student("SV01", 7.5);
System.out.println(s.getId() + " " + s.getGrade());
try {
s.setScore(11);
} catch (IllegalArgumentException e) {
System.out.println("Bắt được: " + e.getMessage());
}
System.out.println(s.getScore());
}
}Có hai chiến lược khi dữ liệu sai: im lặng bỏ qua (như setBalance ở trên) hoặc ném ngoại lệ (như setScore ngay đây). Ném ngoại lệ báo lỗi rõ ràng hơn và buộc người gọi xử lý; bỏ qua thì đơn giản hơn nhưng dễ che giấu lỗi. Hãy làm đúng theo đề bài yêu cầu — nhiều bài tập chấm tự động quy định rõ phản ứng khi dữ liệu sai.
Cảnh báo: Getter trả về tham chiếu tới mảng hoặc danh sách bên trong làm lộ trạng thái dù bạn đã để thuộc tính
private. Người gọi sửa mảng đó là sửa luôn dữ liệu của đối tượng. Khi lớp giữ một mảng, hãy trả về bản sao (Arrays.copyOf).
class Team {
private final int[] scores = {5, 6, 7};
public int[] getScoresLeaky() {
return scores; // lộ mảng gốc
}
public int[] getScoresSafe() {
return java.util.Arrays.copyOf(scores, scores.length); // trả bản sao
}
public int total() {
int sum = 0;
for (int v : scores) sum += v;
return sum;
}
}
public class Main {
public static void main(String[] args) {
Team t = new Team();
t.getScoresLeaky()[0] = 100; // sửa được dữ liệu bên trong!
System.out.println(t.total()); // 113
t.getScoresSafe()[1] = 100; // chỉ sửa bản sao
System.out.println(t.total()); // vẫn 113
}
}4.5. Tại sao đóng gói đáng công sức?
Đóng gói không chỉ để "chặn dữ liệu sai". Lợi ích dài hạn:
- Bảo vệ bất biến: mọi quy tắc nằm ở một nơi (setter/hàm tạo), không phải kiểm tra rải rác.
- Tự do thay đổi bên trong: bạn đổi cách lưu
balancetừdoublesanglong(đơn vị đồng) mà mã bên ngoài vẫn dùnggetBalance()như cũ. - Dễ gỡ lỗi: muốn biết ai đổi số dư, đặt điểm dừng (breakpoint) trong
setBalancehoặcdeposit. - Dễ bảo trì khi làm việc nhóm: mỗi người chỉ cần biết giao diện công khai của lớp người khác viết.
Vừa học xong đóng gói, hãy làm các bài về thiết bị điện tử, hồ sơ học sinh và điểm môn học: mỗi bài yêu cầu bạn đặt thuộc tính private, viết getter/setter và xử lý đúng khi dữ liệu không hợp lệ.
5. Thành viên tĩnh (static)
5.1. Thuộc về lớp, không thuộc về đối tượng
Cho đến giờ mỗi đối tượng có một bộ thuộc tính riêng. Nhưng đôi khi có dữ liệu chung cho cả lớp: tổng số sinh viên đã tạo, lãi suất chung của mọi tài khoản tiết kiệm, hằng số PI. Những thứ đó khai báo với từ khóa static: chỉ có một bản duy nhất, thuộc về lớp, tồn tại dù chưa tạo đối tượng nào.
| Thành viên thường (instance) | Thành viên static |
|
|---|---|---|
| Thuộc về | Từng đối tượng | Lớp (dùng chung) |
| Số bản sao | Mỗi đối tượng một bản | Đúng một bản |
| Truy cập | doiTuong.ten |
TenLop.ten (nên dùng) |
Dùng this |
Có | Không (không có đối tượng hiện hành) |
| Truy cập thành viên thường | Có | Không trực tiếp |
Bạn đã dùng static từ buổi 1: public static void main là phương thức tĩnh nên JVM gọi được mà không cần tạo đối tượng Main. Các lời gọi như Math.sqrt(2) hay Integer.parseInt("12") cũng là phương thức tĩnh.
5.2. Biến tĩnh làm bộ đếm
Ứng dụng kinh điển: đếm số đối tượng đã tạo, đồng thời cấp mã tự tăng.
class Student {
private static int count = 0; // dùng chung cho mọi Student
private final int id;
private final String name;
Student(String name) {
count++;
this.id = count; // mã tự tăng
this.name = name;
}
public static int getCount() {
return count;
}
public String info() {
return id + ": " + name;
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Trước khi tạo: " + Student.getCount());
Student a = new Student("An");
Student b = new Student("Binh");
Student c = new Student("Chi");
System.out.println(a.info() + ", " + b.info() + ", " + c.info());
System.out.println("Tổng: " + Student.getCount());
}
}Biến count chỉ có một bản: mỗi lần hàm tạo chạy, nó tăng chung cho tất cả. Trong khi đó id và name là của riêng từng đối tượng. Chú ý phương thức getCount() là static nên gọi qua Student.getCount() — ngay cả khi chưa có sinh viên nào.
5.3. Hằng số static final
Giá trị không đổi và dùng chung được khai báo static final, tên viết HOA có dấu gạch dưới. Đây là cách thay thế "số ma thuật" (magic number) bằng tên có nghĩa.
class Tax {
public static final double VAT_RATE = 0.1;
public static final int MAX_ITEMS = 100;
public static double withVat(double price) {
return price * (1 + VAT_RATE);
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Tax.withVat(200)); // 220.0
System.out.println(Tax.MAX_ITEMS);
}
}5.4. Phương thức tĩnh và những giới hạn
Phương thức tĩnh phù hợp với hàm tiện ích không phụ thuộc trạng thái của đối tượng nào (tính ƯCLN, kiểm tra số nguyên tố, chuyển đổi đơn vị). Quy tắc quan trọng: phương thức tĩnh không dùng được this và không truy cập trực tiếp thuộc tính/phương thức thường, vì khi nó chạy có thể chưa có đối tượng nào.
class Calc {
private int base = 10;
static int twice(int x) {
// return base * x; // LỖI biên dịch: base là thuộc tính thường
return 2 * x;
}
int scaled(int x) {
return base * twice(x); // phương thức thường gọi được phương thức tĩnh
}
}
public class Main {
public static void main(String[] args) {
System.out.println(Calc.twice(7));
Calc c = new Calc();
System.out.println(c.scaled(7));
}
}Chiều ngược lại thì được phép: phương thức thường gọi thoải mái thành viên tĩnh. Nếu muốn một phương thức tĩnh làm việc với một đối tượng cụ thể, hãy truyền đối tượng đó vào làm tham số.
Lưu ý: Lỗi "non-static variable cannot be referenced from a static context" xuất hiện khi trong
main(tĩnh) bạn gọi trực tiếp một phương thức thường của lớpMain. Cách sửa: tạo đối tượng rồi gọi qua nó, hoặc đổi phương thức thànhstaticnếu thật sự không cần trạng thái.
5.5. Khối khởi tạo tĩnh và thứ tự nạp lớp
Đôi khi một biến tĩnh cần nhiều dòng để khởi tạo. Khi đó dùng khối static { ... }; khối này chạy đúng một lần khi lớp được nạp lần đầu, trước mọi đối tượng.
class Config {
static final int[] PRIMES = new int[5];
static {
int found = 0, n = 2;
while (found < PRIMES.length) {
boolean isPrime = true;
for (int d = 2; d * d <= n; d++) {
if (n % d == 0) { isPrime = false; break; }
}
if (isPrime) PRIMES[found++] = n;
n++;
}
System.out.println("Khối static chạy một lần");
}
}
public class Main {
public static void main(String[] args) {
System.out.println("Bắt đầu");
System.out.println(java.util.Arrays.toString(Config.PRIMES));
System.out.println(Config.PRIMES[4]);
}
}Quan sát thứ tự in: dòng "Bắt đầu" xuất hiện trước, vì lớp Config chỉ được nạp khi lần đầu bị dùng đến.
Cảnh báo: Đừng lạm dụng biến tĩnh thay đổi được (mutable) để chia sẻ dữ liệu giữa các lớp — đó là "biến toàn cục trá hình" và là nguồn gốc của nhiều lỗi khó tìm, nhất là khi chương trình đa luồng (buổi 9). Biến tĩnh hợp lý nhất là hằng số và bộ đếm đơn giản.
Vừa học xong thành viên tĩnh, hãy làm bài đếm số sinh viên bằng biến static và bài quản lý tài khoản dùng các biến dùng chung để thấy rõ khác biệt giữa dữ liệu của đối tượng và dữ liệu của cả lớp.
6. Lớp Object: toString, equals, hashCode
6.1. Mọi lớp đều kế thừa từ Object
Khi bạn viết class Student { ... }, thực ra Java hiểu là class Student extends Object { ... }. java.lang.Object là gốc của cây lớp: mọi lớp, kể cả lớp bạn tự viết, đều "thừa hưởng" các phương thức của nó mà không cần khai báo. Ba phương thức quan trọng nhất cho buổi này:
| Phương thức | Mặc định của Object |
Thường cần ghi đè để |
|---|---|---|
String toString() |
In TenLop@maBamHex (khó đọc) |
In mô tả dễ đọc của đối tượng |
boolean equals(Object o) |
So sánh tham chiếu, giống == |
So sánh theo nội dung |
int hashCode() |
Số suy từ địa chỉ đối tượng | Nhất quán với equals để dùng trong HashMap/HashSet |
Các phương thức này bạn thấy ở lớp String rồi: "a".equals("a") so nội dung, và in một String ra đúng nội dung. String đã ghi đè chúng. Lớp của bạn muốn có hành vi tương tự thì phải tự ghi đè (override).
6.2. toString: mô tả đối tượng thành chuỗi
Nếu không ghi đè, in một đối tượng ra màn hình chỉ cho chuỗi vô nghĩa dạng Student@1b6d3586. Ghi đè toString giúp bạn in, nối chuỗi và gỡ lỗi dễ dàng. Nhớ thêm chú thích @Override để trình biên dịch kiểm tra bạn ghi đè đúng chữ ký.
class Point {
private final int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "(" + x + ", " + y + ")";
}
}
public class Main {
public static void main(String[] args) {
Point p = new Point(3, 4);
System.out.println(p); // gọi toString() ngầm định
System.out.println("Điểm: " + p); // nối chuỗi cũng gọi toString()
System.out.println(String.valueOf(p).length());
}
}System.out.println(p) và phép nối chuỗi "..." + p đều tự gọi p.toString(). Nếu p là null, chúng in chữ null chứ không ném lỗi, nhưng gọi trực tiếp p.toString() thì ném NullPointerException.
Mẹo:
String.format("%.2f", value)rất tiện trongtoStringkhi cần định dạng số thực với số chữ số thập phân cố định, ví dụString.format("%s: %.1f", name, score).
6.3. equals: so sánh theo nội dung
Toán tử == với đối tượng so sánh tham chiếu (hai biến có trỏ cùng một đối tượng không). Hai đối tượng khác nhau, dù cùng nội dung, cho == là false. Mặc định equals của Object cũng làm như ==. Muốn so sánh nội dung, bạn ghi đè equals.
class Point {
private final int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true; // cùng tham chiếu
if (o == null || getClass() != o.getClass()) return false;
Point other = (Point) o; // ép kiểu sau khi đã kiểm tra
return x == other.x && y == other.y;
}
@Override
public int hashCode() {
return java.util.Objects.hash(x, y);
}
}
public class Main {
public static void main(String[] args) {
Point a = new Point(1, 2);
Point b = new Point(1, 2);
System.out.println(a == b); // false: hai đối tượng khác nhau
System.out.println(a.equals(b)); // true: cùng nội dung
System.out.println(a.equals(null)); // false
System.out.println(a.equals("hello")); // false: khác lớp
}
}Khuôn mẫu năm bước nên thuộc lòng khi viết equals:
- Nếu
this == othì trảtrue(tối ưu nhanh). - Nếu
olànullthì trảfalse. - Nếu hai đối tượng khác lớp (
getClass() != o.getClass()) thì trảfalse. - Ép kiểu
ovề lớp của bạn. - So sánh từng thuộc tính: kiểu nguyên thủy dùng
==, kiểu tham chiếu dùngequals(hoặcObjects.equalsan toàn vớinull). Vớidoublenên dùngDouble.compare(a, b) == 0thay vì==.
Tham số của equals phải là Object. Nếu bạn viết equals(Point o) thì đó là chồng hàm chứ không phải ghi đè, và các tập hợp của Java sẽ vẫn gọi equals(Object) mặc định — một cạm bẫy kinh điển, vì vậy luôn ghi @Override.
6.4. hashCode: quy ước đi kèm equals
Các cấu trúc dữ liệu băm như HashMap, HashSet (buổi 7) dùng hashCode() để quyết định đặt phần tử vào "ngăn" nào, rồi mới dùng equals để so sánh trong ngăn. Hợp đồng bắt buộc:
- Nếu
a.equals(b)làtruethìa.hashCode() == b.hashCode()phải bằng nhau. - Chiều ngược lại không bắt buộc: hai đối tượng khác nhau có thể trùng mã băm (va chạm), chỉ làm chậm chứ không sai.
- Trong một lần chạy, mã băm của cùng một đối tượng không đổi nếu các thuộc tính tham gia
equalskhông đổi.
Cách đơn giản và đủ tốt: Objects.hash(thuộcTính1, thuộcTính2, …) với đúng những thuộc tính đã dùng trong equals.
import java.util.*;
class Point {
private final int x, y;
Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point p = (Point) o;
return x == p.x && y == p.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
public class Main {
public static void main(String[] args) {
Set<Point> set = new HashSet<>();
set.add(new Point(1, 2));
set.add(new Point(1, 2));
set.add(new Point(2, 1));
System.out.println("Số điểm trong set: " + set.size()); // 2
System.out.println(set.contains(new Point(2, 1))); // true
System.out.println(new Point(1, 2).hashCode() == new Point(1, 2).hashCode()); // true
}
}Nhờ equals và hashCode đi cùng nhau, HashSet nhận ra hai điểm (1, 2) là trùng nhau nên kích thước chỉ là 2. Nếu bạn bỏ phương thức hashCode đi, HashSet thường coi chúng là hai phần tử khác nhau (vì mã băm mặc định suy từ địa chỉ), và kích thước có thể thành 3.
Cảnh báo: Ghi đè
equalsmà không ghi đèhashCode(hoặc ngược lại) là lỗi nghiêm trọng khi đối tượng được cho vàoHashSet/HashMap. IDE như IntelliJ có sẵn chức năng sinh tự động cả hai (Alt+Insert, Generate).
6.5. Comparable và compareTo: sắp xếp theo thứ tự tự nhiên
equals chỉ trả lời "bằng hay không". Để sắp xếp các đối tượng, Java cần biết "bé hơn hay lớn hơn". Lớp implement interface Comparable<T> và cài đặt int compareTo(T other): trả số âm nếu đối tượng hiện tại đứng trước, 0 nếu bằng, dương nếu đứng sau. Khi đó Arrays.sort và Collections.sort dùng được ngay. (Buổi 7 sẽ học thêm Comparator.)
import java.util.*;
class Student implements Comparable<Student> {
private final String name;
private final double score;
Student(String name, double score) {
this.name = name;
this.score = score;
}
@Override
public int compareTo(Student o) {
return Double.compare(o.score, this.score); // điểm giảm dần
}
@Override
public String toString() {
return name + "(" + score + ")";
}
}
public class Main {
public static void main(String[] args) {
Student[] list = {
new Student("An", 7.5), new Student("Binh", 9.0), new Student("Chi", 8.0)
};
Arrays.sort(list);
System.out.println(Arrays.toString(list)); // [Binh(9.0), Chi(8.0), An(7.5)]
}
}Quy ước đẹp: compareTo nên nhất quán với equals (hai đối tượng compareTo bằng 0 thì nên equals đúng). Nếu so sánh số nguyên, tránh viết this.x - o.x vì có thể tràn số; hãy dùng Integer.compare(this.x, o.x).
Vừa học xong toString, equals, hashCode và compareTo, hãy làm ba bài về phân số, số phức và ngày tháng: các lớp giá trị này cần so sánh theo nội dung, in đúng định dạng và (với phân số, ngày) sắp xếp được.
7. Nhiều lớp phối hợp và cái nhìn đầu tiên về kế thừa
7.1. Quan hệ "có một" (composition)
Chương trình thật hiếm khi chỉ có một lớp. Cách đơn giản nhất để các lớp làm việc với nhau là chứa nhau: một thuộc tính của lớp này có kiểu là lớp kia. Một Library có nhiều Book; một Order có nhiều OrderLine; một Course có một Teacher. Quan hệ này gọi là "có một" (has-a) hay hợp thành (composition).
Khi thiết kế, hãy gạch chân các danh từ trong đề bài để chọn lớp, và các động từ để chọn phương thức. Ví dụ đề "thư viện cho mượn sách" cho ra các lớp Book, Library và phương thức borrow, giveBack.
class Book {
private final String title;
private boolean borrowed;
Book(String title) {
this.title = title;
}
public String getTitle() { return title; }
public boolean isBorrowed() { return borrowed; }
public boolean borrow() {
if (borrowed) return false;
borrowed = true;
return true;
}
public void giveBack() { borrowed = false; }
}
class Library {
private final Book[] shelf;
private int size;
Library(int capacity) {
shelf = new Book[capacity];
}
public void add(Book b) {
if (size < shelf.length) shelf[size++] = b;
}
public boolean borrow(String title) {
for (int i = 0; i < size; i++) {
if (shelf[i].getTitle().equals(title)) {
return shelf[i].borrow();
}
}
return false; // không có sách
}
public int countAvailable() {
int n = 0;
for (int i = 0; i < size; i++) {
if (!shelf[i].isBorrowed()) n++;
}
return n;
}
}
public class Main {
public static void main(String[] args) {
Library lib = new Library(10);
lib.add(new Book("Java"));
lib.add(new Book("SQL"));
System.out.println(lib.borrow("Java")); // true
System.out.println(lib.borrow("Java")); // false: đã mượn
System.out.println(lib.borrow("Python")); // false: không có
System.out.println(lib.countAvailable()); // 1
}
}Ở ví dụ này Library giữ một mảng các Book; mỗi lớp lo đúng phần việc của mình (Book biết trạng thái của sách, Library biết tìm sách). Đây chính là tinh thần một lớp một trách nhiệm. Buổi 7 bạn sẽ thay mảng bằng ArrayList linh hoạt hơn.
7.2. Đối tượng gọi đối tượng
Khi các lớp phối hợp, bạn thường có chuỗi lời gọi như order.getCustomer().getName(). Mỗi bước trả về một đối tượng để gọi tiếp. Hãy chú ý khả năng một khâu trả về null giữa chuỗi và gây NullPointerException.
class Address {
private final String city;
Address(String city) { this.city = city; }
public String getCity() { return city; }
}
class Customer {
private final String name;
private final Address address; // có một Address
Customer(String name, Address address) {
this.name = name;
this.address = address;
}
public String getName() { return name; }
public Address getAddress() { return address; }
}
public class Main {
public static void main(String[] args) {
Customer a = new Customer("An", new Address("Hà Nội"));
Customer b = new Customer("Binh", null);
System.out.println(a.getName() + " ở " + a.getAddress().getCity());
if (b.getAddress() == null) {
System.out.println(b.getName() + " chưa có địa chỉ");
}
}
}7.3. Nhìn trước: lớp con và lớp trừu tượng
Nhiều bài trong phần thực hành cuối buổi nói đến "lớp trừu tượng" và "kế thừa". Đây là nội dung chính của buổi 6, nhưng bạn nên làm quen trước với hình dạng của chúng để đọc hiểu đề.
- Kế thừa:
class Dog extends Animalnghĩa làDogthừa hưởng mọi thuộc tính, phương thức củaAnimalvà có thể bổ sung hoặc ghi đè (override) hành vi. - Lớp trừu tượng (
abstract class): lớp "chưa hoàn chỉnh", khôngnewtrực tiếp được; chứa các phương thứcabstractchỉ có khai báo, lớp con bắt buộc phải cài đặt. - Đa hình: một biến kiểu lớp cha có thể trỏ tới đối tượng lớp con; gọi phương thức sẽ chạy phiên bản của lớp con thật sự.
- Hàm tạo của lớp con gọi hàm tạo lớp cha bằng
super(...).
abstract class Animal {
private final String name;
Animal(String name) {
this.name = name;
}
public String getName() { return name; }
public abstract String sound(); // lớp con phải cài đặt
}
class Dog extends Animal {
Dog(String name) { super(name); }
@Override
public String sound() { return "Gâu gâu"; }
}
class Cat extends Animal {
Cat(String name) { super(name); }
@Override
public String sound() { return "Meo meo"; }
}
public class Main {
public static void main(String[] args) {
Animal[] zoo = { new Dog("Mực"), new Cat("Mướp") };
for (Animal a : zoo) {
System.out.println(a.getName() + ": " + a.sound());
}
}
}Hãy chú ý vòng lặp: biến a có kiểu Animal nhưng mỗi lần gọi a.sound() lại chạy phiên bản của Dog hoặc Cat. Đó là đa hình; bạn sẽ học kỹ nó ở buổi sau. Hôm nay chỉ cần đọc hiểu mẫu này và biết rằng các bài "nhìn trước" ở cuối buổi đều xây trên mẫu: lớp trừu tượng khai báo khung chung, các lớp con cài đặt công thức riêng (ví dụ phí bảo hiểm của ô tô khác xe máy, lương của quản lý khác thực tập sinh).
Mở rộng (Java 14+): Từ Java 14 có
recordgiúp khai báo nhanh lớp dữ liệu bất biến (tự sinh hàm tạo, getter,equals,hashCode,toString). Máy chấm của khóa này dùng Java 13 nên chúng ta vẫn viết đầy đủ bằng tay để hiểu rõ từng phương thức.
7.4. Quy trình thiết kế một lớp từ đề bài
Khi gặp đề bài OOP, hãy đi qua các bước sau; làm đúng thứ tự giúp bạn ít lỗi và ít phải sửa:
- Đọc đề, liệt kê lớp và quan hệ: danh từ nào có dữ liệu riêng và hành vi riêng thì thành lớp.
- Chốt thuộc tính và kiểu của chúng (
int,double,String…); đánh dấu thuộc tính nào chỉ đọc (final). - Viết hàm tạo đúng chữ ký đề yêu cầu; hàm tạo gọi setter nếu cần kiểm tra hợp lệ.
- Viết getter/setter cần thiết và các phương thức nghiệp vụ.
- Ghi đè
toString/equals/hashCodenếu đề yêu cầu in hoặc so sánh. - Chạy với các ca biên: số âm, giá trị 0, chuỗi rỗng,
null.
Mẹo: Máy chấm tự động so sánh đầu ra từng ký tự. Hãy đọc kỹ định dạng in (số chữ số thập phân, dấu phân cách, xuống dòng) và dùng đúng
String.format("%.2f", x)khi cần.
Vừa học xong cách các lớp phối hợp và mẫu lớp trừu tượng, hãy làm bốn bài sau để luyện đặt thuộc tính, viết hàm tạo và để mỗi lớp con tự tính công thức riêng của nó.
Cuối cùng là các bài thử thách tổng hợp: mỗi bài trộn đóng gói, hàm tạo, lớp trừu tượng và tính toán; hãy chia nhỏ đề thành từng lớp, làm xong lớp nào chạy thử lớp đó rồi mới ghép lại.
Sai lầm thường gặp
| Sai lầm | Hậu quả | Cách đúng |
|---|---|---|
Viết name = name; trong hàm tạo |
Thuộc tính không được gán, vẫn là null/0 |
Viết this.name = name; |
Để thuộc tính public |
Bất kỳ đâu cũng gán được giá trị sai, mất bất biến | Thuộc tính private, truy cập qua getter/setter |
Dùng == để so sánh nội dung đối tượng |
Hai đối tượng cùng nội dung vẫn cho false |
Ghi đè và gọi equals |
Ghi đè equals mà quên hashCode |
Đối tượng "bằng nhau" bị HashSet/HashMap coi là khác nhau |
Ghi đè cả hai, dùng Objects.hash |
Viết equals(Point p) thay vì equals(Object o) |
Chỉ là chồng hàm, tập hợp không dùng đến | Tham số kiểu Object và luôn ghi @Override |
Gọi thành viên thường từ phương thức static |
Lỗi biên dịch "non-static … static context" | Tạo đối tượng hoặc truyền nó vào làm tham số |
Dùng static cho dữ liệu riêng của đối tượng |
Mọi đối tượng chia nhau một giá trị, kết quả sai | Chỉ dùng static cho dữ liệu chung của cả lớp |
| Getter trả về mảng/danh sách nội bộ | Bên ngoài sửa được dữ liệu dù đã private |
Trả về bản sao (Arrays.copyOf) |
Tưởng b = a sao chép đối tượng |
Hai biến cùng trỏ một đối tượng, sửa một nơi đổi cả hai | Tạo đối tượng mới (new) hoặc viết hàm sao chép |
Gọi phương thức trên biến null |
NullPointerException |
Khởi tạo đối tượng trước khi dùng, kiểm tra null khi cần |
compareTo dùng phép trừ số nguyên |
Có thể tràn số, thứ tự sai | Dùng Integer.compare / Double.compare |
Quên @Override |
Gõ sai chữ ký mà không được báo lỗi | Luôn ghi @Override khi ghi đè |
Tóm tắt
- OOP gói dữ liệu và hành vi vào đối tượng; lớp là bản thiết kế, đối tượng là thực thể trên heap, biến chỉ giữ tham chiếu.
- Hàm tạo trùng tên lớp, không kiểu trả về; có thể chồng nhiều hàm tạo và gọi nhau bằng
this(...).this.fieldphân biệt thuộc tính với tham số. - Đóng gói: thuộc tính
private, phương thức công khai kiểm soát truy cập; getter/setter kiểm tra hợp lệ; thuộc tính không đổi dùngfinal. static: thuộc về lớp, chỉ một bản; dùng cho bộ đếm, hằng sốstatic final, hàm tiện ích; phương thức tĩnh không dùngthis.Objectlà gốc của mọi lớp: ghi đètoStringđể in đẹp,equalsđể so sánh nội dung,hashCodenhất quán vớiequals;Comparable.compareTođể sắp xếp.- Nhiều lớp phối hợp bằng quan hệ "có một"; kế thừa, lớp trừu tượng và đa hình là chủ đề của buổi 6.