Khóa OOP người mới: Bài 3 — Điểm & Hình chữ nhật

Bài 3 thuộc Lab 03: object chứa object (composition) qua lớp Điểm và Hình chữ nhật.

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:

Lớp HCN:

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):

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:

  1. Vì sao không lưu thẳng x1, y1, x2, y2 trong HCN cho đơn giản?
Đáp ánVì 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.
  1. Bỏ Math.abs thì bug nào xảy ra?
Đáp ánNế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ả.
  1. new HCN(d1, d2) có copy d1, d2 không?
Đáp ánKhô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)!