Bỏ qua điều hướng
EN
Tất cả bài viết

Java cơ bản #6: lớp và đối tượng — constructor, this, đóng gói, static

Bước vào lập trình hướng đối tượng: lớp là bản thiết kế, đối tượng là thực thể; thuộc tính và phương thức, constructor, từ khoá this, đóng gói với private + getter/setter có kiểm tra, static khác gì instance, null và NullPointerException, package và tổ chức file.

12 phút đọc

Ôn tập bài nàyThẻ ghi nhớ & câu hỏi ôn tập7 thẻ ghi nhớ · 6 câu trắc nghiệm
Mục lục

Java là ngôn ngữ hướng đối tượng: chương trình được tổ chức quanh các đối tượng mô phỏng thứ ta đang xử lý — tài khoản ngân hàng, đơn hàng, sinh viên — mỗi đối tượng gom dữ liệu và hành vi liên quan lại một chỗ.

Tới giờ, dữ liệu của ta nằm rải rác: mảng tên, mảng điểm, mảng tuổi, phải giữ cho cùng chỉ số khớp nhau. Bài này cho thấy vì sao gom chúng vào một lớp lại dễ hơn nhiều, và các khái niệm cốt lõi: constructor, this, đóng gói, static.

Lớp và đối tượng

  • Lớp (class): bản thiết kế — mô tả một loại thứ có những thuộc tính (field) gì và làm được gì (phương thức).
  • Đối tượng (object): một thực thể cụ thể tạo ra từ lớp bằng new. Một lớp tạo được vô số đối tượng.
LopDauTien.java
class SinhVien {
    String hoTen;          // thuộc tính (field)
    int namSinh;
    double diemTb;
 
    int tuoi(int namHienTai) {                 // phương thức dùng dữ liệu của chính đối tượng
        return namHienTai - namSinh;
    }
 
    String xepLoai() {
        return diemTb >= 8 ? "Giỏi" : diemTb >= 6.5 ? "Khá" : "Trung bình";
    }
}
 
void main() {
    SinhVien an = new SinhVien();      // tạo đối tượng
    an.hoTen = "Nguyễn Văn An";
    an.namSinh = 2005;
    an.diemTb = 8.2;
 
    SinhVien binh = new SinhVien();    // đối tượng thứ hai, dữ liệu riêng
    binh.hoTen = "Trần Thị Bình";
    binh.namSinh = 2006;
    binh.diemTb = 7.1;
 
    for (SinhVien sv : new SinhVien[]{an, binh}) {
        IO.println(sv.hoTen + " — " + sv.tuoi(2026) + " tuổi — " + sv.xepLoai());
    }
}

Trong dự án thật, mỗi lớp public nằm trong file riêng cùng tên (SinhVien.java). Ở loạt bài, để ví dụ chạy được bằng một lệnh java File.java, ta đặt nhiều lớp trong một file.

Đối tượng nằm ở đâu

Biến tham chiếu và đối tượng

Đang vẽ sơ đồ…

flowchart LR
  subgraph Stack["Biến (stack)"]
    A["an"]
    B["binh"]
  end
  subgraph Heap["Đối tượng (heap)"]
    O1["SinhVien<br/>hoTen = Nguyễn Văn An<br/>diemTb = 8.2"]
    O2["SinhVien<br/>hoTen = Trần Thị Bình<br/>diemTb = 7.1"]
  end
  A --> O1
  B --> O2

Biến an không chứa đối tượng mà chứa tham chiếu tới đối tượng nằm trên vùng nhớ heap. SinhVien c = an; chỉ tạo thêm một tham chiếu tới cùng đối tượng — như đã thấy với mảng ở bài 4. Đối tượng không còn tham chiếu nào trỏ tới sẽ được bộ thu gom rác (garbage collector) tự giải phóng.

Constructor

Gán từng thuộc tính sau khi new rất dễ quên. Constructor là phương thức đặc biệt chạy khi tạo đối tượng — trùng tên lớp, không có kiểu trả về:

