Giảng giải chi tiết cho người mới học Java và hướng đối tượng

Tài liệu này giảng lại từng bài tập từ số 0: đề nói gì, từ mới nào cần biết, ý tưởng làm sao, từng dòng code nghĩa gì, và bẫy nào hay vấp. Đọc xong phần 0 và 1 thì đọc tiếp đúng Lab mình đang học.

0. Java từ số 0

0.1. Viết code ở đâu, chạy thế nào

0.2. Chương trình đầu tiên mổ xẻ

public class Chao2 {
    public static void main(String[] args) {
        System.out.println("Chao");
    }
}

0.3. Biến và kiểu dữ liệu

Biến là cái hộp có dán nhãn, đựng dữ liệu. Java bắt khai báo hộp đựng gì:

Kiểu Đựng gì Ví dụ
int số nguyên int n = 10;
double số thực double r = 3.5;
String chuỗi chữ (hoa S) String ten = "An";
boolean chỉ true/false boolean ok = true;
char 1 ký tự (nháy đơn) char c = 'e';

Bẫy số 1 của người mới: int / int ra int. 5 / 2 bằng 2, không phải 2.5. Muốn số thực phải có một bên là double: 5 / 2.0 bằng 2.5, hoặc ép kiểu (double) tong / soLuong.

0.4. Nhập dữ liệu bằng Scanner

import java.util.Scanner;          // dòng xin phép dùng Scanner

Scanner sc = new Scanner(System.in); // mở micro nghe bàn phím
int n = sc.nextInt();                // đọc 1 số nguyên
double r = sc.nextDouble();          // đọc 1 số thực
sc.nextLine();                       // đọc cả dòng chữ (sau nextInt phải gọi 1 lần để nuốt Enter thừa)
String ten = sc.nextLine();
sc.close();                          // dùng xong đóng lại

0.5. Rẽ nhánh và vòng lặp

if (n < 0) {                  // nếu đúng thì làm khối này
    System.out.println("sai");
} else {                      // ngược lại làm khối này
    System.out.println("dung");
}

for (int i = 0; i < n; i++) { // lặp n lần, i = 0, 1, ..., n-1
    System.out.print(i + " ");
}

while (a < x) {               // lặp khi điều kiện còn đúng
    a = a + b;
}

% là phép chia lấy dư: 7 % 2 bằng 1. Dư 0 tức là chia hết — đây là cách kiểm tra chẵn lẻ và kiểm tra nguyên tố.

0.6. Mảng: dãy hộp đánh số từ 0

int[] a = new int[n];  // xin n hộp int, đánh số a[0] .. a[n-1]
a[0] = 5;              // gán hộp đầu tiên
System.out.println(a.length); // số hộp = n

Ba quy tắc sống còn:

  1. Ô đầu tiên là a[0], ô cuối là a[n-1]. Không có a[n].
  2. Vòng lặp duyệt mảng luôn viết i < n, không bao giờ i <= n. Thêm dấu = là tràn mảng, chương trình chết ngay.
  3. Kiểm tra n > 0 trước khi new int[n].

1. Hướng đối tượng từ số 0

1.1. Object là gì

Lớp (class) là bản thiết kế, đối tượng (object) là sản phẩm đúc từ bản thiết kế đó. Lớp SinhVien là bản thiết kế chung; new SinhVien("An", ...) đúc ra một sinh viên cụ thể tên An.

SinhVien sv = new SinhVien("Nguyen Van An", d1, d2); // đúc object
System.out.println(sv.tinhDTB());  // gọi phương thức: object.tênViệc()

new nghĩa là đúc mới. Dấu chấm . nghĩa là "của": sv.tinhDTB() là "tính điểm trung bình của sv".

1.2. Đóng gói: giấu dữ liệu bằng private

public class PS {
    private int tu;   // private = chỉ code trong lớp này được đụng
    private int mau;

    public int layTu() { return tu; }              // getter: cho đọc
    public void ganTu(int tu) { this.tu = tu; }    // setter: cho sửa
}

