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
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.
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
publicnằ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ệnhjava File.java, ta đặt nhiều lớp trong một file.
Đối tượng nằm ở đâu
Đ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 --> O2Biế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ề:
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.
thislà tham chiếu tới chính đối tượng đang xét.this.ten = tenphâ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ớithis— 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:
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ùngfinalcho thứ không bao giờ đổi.
Phạm vi truy cập
| Từ khoá | Truy cập được từ |
|---|---|
private | Chỉ trong cùng lớp |
| (không ghi) | Các lớp cùng package |
protected | Cùng package + lớp con (bài 7) |
public | Mọ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:
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
staticcó 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ùngstaticchủ 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:
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):
src/main/java/
└── vn/dtcode/nganhang/
├── App.java package vn.dtcode.nganhang;
├── model/
│ ├── TaiKhoan.java package vn.dtcode.nganhang.model;
│ └── GiaoDich.java
└── service/
└── ChuyenKhoanService.javapackage 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
- Viết lớp
HinhChuNhatcódai,rong(private, phải dương), constructor,dienTich(),chuVi(),laHinhVuong(). - Viết lớp
PhanSocó tử, mẫu (mẫu khác 0), tự rút gọn trong constructor, các phương thứccong,nhantrả vềPhanSomới. - 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ặcStringBuilder). - Viết lớp
GioHangchứa mảngSanPham(tối đa 10), phương thứcthem,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;
thischỉ 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;finalcho thứ không đổi; đừng sinh setter máy móc. staticthuộc về lớp (dùng chung), instance thuộc về từng đối tượng; dùngstaticcho 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.
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.