Constructor.java
class SanPham {
    String ten;
    long gia;
    int soLuong;
 
    SanPham(String ten, long gia, int soLuong) {
        this.ten = ten;           // this.ten: thuộc tính; ten: tham số
        this.gia = gia;
        this.soLuong = soLuong;
    }
 
    SanPham(String ten, long gia) {
        this(ten, gia, 0);        // gọi constructor khác của cùng lớp, tránh lặp code
    }
 
    long thanhTien() {
        return gia * soLuong;
    }
}
 
void main() {
    var ao = new SanPham("Áo thun", 150_000, 3);
    var mu = new SanPham("Mũ lưỡi trai", 90_000);
    IO.println(ao.ten + ": " + ao.thanhTien());
    IO.println(mu.ten + ": " + mu.soLuong + " cái");
}
  • Không viết constructor nào → Java tự tạo constructor mặc định không tham số. Viết một constructor có tham số → constructor mặc định biến mất.
  • this là tham chiếu tới chính đối tượng đang xét. this.ten = ten phân biệt thuộc tính với tham số trùng tên. this(...) gọi constructor khác (phải là dòng đầu tiên… trừ các lệnh không đụng tới this — Java 25 nới lỏng quy tắc này để kiểm tra tham số trước).

Đóng gói (encapsulation)

Thuộc tính để ai cũng sửa được thì không có gì ngăn ai đó viết taiKhoan.soDu = -1_000_000. Đóng gói: giấu dữ liệu bằng private, chỉ cho thay đổi qua phương thức có kiểm tra quy tắc:

ViDuTaiKhoan.java
class TaiKhoan {
    private final String soTaiKhoan;   // final: gán một lần trong constructor, không đổi được nữa
    private final String chuTk;
    private long soDu;                 // private: bên ngoài lớp không đọc/sửa trực tiếp được
 
    TaiKhoan(String soTaiKhoan, String chuTk) {
        if (soTaiKhoan == null || !soTaiKhoan.matches("\\d{10}")) {
            throw new IllegalArgumentException("Số tài khoản phải gồm 10 chữ số");
        }
        this.soTaiKhoan = soTaiKhoan;
        this.chuTk = chuTk;
    }
 
    long getSoDu() { return soDu; }               // getter: chỉ đọc
    String getChuTk() { return chuTk; }
 
    void napTien(long soTien) {
        if (soTien <= 0) throw new IllegalArgumentException("Số tiền nạp phải dương");
        soDu += soTien;
    }
 
    boolean rutTien(long soTien) {
        if (soTien <= 0 || soTien > soDu) return false;   // quy tắc nghiệp vụ nằm Ở MỘT CHỖ
        soDu -= soTien;
        return true;
    }
 
    boolean chuyenKhoan(TaiKhoan den, long soTien) {
        if (!rutTien(soTien)) return false;
        den.napTien(soTien);
        return true;
    }
}
 
void main() {
    var a = new TaiKhoan("0123456789", "An");
    var b = new TaiKhoan("9876543210", "Bình");
    a.napTien(5_000_000);
    IO.println("Rút 7 triệu: " + a.rutTien(7_000_000));        // false — không đủ tiền
    IO.println("Chuyển 2 triệu: " + a.chuyenKhoan(b, 2_000_000));
    IO.println(a.getChuTk() + ": " + a.getSoDu() + " | " + b.getChuTk() + ": " + b.getSoDu());
    // a.soDu = 999;  → lỗi biên dịch: soDu has private access in TaiKhoan
}

Lợi ích: mọi quy tắc về số dư nằm trong lớp TaiKhoan. Muốn đổi quy tắc (thêm hạn mức rút mỗi ngày), chỉ sửa một chỗ.

Lưu ý: đừng máy móc sinh getter và setter cho mọi thuộc tính — setter mở toang dữ liệu y như để public. Chỉ cung cấp những thao tác có nghĩa nghiệp vụ (napTien, rutTien), và dùng final cho thứ không bao giờ đổi.

Phạm vi truy cập