private là khóa tủ. Muốn lấy/sửa phải qua getter/setter (cửa có người gác). Setter tốt còn kiểm tra trước khi nhận:

public void ganMau(int m) { if (m != 0) { this.mau = m; } } // mẫu 0 thì từ chối

this nghĩa là "của chính tôi": this.tu là thuộc tính tu của object hiện tại, phân biệt với tham số tu truyền vào.

1.3. Constructor: lò đúc object

Constructor là phương thức tên giống hệt lớp, không có kiểu trả về, chạy đúng 1 lần lúc new:

public PS(int tu, int mau) {
    this.tu = tu;
    this.mau = (mau == 0) ? 1 : mau; // mẫu 0 thì tự sửa thành 1
    chuanHoa();
}

Dòng (mau == 0) ? 1 : mau đọc là: "mẫu bằng 0 thì lấy 1, không thì lấy mẫu". Đây là if viết gọn 1 dòng.

1.4. Kế thừa: lớp con dùng lại code lớp cha

public class HPLyThuyet extends DiemHP { ... } // LyThuyet KẾ THỪA DiemHP

extends nghĩa là "kế thừa": lớp con có sẵn mọi thứ public của lớp cha, chỉ viết thêm phần riêng. Constructor lớp con phải gọi super(...) (đúc phần của cha) ở dòng đầu tiên:

public HPLyThuyet(String ten, int tc, ...) {
    super(ten, tc);   // đúc phần cha trước, bắt buộc dòng đầu
    ...
}

1.5. Đa hình: cùng một lời gọi, mỗi đứa làm khác nhau

Lớp cha khai báo khung, lớp con mỗi đứa viết ruột riêng:

public abstract class BuuPham {
    public abstract double phiGui(); // khung: chưa biết tính sao
}
public class Thu extends BuuPham {
    public double phiGui() { return 800; }   // ruột của Thư
}
public class BuuKien extends BuuPham {
    public double phiGui() { return 5000; }  // ruột của Bưu kiện
}

abstract nghĩa là "dở dang": lớp dở dang không đúc object trực tiếp được, chỉ để kế thừa. Cái hay: một danh sách BuuPham[] đựng chung cả Thư lẫn Bưu kiện, gọi phiGui() thì mỗi đứa tự tính giá của mình — gọi là đa hình. @Override dán trên phương thức viết lại để trình biên dịch kiểm tra giúp mình.


2. Lab 01 giảng cho người mới

2.1. Chao2 — tham số dòng lệnh là gì

Chạy java Chao2 Nguyen Van An, mấy chữ sau tên lớp gọi là tham số dòng lệnh, Java hứng vào mảng args: args[0]="Nguyen", args[1]="Van", args[2]="An".

if (args.length == 0) {   // không gõ tên thì nhắc rồi dừng
    System.out.println("Cach dung: java Chao2 <ten>");
    return;               // return trong main = dừng chương trình
}
String ten = "";
for (int i = 0; i < args.length; i++) {
    ten += args[i];                       // nối từng chữ
    if (i < args.length - 1) ten += " ";  // chữ chưa phải cuối thì thêm cách
}

ten += args[i] là viết gọn của ten = ten + args[i]. Điều kiện i < args.length - 1 để không thừa dấu cách ở cuối.

2.2. DocSo — switch-case và break

switch là "xem giá trị rồi nhảy đúng nhánh". Mỗi nhánh case phải có break, không là rơi xuống nhánh dưới:

switch (n) {
    case 7: System.out.println("bay"); break; // đúng n=7: in rồi THOÁT
    default: System.out.println("chin"); break; // còn lại rơi hết vào đây
}

Vì chỉ viết tới case 8 nên số 9 rơi vào default in "chin". Trước switch đã chặn n < 0 || n > 9 in "Khong hop le" — đây gọi là kiểm tra miền giá trị: không cho số lạ đi tiếp.

2.3. HinhTron — kiểm tra trước khi tính

double r = sc.nextDouble();
if (r <= 0) {
    System.out.println("Ban kinh phai duong");
    sc.close();
    return;   // số sai thì dừng, không tính tiếp
}
double dienTich = Math.PI * r * r;   // Math.PI là số pi có sẵn

