Chúng ta cùng sang bài cuối cùng của PART 1: KHAI BÁO CLASS VÀ OBJECT CƠ BẢN – Bài 3: Lớp Điểm (Diem) & Lớp Hình Chữ Nhật (HCN) (thuộc Lab 03).
📐 BÀI 3: LỚP ĐIỂM (DIEM) VÀ HÌNH CHỮ NHẬT (HCN) - TƯ DUY NĂM TRONG (COMPOSITION)
1. Trực quan hóa tư duy OOP: Tái sử dụng đối tượng làm thuộc tính
Ở bài Phân số và ATM, thuộc tính của lớp chỉ là các kiểu dữ liệu nguyên thủy (int, double, String).
Ở bài này, ta sẽ nâng cấp tư duy: Một Object có thể chứa một Object khác làm thuộc tính bên trong nó.
Một điểm A(x,y) là một đối tượng Diem biểu diễn tọa độ trong không gian 2D.
Một chiếc bánh chưng hay mảnh đất hình chữ nhật (HCN) không tự nhiên xuất hiện các thuộc tính chiều dài/chiều rộng đơn thuần, mà được định hình bằng 2 điểm: điểm góc trên bên trái (d1) và điểm góc dưới bên phải (d2).
2. Đọc hiểu Sơ đồ UML (Lab 03)
Lớp Diem:
-
x, y: double: Tọa độ x, y (giấu kín).
-
Diem(x, y): Constructor khởi tạo tọa độ.
-
khoangCach(Diem p): double: Tính khoảng cách Euclid từ điểm hiện tại (this) tới điểm p: căn bậc hai của (xB−xA)² + (yB−yA)².
-
toString(): String: Chuyển điểm thành dạng chuỗi (x, y).
Lớp HCN:
-
d1, d2: Diem: Hai đối tượng thuộc lớp Diem đại diện cho góc trên-trái và góc dưới-phải.
-
HCN(Diem d1, Diem d2): Constructor nhận vào 2 điểm.
-
chuVi(): double: Tính chu vi: 2 × (chiều dài + chiều rộng).
-
dienTich(): double: Tính diện tích: chiều dài × chiều rộng.
-
toString(): String: Chuyển thông tin hình chữ nhật thành chuỗi [(x1, y1), (x2, y2)].
3. Phân tích & Cài đặt Mã nguồn Java Chi Tiết
Bước 3.1: Tạo lớp Diem.java
public class Diem {
// Thuộc tính private
private double x;
private double y;
// Constructor khởi tạo điểm khi biết tọa độ
public Diem(double x, double y) {
this.x = x;
this.y = y;
}
// Các hàm Getter & Setter
public double layX() {
return this.x;
}
public double layY() {
return this.y;
}
// Tính khoảng cách từ điểm hiện tại (this) tới điểm p
public double khoangCach(Diem p) {
double dx = this.x - p.x;
double dy = this.y - p.y;
return Math.sqrt(dx * dx + dy * dy); // Công thức Căn bậc 2 ((x2-x1)^2 + (y2-y1)^2)
}
// Biểu diễn điểm dưới dạng "(x, y)"
@Override
public String toString() {
return "(" + this.x + ", " + this.y + ")";
}
}
Bước 3.2: Tạo lớp HCN.java
public class HCN {
// Thuộc tính d1, d2 là 2 đối tượng kiểu Diem
private Diem d1; // Góc trên bên trái
private Diem d2; // Góc dưới bên phải
// Constructor nhận 2 đối tượng Diem
public HCN(Diem d1, Diem d2) {
this.d1 = d1;
this.d2 = d2;
}
// Lấy chiều dài (Khoảng cách theo trục X giữa d1 và d2)
public double layChieuDai() {
return Math.abs(this.d2.layX() - this.d1.layX());
}
// Lấy chiều rộng (Khoảng cách theo trục Y giữa d1 và d2)
public double layChieuRong() {
return Math.abs(this.d1.layY() - this.d2.layY());
}
// Tính chu vi: 2 * (dài + rộng)
public double chuVi() {
return 2 * (layChieuDai() + layChieuRong());
}
// Tính diện tích: dài * rộng
public double dienTich() {
return layChieuDai() * layChieuRong();
}
// Biểu diễn dạng [(x1, y1), (x2, y2)]
@Override
public String toString() {
return "[" + this.d1.toString() + ", " + this.d2.toString() + "]";
}
}
Bước 3.3: Viết chương trình chính MainHCN.java (Lab 03)
public class MainHCN {
public static void main(String[] args) {
// 1. Tạo một mảng chứa 5 hình chữ nhật với các tọa độ khác nhau
HCN[] dsHCN = new HCN[] {
new HCN(new Diem(0, 5), new Diem(4, 0)), // Dài 4, rộng 5 => DT = 20
new HCN(new Diem(1, 8), new Diem(3, 2)), // Dài 2, rộng 6 => DT = 12
new HCN(new Diem(-2, 3), new Diem(5, -1)),// Dài 7, rộng 4 => DT = 28
new HCN(new Diem(0, 2), new Diem(2, 0)), // Dài 2, rộng 2 => DT = 4
new HCN(new Diem(1, 10), new Diem(6, 2)) // Dài 5, rộng 8 => DT = 40
};
// 2. Hiển thị mảng các hình chữ nhật cùng chu vi và diện tích lên màn hình
System.out.println("=== DANH SÁCH HÌNH CHỮ NHẬT BAN ĐẦU ===");
inDanhSachHCN(dsHCN);
// 3. Sắp xếp các hình chữ nhật theo thứ tự tăng dần của diện tích
for (int i = 0; i < dsHCN.length - 1; i++) {
for (int j = i + 1; j < dsHCN.length; j++) {
if (dsHCN[i].dienTich() > dsHCN[j].dienTich()) {
// Tráo đổi vị trí 2 hình chữ nhật
HCN temp = dsHCN[i];
dsHCN[i] = dsHCN[j];
dsHCN[j] = temp;
}
}
}
System.out.println("\n=== DANH SÁCH HÌNH CHỮ NHẬT BÀN GIAO SAU KHI SẮP XẾP DIỆN TÍCH TĂNG DẦN ===");
inDanhSachHCN(dsHCN);
}
public static void inDanhSachHCN(HCN[] ds) {
for (HCN hcn : ds) {
System.out.printf("HCN %s | Chu vi: %.2f | Diện tích: %.2f\n",
hcn.toString(), hcn.chuVi(), hcn.dienTich());
}
}
}
⚠️ Lỗi sai phổ biến người mới bắt đầu cần tránh!
Nhầm lẫn giữa giá trị tọa độ và độ dài cạnh:
Mắc phải: Tính diện tích bằng Math.abs(x2 - x1) mà quên nhân với Math.abs(y2 - y1).
Khắc phục: Hãy tách ra 2 hàm phụ trợ layChieuDai() và layChieuRong() riêng biệt để tránh nhầm lẫn.
Thay đổi điểm bên ngoài làm ảnh hưởng bên trong HCN (Aliasing Bug):
Trong OOP nâng cao, nếu bạn tạo Diem d = new Diem(0, 5); rồi đưa d vào HCN, nếu bên ngoài thay đổi tọa độ của d, hình chữ nhật cũng bị biến dạng theo. Bài này chưa cần xử lý phức tạp, nhưng là lưu ý tư duy quan trọng.
📝 BÀI TẬP TỰ RÈN LỰC
Đề bài: Bổ sung phương thức public boolean laHinhVuong() vào lớp HCN để kiểm tra xem hình chữ nhật đó có phải là hình vuông hay không.
public boolean laHinhVuong() {
// Hình chữ nhật là hình vuông nếu chiều dài bằng chiều rộng
return Math.abs(layChieuDai() - layChieuRong()) < 0.0001; // Dùng sai số nhỏ khi so sánh số double
}
Bổ sung: hiểu sâu từng dòng (Bài 3)
Từ mới trong bài này (giải từ số 0):
- Composition (kết hợp): object chứa object khác làm thuộc tính. HCN không lưu 4 số x1 y1 x2 y2, mà lưu 2 object
Diem— mỗi Diem tự lo tọa độ của mình. - Getter: hàm đọc thuộc tính private từ bên ngoài (
layX(),layY()). HCN phải hỏid1.layX()chứ không được sờd1.xtrực tiếp. - Math.abs(): trị tuyệt đối — đảm bảo cạnh luôn dương dù điểm nào nằm trái/phải.
- Math.sqrt(): căn bậc hai, dùng trong công thức Euclid.
- printf("%.2f"): in số thực với đúng 2 chữ số thập phân cho gọn bảng.
Mổ xẻ 4 hàm đo đạc:
public double khoangCach(Diem p) {
double dx = this.x - p.x; // chênh lệch theo trục ngang
double dy = this.y - p.y; // chênh lệch theo trục dọc
return Math.sqrt(dx * dx + dy * dy); // Pytago: cạnh huyền của tam giác vuông dx, dy
}
public double layChieuDai() {
return Math.abs(this.d2.layX() - this.d1.layX()); // |x2 - x1|
}
public double layChieuRong() {
return Math.abs(this.d1.layY() - this.d2.layY()); // |y1 - y2|
}
Chu vi = 2 × (dài + rộng), diện tích = dài × rộng — hai hàm này chỉ gọi lại hai hàm đo, không tính trực tiếp, nên nếu cách đo đổi (ví dụ dùng khoangCach) thì chỉ sửa một chỗ.
Trace tay: HCN(Diem(0, 5), Diem(4, 0)): dài = |4 − 0| = 4, rộng = |5 − 0| = 5, chu vi = 18, diện tích = 20 — khớp dòng đầu output.
Tự kiểm tra:
- Vì sao không lưu thẳng
x1, y1, x2, y2trong HCN cho đơn giản?
Đáp án
Vì như vậy HCN phải tự lo mọi thứ về điểm (khoảng cách, chuỗi...). Giao việc đó cho lớp Diem thì mỗi lớp một nhiệm vụ, code ngắn và tái dùng được.- Bỏ
Math.absthì bug nào xảy ra?
Đáp án
Nếu d2 nằm bên trái d1 thì cạnh ra số âm, diện tích âm — vô lý.abs bảo đảm thứ tự điểm không ảnh hưởng kết quả.new HCN(d1, d2)có copy d1, d2 không?
Đáp án
Không — HCN giữ đúng 2 object gốc. Ai sửa d1 bên ngoài thì HCN méo theo (lỗi aliasing, bài này chấp nhận để đơn giản).Chúc mừng bạn đã hoàn thành xong PART 1 (Khái niệm Class & Object cơ bản)!