Từ khoáTruy cập được từ
privateChỉ trong cùng lớp
(không ghi)Các lớp cùng package
protectedCùng package + lớp con (bài 7)
publicMọi nơi

Quy tắc mặc định: thuộc tính private, phương thức public nếu là "giao diện" của lớp, private nếu chỉ là phương thức phụ trợ bên trong.

static: thuộc về lớp, không thuộc đối tượng

Thuộc tính thường (instance) — mỗi đối tượng có bản riêng. Thuộc tính static — một bản duy nhất dùng chung cho cả lớp:

Static.java
class DonHang {
    private static int demDonHang = 0;          // dùng chung cho mọi đơn hàng
    static final double PHI_VAN_CHUYEN = 30_000; // hằng số của lớp
 
    private final int maDon;
    private final double tienHang;
 
    DonHang(double tienHang) {
        demDonHang++;
        this.maDon = demDonHang;
        this.tienHang = tienHang;
    }
 
    double tongTien() {
        return tienHang >= 500_000 ? tienHang : tienHang + PHI_VAN_CHUYEN;
    }
 
    static int soDonDaTao() {        // phương thức static: gọi qua tên lớp, không cần đối tượng
        return demDonHang;
        // không dùng được this hay maDon ở đây — phương thức static không gắn với đối tượng nào
    }
 
    int getMaDon() { return maDon; }
}
 
void main() {
    var d1 = new DonHang(200_000);
    var d2 = new DonHang(800_000);
    IO.println("Đơn " + d1.getMaDon() + ": " + d1.tongTien());
    IO.println("Đơn " + d2.getMaDon() + ": " + d2.tongTien());
    IO.println("Đã tạo " + DonHang.soDonDaTao() + " đơn, phí ship " + DonHang.PHI_VAN_CHUYEN);
    IO.println(Math.max(3, 7));      // Math.max cũng là phương thức static
}

Đây là lý do main truyền thống có static: JVM gọi main khi chưa có đối tượng nào. Và là lý do phương thức gọi từ public static void main cũng phải static (hoặc phải tạo đối tượng trước).

Lưu ý: biến static có thể sửa được (như demDonHang) là trạng thái toàn cục — dễ gây lỗi khó tìm khi chương trình lớn hoặc chạy đa luồng. Dùng static chủ yếu cho hằng số (static final) và phương thức tiện ích không cần trạng thái.

null và NullPointerException

Biến tham chiếu chưa trỏ tới đối tượng nào có giá trị null. Gọi phương thức trên null là lỗi phổ biến nhất của Java:

NullPointer.java
class Nguoi {
    String ten;
    Nguoi(String ten) { this.ten = ten; }
}
 
void main() {
    Nguoi[] danhSach = new Nguoi[3];      // 3 phần tử, đều là null
    danhSach[0] = new Nguoi("Mai");
    for (Nguoi n : danhSach) {
        if (n != null) {
            IO.println(n.ten.toUpperCase());
        } else {
            IO.println("(trống)");
        }
    }
    String ten = danhSach[1] == null ? "khách" : danhSach[1].ten;
    IO.println("Xin chào " + ten);
    // danhSach[2].ten → NullPointerException: Cannot read field "ten" because "<local>[2]" is null
}

Cách phòng: khởi tạo đầy đủ trong constructor, kiểm tra tham số đầu vào, tránh trả về null (bài 11 có Optional). Từ Java 14, thông báo NullPointerException chỉ rõ biến nào là null — đọc kỹ nó.

Package và tổ chức file

Dự án thật có hàng trăm lớp, được nhóm vào package (tương ứng thư mục):

cau-truc-du-an
src/main/java/
└── vn/dtcode/nganhang/
    ├── App.java              package vn.dtcode.nganhang;
    ├── model/
    │   ├── TaiKhoan.java     package vn.dtcode.nganhang.model;
    │   └── GiaoDich.java
    └── service/
        └── ChuyenKhoanService.java
vn/dtcode/nganhang/App.java (minh hoạ)
package vn.dtcode.nganhang;
 