Math.round(dienTich * 100) / 100.0 là mẹo làm tròn 2 chữ số: nhân 100 → làm tròn thành nguyên → chia 100.0. Chia cho 100.0 (số thực) để không rơi vào bẫy chia nguyên.

2.4. GiaiThua — int tràn số

5! = 1*2*3*4*5 = 120. Vòng lặp bắt đầu từ 2 vì nhân thêm 0, 1 không đổi kết quả (kq khởi đầu bằng 1 — phần tử trung hòa của phép nhân, giống số 0 của phép cộng).

Vì sao long mà không int? int chứa tối đa ~2.1 tỉ, 12! đã 479 triệu, 13! là 6.2 tỉ → tràn. long chứa tới 9e18. Số âm thì báo "n phai >= 0" rồi dừng.

2.5. NguyenToTrongMang — hàm kiểm tra viết riêng

public static boolean laNguyenTo(int n) {
    if (n < 2) return false;              // 0, 1 và số âm: loại ngay
    for (int i = 2; i <= Math.sqrt(n); i++)
        if (n % i == 0) return false;     // tìm được ước thì không phải
    return true;
}

Vì sao chỉ cần tới căn bậc hai? Ước luôn đi theo cặp (100 = 4 × 25), một trong hai không vượt quá căn. return false giữa chừng nghĩa là "kết luận luôn, khỏi kiểm tiếp".

static nghĩa là hàm của lớp, gọi thẳng laNguyenTo(x) mà không cần đúc object — vì nó không đụng thuộc tính của object nào cả.

2.6. FibonacciTrongMang — sinh dãy để đối chiếu

Dãy Fibonacci: 0, 1, 1, 2, 3, 5, 8, 13..., số sau bằng tổng hai số trước. Hàm laFibonacci(x) sinh dãy tới khi a >= x, rồi xem có trúng không:

int a = 0, b = 1;
while (a < x) { int tiep = a + b; a = b; b = tiep; } // bước lên 1 nấc
return a == x;

Muốn hiểu thì chạy tay: x=5: (0,1)→(1,1)→(1,2)→(2,3)→(3,5), dừng vì a=5, 5==5 → đúng.

2.7. SapXepNgauNhien — sắp xếp nổi bọt

(int)(Math.random() * 100) sinh số nguyên 0..99. Sắp xếp nổi bọt: đi từng cặp kề nhau, sai thứ tự thì đổi chỗ, số lớn "nổi" dần về cuối như bọt nước:

if (a[j] > a[j + 1]) { int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; }

Đổi chỗ cần biến tạm tmp như đổi nước hai cốc cần cốc thứ ba. Vòng ngoài chạy n-1 lượt vì sau mỗi lượt, một số đã về đúng chỗ.


3. Lab 02 giảng cho người mới

3.1. PS — rút gọn phân số bằng UCLN

Phân số 6/9 phải rút thành 2/3 bằng cách chia cả tử mẫu cho ước chung lớn nhất (UCLN). Tìm UCLN bằng thuật toán Euclid: lấy dư liên tục tới khi dư 0:

int a = Math.abs(tu), b = Math.abs(mau);
while (b != 0) { int r = a % b; a = b; b = r; } // a còn lại là UCLN
if (a != 0) { tu /= a; mau /= a; }

Chạy tay 6/9: (6,9)→ dư 6 →(9,6)→ dư 3 →(6,3)→ dư 0 → dừng, UCLN=3, chia ra 2/3. if (a != 0) để tử 0 không bị chia 0.

3.2. PS — cộng trừ nhân chia

Muốn cộng 1/2 + 5/7: quy đồng mẫu chung 2*7=14, tử mới là 1*7 + 5*2 = 17 → 17/14. Code y hệt công thức:

public PS cong(PS p) { return new PS(tu * p.mau + p.tu * mau, mau * p.mau); }