import vn.dtcode.nganhang.model.TaiKhoan;      // dùng lớp ở package khác phải import (nếu lớp đó public)
 
public class App {
    public static void main(String[] args) {
        TaiKhoan tk = new TaiKhoan("0123456789", "An");
        tk.napTien(1_000_000);
        System.out.println(tk.getSoDu());
    }
}

Bài 12 sẽ dựng một dự án Maven đúng cấu trúc này.

Bài tập

  1. Viết lớp HinhChuNhat có dai, rong (private, phải dương), constructor, dienTich(), chuVi(), laHinhVuong().
  2. Viết lớp PhanSo có tử, mẫu (mẫu khác 0), tự rút gọn trong constructor, các phương thức cong, nhan trả về PhanSo mới.
  3. Thêm vào TaiKhoan: hạn mức rút tối đa 20 triệu mỗi lần và lịch sử giao dịch (mảng chuỗi hoặc StringBuilder).
  4. Viết lớp GioHang chứa mảng SanPham (tối đa 10), phương thức them, tongTien, inHoaDon.

Tổng kết

  • Lớp là bản thiết kế (thuộc tính + phương thức); đối tượng là thực thể tạo bằng new; biến chỉ giữ tham chiếu.
  • Constructor khởi tạo đối tượng hợp lệ ngay từ đầu; this chỉ chính đối tượng, this(...) gọi constructor khác.
  • Đóng gói: thuộc tính private, thay đổi qua phương thức có kiểm tra; final cho thứ không đổi; đừng sinh setter máy móc.
  • static thuộc về lớp (dùng chung), instance thuộc về từng đối tượng; dùng static cho hằng số và tiện ích.
  • Kiểm soát null để tránh NullPointerException; tổ chức lớp theo package.
  • Bài tiếp theo: kế thừa, đa hình và interface — ba trụ cột còn lại của OOP.

Nguồn tham khảo: dev.java — "Classes and Objects"; The Java Language Specification (Java SE 25), chương 8; JEP 513: Flexible Constructor Bodies; Bloch — Effective Java, mục 15–17 (tối thiểu hoá khả năng truy cập, ưu tiên bất biến); Java Programming MOOC — University of Helsinki, phần 4–5.

Đang tải câu hỏi luyện tập…

Bài này thế nào?

Bài viết có hữu ích với bạn?

Đánh giá giúp mình biết nên viết thêm về chủ đề nào.

Đang tải đánh giá…
Chọn số sao từ 1 đến 5

Bình luận

Đăng nhập để bình luận không cần nhập tên và được duyệt nhanh hơn.

0/2000
Đang tải bình luận…

Đọc tiếp

Bài số № 51

Java cơ bản #8: record, enum và equals, hashCode, toString

Ba phương thức mọi đối tượng đều có — toString, equals, hashCode — vì sao phải ghi đè chúng cùng nhau và cái bẫy khi quên; record (Java 16+) viết lớp dữ liệu bất biến trong một dòng; enum cho tập giá trị cố định, có thuộc tính, phương thức và dùng trong switch.

15 phút đọc

Bài số № 50

Java cơ bản #7: kế thừa, đa hình, lớp trừu tượng, interface và sealed

Ba trụ cột còn lại của OOP: kế thừa với extends và super, ghi đè @Override, đa hình (một lời gọi, nhiều hành vi), lớp trừu tượng, interface và default method, instanceof với pattern matching, sealed + switch pattern, và nguyên tắc ưu tiên kết hợp (composition) hơn kế thừa.

17 phút đọc

Bài số № 55

Java cơ bản #12: dự án cuối — ứng dụng quản lý chi tiêu với Maven và JUnit

Gom toàn bộ kiến thức vào một dự án thật: cấu trúc Maven và pom.xml, chia lớp model–kho dữ liệu–service–giao diện, lưu file CSV, thống kê bằng Stream, cảnh báo ngân sách, viết kiểm thử JUnit 5, đóng gói file jar chạy được — và lộ trình học tiếp sau Java cơ bản.

19 phút đọc