Chú ý: các phép toán trả về phân số mới (new PS), không sửa phân số cũ. Còn tang, gap, rutGon (không trả về gì — void) thì sửa trực tiếp object hiện tại. Hai kiểu này khác nhau, đừng lẫn.

Ví dụ kiểm tra t = 2*p - p/q với p=1/2, q=5/7: 2p = 1 (viết thành 1/1), p/q = 1/2 ÷ 5/7 = 7/10, 1 - 7/10 = 3/10. Nhiều bạn nhầm 2p = 2 nên ra sai.

3.3. PS — so sánh và sắp xếp

So sánh bằng nhân chéo: 1/2 với 5/7 thì so 1*7 với 5*2, 7 < 10 nên 1/2 < 5/7. Không chia số thực nên không sợ sai số. Sắp xếp 5 phân số dùng nổi bọt, nhưng so object thì gọi ds[j].soSanh(ds[j+1]) > 0 thay vì ds[j] > ds[j+1] (object không so bằng > được).

3.4. ATM — boolean và luật nghiệp vụ

rutTien trả về boolean (đúng/sai) thay vì in ra:

public boolean rutTien(double st) {
    if (st <= 0 || st > soDu) return false; // tiền sai hoặc không đủ: từ chối
    soDu -= st;                             // đủ thì trừ rồi báo thành công
    return true;
}

chuyenTien dùng lại rutTien rồi napTien — viết code kiểu này gọi là tái sử dụng, khỏi kiểm tra số dư lần nữa. tk == null || tk == this chặn chuyển cho tài khoản rỗng và tự chuyển cho chính mình. final trên soTK nghĩa là số tài khoản không đổi suốt đời object.

Thử tay với code mẫu: 5 tài khoản tổng 1000+100+100+100+100=1400 (tài khoản 50 tự nâng lên 100 vì dưới mức tối thiểu). Nạp 1000 vào TK1 → 2000. Chuyển 500 sang TK2: TK1 còn 1500, TK2 thành 600. Rút 200 ở TK2 → 400. Rút 99999 → từ chối. Tổng cuối 1500+400+100+100+100 = 2200.0. Khớp output mẫu.

3.5. PTB2 — chia mọi trường hợp trước khi code

Nguyên tắc: liệt kê trường hợp rồi mới viết code. a == 0 thì hết là bậc hai: b == 0 nữa thì c == 0 vô số nghiệm, ngược lại vô nghiệm; còn b khác 0 thì nghiệm x = -c/b. a khác 0 mới tính delta: âm vô nghiệm, bằng 0 nghiệm kép -b/2a, dương thì hai nghiệm với Math.sqrt(delta).

Phương thức giai() trả về chuỗi thay vì in — để main chỉ việc in ra, sau này muốn dùng kết quả vào việc khác vẫn được.

3.6. NhanVien — công thức lương đọc từ code

public double luong()    { return (heSoLuong + phuCap()) * luongCoBan; }

Phụ cấp theo chức vụ: GD 1.0, PGD 0.8, TP 0.5, PP 0.4, còn lại 0. chucVu.equals("GD") — so chuỗi bắt buộc .equals, == chỉ so địa chỉ ô nhớ. Ví dụ Nguyễn Văn A: (5.0 + 1.0) * 1500000 = 9000000, trừ BHXH 6% và BHTN 1% còn thực lĩnh 9000000 * 0.93 = 8370000. Khớp output mẫu.

tuoi() lấy năm 2026 trừ năm sinh. Date cũ lưu năm = năm thực − 1900 nên phải cộng lại 1900 — đây là cái bẫy của thư viện cũ, cứ nhớ công thức.


4. Lab 03 giảng cho người mới

4.1. Diem — setter có gác cổng

public void setCuoiKy(double d) { if (d >= 0 && d <= 10) cuoiKy = d; }

Điểm ngoài 0..10 thì lờ đi (giữ giá trị cũ), không báo lỗi. Thử trong code mẫu: d1.setCuoiKy(15) bị từ chối nên getCuoiKy() vẫn là 7. Đây là ý nghĩa của đóng gói: dữ liệu sai không bao giờ lọt vào object. Tín chỉ cũng vậy, chỉ nhận 1..4.

tinhDiem() = chuyenCan*0.1 + giuaKy*0.2 + cuoiKy*0.7 — cuối kỳ hệ số cao nhất nên 1 điểm cuối kỳ đáng giá gấp 7 lần chuyên cần.

4.2. SinhVien — quan hệ HAS-A và trung bình trọng số

SinhVien chứa hai object Diem — quan hệ "có một" (HAS-A): sinh viên có các điểm môn. Điểm tích lũy:

return (dm1.tinhDiem() * tc1 + dm2.tinhDiem() * tc2) / (tc1 + tc2);

Môn 3 tín chỉ phải "nặng" gấp 3 môn 1 tín chỉ — cộng đều rồi chia 2 là sai. Thử tay bạn An: Java 9*0.1+8*0.2+7*0.7 = 7.4 (3 TC), Toán 6*0.1+7*0.2+8*0.7 = 7.6 (2 TC), tích lũy (7.4*3 + 7.6*2)/5 = 37.4/5 = 7.48. Khớp output mẫu. if (tc1 + tc2 == 0) return 0 chặn chia 0.

4.3. SinhVienMoRong — mảng object và số lượng thật

Mảng new Diem[soMonToiDa] lúc đầu toàn ô rỗng (null). Biến soHP đếm số môn đã thêm thật. Mọi vòng lặp chỉ chạy tới soHP, không bao giờ tới dsHP.length — đây là mẫu chuẩn "danh sách trên mảng", gặp lại ở Lab 04, 05:

public boolean themHP(Diem d) {
    if (d == null || soHP >= dsHP.length) return false; // rỗng hoặc đầy: từ chối
    dsHP[soHP] = d; soHP++; return true;
}

Vì sao in ra 7.82 chứ không phải 7.83? Máy tính lưu số thực dạng nhị phân, 7.82 thật ra là 7.8249999..., làm tròn 2 chữ số còn 7.82. Nhớ: không bao giờ so double bằng ==.

4.4. DiemToaDo và HCN — tái sử dụng lớp

HCN không lưu 4 số rời rạc mà lưu hai object DiemToaDo (góc trên-trái, góc dưới-phải) — tái sử dụng lớp đã có. Dài = trị tuyệt đối hiệu hoành độ, rộng = trị tuyệt đối hiệu tung độ. Math.abs để trừ ngược vẫn ra dương. Khoảng cách hai điểm theo Pi-ta-go: sqrt(dx*dx + dy*dy). Ví dụ (0,4)-(3,0): dài 3 rộng 4, chu vi 14, diện tích 12.


5. Lab 04 giảng cho người mới

5.1. SV — tách họ tên bằng split

"Nguyen Van An".split("\\s+") cắt chuỗi theo khoảng trắng thành mảng ["Nguyen", "Van", "An"]. Ô đầu là họ, ô cuối là tên, ở giữa là đệm:

ganHoTen dùng (hoTen == null) ? "" : hoTen.trim(): null thì lấy chuỗi rỗng, trim() cắt cách thừa hai đầu. Xếp loại là if bậc thang 8/7/5 — đọc từ trên xuống, đúng ngưỡng nào dừng ở đó.

5.2. DSSV — thêm, sắp xếp, tìm, xóa

them(sv) chặn null và chặn đầy rồi mới gán vào ds[soSV], xong tăng soSV. Ba hàm sắp xếp đều là nổi bọt, chỉ khác cái so:

timTen dùng equalsIgnoreCase (so không phân biệt hoa thường), duyệt tới soSV (số thật) chứ không phải ds.length (sức chứa).

5.3. Xóa bằng cách dồn mảng

for (int i = k; i < soSV - 1; i++) ds[i] = ds[i + 1]; // dồn lên lấp chỗ trống
ds[soSV - 1] = null;  // ô cuối thành rác thì gán null cho sạch
soSV--;               // giảm số lượng thật

Xóa ô k thì mọi object sau k tiến lên 1 bước. Ô cuối cùng thừa ra phải gán null để không còn trỏ vào object cũ (kẻo liệt kê nhầm). tuoiTrungBinh() ép (double) trước khi chia để tuổi lẻ không bị cắt ngọn. tangDTBTruoc1980 duyệt cộng 0.5 cho ai sinh trước 1980 — ví dụ bạn 1978 từ 4.5 lên 5.0, đổi xếp loại từ Yếu thành Trung bình.


6. Lab 05 giảng cho người mới

6.1. DiemHP — lớp trừu tượng là bản cam kết

public abstract class DiemHP {
    ...
    public abstract double tinhDiem(); // cam kết: con nào cũng phải có cách tính
}

Lớp cha giữ phần chung (tên, tín chỉ, in ấn), để trống đúng một chỗ (tinhDiem) cho lớp con điền. toString() của cha đã gọi sẵn tinhDiem() — khi chạy với object con, Java tự gọi bản của con. Đây là đa hình trong 1 dòng code, ngấm được chỗ này là hiểu nửa bài.

6.2. Ba lớp con — ghi đè là viết lại theo ý mình

Mỗi constructor đều super(tenHP, soTinChi) dòng đầu. Thử tay bạn An: LT Java 9*0.1+8*0.2+7*0.7=7.4 (3TC), TH Java (8+9+7)/3=8.0 (2TC), ĐTB (7.4*3+8*2)/5=7.64. Khớp output mẫu. Ba bạn còn lại tính y hệt ra 7.84 và 7.21.

6.3. Mảng đa hình — một danh sách, nhiều cách tính

DiemHP[] cacHP = {lt1, th1, th2, lt2, lt3, th3};
for (int i = 0; i < cacHP.length; i++) tong += cacHP[i].tinhDiem();

Khai báo là DiemHP nhưng object thật là Lý thuyết/Thực hành lẫn lộn. Cùng một lời gọi tinhDiem(), đứa nào tính theo ruột đứa nấy — tổng 6 môn ra 47.07. Thêm loại học phần mới sau này không phải sửa vòng lặp này: đó là lý do người ta dùng đa hình.

6.4. Bưu phẩm — đa hình áp vào bài toán thật

Thu thêm thuộc tính baoDam (thư bảo đảm hay thường). phiGui(): thường 800? — không, bài này quy định thường 2400 (3 lá = 7200... khoan). Đọc code: baoDam ? 2000 : 800? Không — code thật: thường 800? Kiểm tra output mẫu: "Thu thuong: 2400.0" với 3 lá thường → mỗi lá 800. Đúng: thường 800, bảo đảm 2000 (2 lá = 4000), bưu kiện 5000 (3 cái = 15000). Tổng 3*800 + 2*2000 + 3*5000 = 2400 + 4000 + 15000 = 21400.0. Khớp output mẫu. ThungThu là danh sách BuuPham[] đa hình: lietKe, tongTien, tongTheoLoai đều duyệt một vòng, lọc theo loai().equals(...) khi cần.


7. Mảng bổ trợ giảng nhanh cho người mới

8. Từ điển lỗi cho người mới

Thông báo javac Nghĩa là gì Sửa sao
';' expected thiếu ; cuối lệnh thêm ;
cannot find symbol sai tên biến/lớp/phương thức kiểm tra chính tả, hoa thường
incompatible types nhét kiểu này vào biến kiểu khác ép kiểu hoặc đổi kiểu biến
array index out of bounds (lúc chạy) i <= length hoặc a[n] sửa thành i < length
NullPointerException (lúc chạy) gọi phương thức trên ô rỗng/null new object / kiểm tra null
ArithmeticException: / by zero (lúc chạy) chia 0 kiểm tra mẫu/số chia trước
class X is public, should be in X.java tên file khác tên lớp đổi tên file giống hệt lớp
non-static ... cannot be referenced gọi hàm object từ main static thêm static hoặc đúc object

Quy tắc đọc lỗi: đọc dòng đầu tiên, nó ghi đúng file và số dòng. Sửa từ lỗi đầu tiên trở đi, vì một lỗi hay đẻ ra thêm nhiều lỗi giả phía sau.