PART 2: THAO TÁC NÂNG CAO VÀ XỬ LÝ CHUỖI / NGÀY THÁNG (LAB 04)
Trong phần này, chúng ta sẽ chinh phục Bài 4: Quản lý Sinh viên (SV) & Danh sách Sinh viên (DSSV) từ đề Lab 04. Đây là bài học rất thực tế, giúp bạn làm quen với việc xử lý chuỗi tiếng Việt (String), làm việc với dữ liệu ngày tháng (Date) và quản lý danh sách bằng OOP.
🎓 BÀI 4: QUẢN LÝ SINH VIÊN (SV) VÀ DANH SÁCH SINH VIÊN (DSSV)
1. Trực quan hóa tư duy OOP: Quản lý và Xử lý Chuỗi
Lớp SV (Sinh viên đơn lẻ): Quản lý thông tin từng sinh viên (họ tên, ngày sinh, ĐTB). Lớp này chứa các "kỹ năng" xử lý chuỗi cá nhân: tự tách họ, tên đệm, tên chính từ một chuỗi họ tên đầy đủ.
Lớp DSSV (Danh sách sinh viên): Giống như một Sổ quản lý lớp học. Sổ này chứa một mảng các sinh viên và thực hiện các nghiệp vụ: thêm, xóa, tìm kiếm, sắp xếp danh sách sinh viên theo quy chuẩn tên tiếng Việt (Tên -> Họ -> Đệm).
2. Đọc hiểu Sơ đồ UML (Lab 04)
Lớp SV:
hoTen: String, ngaySinh: Date, dtb: double (các thuộc tính đóng gói).
layHo(), layTen(), layDem(): Tách phần họ, tên, đệm từ hoTen.
layTuoi(): Lấy năm hiện tại trừ cho năm sinh.
layXepLoai(): Giỏi (≥8), Khá (7≤ĐTB<8), Trung bình (5≤ĐTB<7), Yếu (<5).
Lớp DSSV:
ds: SV[]: Mảng lưu trữ danh sách sinh viên.
SOSV: int: Số lượng sinh viên hiện có trong danh sách.
them(SV), xoa(String), timTen(String), sapHoten(), sapTuoi(), sapDTB().
3. Phân tích & Cài đặt Mã nguồn Java Chi Tiết
Bước 3.1: Tạo lớp SV.java
import java.util.Calendar;
import java.util.Date;
public class SV {
// Thuộc tính private
private String hoTen;
private Date ngaySinh;
private double dtb;
// Constructor
public SV(String hoTen, Date ngaySinh, double dtb) {
this.hoTen = hoTen.trim(); // Loại bỏ khoảng trắng thừa 2 đầu
this.ngaySinh = ngaySinh;
this.dtb = dtb;
}
// Getter và Setter
public String layHoTen() { return hoTen; }
public void ganHoTen(String hoTen) { this.hoTen = hoTen.trim(); }
public Date layNgaySinh() { return ngaySinh; }
public void ganNgaySinh(Date ngaySinh) { this.ngaySinh = ngaySinh; }
public double layDTB() { return dtb; }
public void ganDTB(double dtb) { this.dtb = dtb; }
// 1. Tách TÊN (Từ cuối cùng trong chuỗi họ tên)
// Ví dụ: "Nguyen Van Nam" -> "Nam"
public String layTen() {
int idx = this.hoTen.lastIndexOf(' ');
if (idx == -1) return this.hoTen; // Nếu tên chỉ có 1 từ
return this.hoTen.substring(idx + 1);
}
// 2. Tách HỌ (Từ đầu tiên trong chuỗi họ tên)
// Ví dụ: "Nguyen Van Nam" -> "Nguyen"
public String layHo() {
int idx = this.hoTen.indexOf(' ');
if (idx == -1) return ""; // Nếu tên chỉ có 1 từ thì không có họ
return this.hoTen.substring(0, idx);
}
// 3. Tách ĐỆM (Các từ ở giữa họ và tên)
// Ví dụ: "Nguyen Van Nam" -> "Van"
public String layDem() {
int idxFirst = this.hoTen.indexOf(' ');
int idxLast = this.hoTen.lastIndexOf(' ');
if (idxFirst == -1 || idxFirst == idxLast) return ""; // Không có tên đệm
return this.hoTen.substring(idxFirst + 1, idxLast);
}
// 4. Tính tuổi: Năm hiện tại - Năm sinh
public int layTuoi() {
Calendar cal = Calendar.getInstance();
int namHienTai = cal.get(Calendar.YEAR);
Calendar calBirth = Calendar.getInstance();
calBirth.setTime(this.ngaySinh);
int namSinh = calBirth.get(Calendar.YEAR);
return namHienTai - namSinh;
}
// 5. Xếp loại học lực
public String layXepLoai() {
if (this.dtb >= 8.0) return "Giỏi";
if (this.dtb >= 7.0) return "Khá";
if (this.dtb >= 5.0) return "Trung bình";
return "Yếu";
}
// Chuẩn hóa họ tên (Ví dụ: "ngUYen vaN A" -> "Nguyen Van A")
public void chuanHoTen() {
String[] tu = this.hoTen.toLowerCase().split("\\s+");
StringBuilder sb = new StringBuilder();
for (String t : tu) {
if (t.length() > 0) {
sb.append(Character.toUpperCase(t.charAt(0)))
.append(t.substring(1)).append(" ");
}
}
this.hoTen = sb.toString().trim();
}
public void hienThi() {
System.out.printf("Họ tên: %-20s | Tuổi: %-2d | ĐTB: %-4.1f | Xếp loại: %-10s\n",
this.hoTen, layTuoi(), this.dtb, layXepLoai());
}
}
Bước 3.2: Tạo lớp DSSV.java (Quản lý mảng sinh viên)
import java.util.Calendar;
public class DSSV {
private SV[] ds;
private int SOSV; // Số lượng sinh viên hiện có
// Constructor với sức chứa tối đa kích thước max
public DSSV(int max) {
ds = new SV[max];
SOSV = 0;
}
// Thêm một sinh viên vào danh sách
public void them(SV sv) {
if (SOSV < ds.length) {
ds[SOSV] = sv;
SOSV++;
} else {
System.out.println("Danh sách đã đầy!");
}
}
public SV laySV(int i) {
if (i >= 0 && i < SOSV) return ds[i];
return null;
}
public void hienThi() {
for (int i = 0; i < SOSV; i++) {
ds[i].hienThi();
}
}
// Sắp xếp danh sách theo Họ Tên kiểu Việt Nam (Tên -> Họ -> Đệm)
public void sapHoten() {
for (int i = 0; i < SOSV - 1; i++) {
for (int j = i + 1; j < SOSV; j++) {
// Tạo chuỗi so sánh: Tên + Họ + Đệm theo hướng dẫn Lab 04
String s1 = ds[i].layTen() + " " + ds[i].layHo() + " " + ds[i].layDem();
String s2 = ds[j].layTen() + " " + ds[j].layHo() + " " + ds[j].layDem();
if (s1.compareToIgnoreCase(s2) > 0) {
SV temp = ds[i];
ds[i] = ds[j];
ds[j] = temp;
}
}
}
}
// Liệt kê các sinh viên có xếp loại được chỉ định
public void lietKeXepLoai(String xepLoai) {
System.out.println("\n--- DANH SÁCH SINH VIÊN XẾP LOẠI: " + xepLoai.toUpperCase() + " ---");
for (int i = 0; i < SOSV; i++) {
if (ds[i].layXepLoai().equalsIgnoreCase(xepLoai)) {
ds[i].hienThi();
}
}
}
// Tìm sinh viên theo Tên
public void timTen(String ten) {
System.out.println("\n--- TÌM SINH VIÊN TÊN: " + ten + " ---");
boolean timThay = false;
for (int i = 0; i < SOSV; i++) {
if (ds[i].layTen().equalsIgnoreCase(ten)) {
ds[i].hienThi();
timThay = true;
}
}
if (!timThay) System.out.println("Không tìm thấy sinh viên nào tên " + ten);
}
// Xóa sinh viên đầu tiên có tên trùng khớp
public void xoa(String ten) {
int idx = -1;
for (int i = 0; i < SOSV; i++) {
if (ds[i].layTen().equalsIgnoreCase(ten)) {
idx = i;
break;
}
}
if (idx != -1) {
// Dồn các phần tử sau idx lên 1 vị trí (hướng dẫn Lab 04)
for (int i = idx; i < SOSV - 1; i++) {
ds[i] = ds[i + 1];
}
ds[SOSV - 1] = null; // Tránh rác
SOSV--;
System.out.println("Đã xóa sinh viên đầu tiên tên: " + ten);
} else {
System.out.println("Không tìm thấy sinh viên tên " + ten + " để xóa.");
}
}
// Tính tuổi trung bình
public double tinhTuoiTrungBinh() {
if (SOSV == 0) return 0;
double tongTuoi = 0;
for (int i = 0; i < SOSV; i++) {
tongTuoi += ds[i].layTuoi();
}
return tongTuoi / SOSV;
}
// Tăng ĐTB lên 0.5 cho những SV sinh trước năm 1980
public void congDiemChoSVSinhTruoc1980() {
Calendar cal = Calendar.getInstance();
for (int i = 0; i < SOSV; i++) {
cal.setTime(ds[i].layNgaySinh());
if (cal.get(Calendar.YEAR) < 1980) {
double dtbMoi = Math.min(10.0, ds[i].layDTB() + 0.5);
ds[i].ganDTB(dtbMoi);
}
}
}
}
Bước 3.3: Viết chương trình chính MainSV.java (Lab 04)
import java.text.SimpleDateFormat;
public class MainSV {
public static void main(String[] args) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
// 1. Tạo danh sách gồm 5 sinh viên
DSSV dssv = new DSSV(10);
dssv.them(new SV("Nguyen Van Nam", sdf.parse("15/05/1979"), 8.5));
dssv.them(new SV("Tran Thi Hoa", sdf.parse("20/10/2002"), 7.2));
dssv.them(new SV("Le Van Hoa", sdf.parse("05/12/2001"), 6.5));
dssv.them(new SV("Pham Duc Anh", sdf.parse("10/01/1978"), 7.8));
dssv.them(new SV("Hoang Van Nam", sdf.parse("25/08/2003"), 4.5));
System.out.println("=== DANH SÁCH BAN ĐẦU ===");
dssv.hienThi();
// 2. Sắp xếp danh sách theo họ tên
System.out.println("\n=== DANH SÁCH SAU KHU SẮP XẾP THEO TÊN VIỆT NAM ===");
dssv.sapHoten();
dssv.hienThi();
// 3. Liệt kê sinh viên giỏi
dssv.lietKeXepLoai("Giỏi");
// 4. Tìm sinh viên tên Nam
dssv.timTen("Nam");
// 5. Xóa sinh viên đầu tiên tên Hòa
dssv.xoa("Hoa");
System.out.println("\n=== DANH SÁCH SAU KHU XÓA 'HOÀ' ===");
dssv.hienThi();
// 6. Tính tuổi trung bình
System.out.printf("\n=> Tuổi trung bình của danh sách: %.2f\n", dssv.tinhTuoiTrungBinh());
// 7. Cộng 0.5 điểm cho SV sinh trước năm 1980
dssv.congDiemChoSVSinhTruoc1980();
System.out.println("\n=== DANH SÁCH SAU KHI CỘNG 0.5 ĐIỂM CHO SV SINH TRƯỚC 1980 ===");
dssv.hienThi();
}
}
⚠️ Lỗi sai phổ biến người mới bắt đầu cần tránh!
Nhầm lẫn khi so sánh Chuỗi (== thay vì .equals() hoặc .compareTo()):
Mắc phải: Viết if (sv.layTen() == "Nam"). Trong Java, so sánh dấu == với String sẽ so sánh địa chỉ bộ nhớ chứ không phải nội dung chuỗi.
Khắc phục: Sử dụng .equalsIgnoreCase("Nam") hoặc .compareToIgnoreCase(...).
Lỗi tràn mảng khi xóa/thêm phần tử:
Khi xóa phần tử tại chỉ số k, phải dồn các phần tử sau lên 1 bước và giảm biến đếm SOSV đi 1. Nếu không sẽ để lại "ô trống dơ" (null) gây lỗi NullPointerException về sau.
📝 BÀI TẬP TỰ RÈN LỰC
Đề bài: Viết thêm phương thức public void sapTuoi() cho lớp DSSV để sắp xếp danh sách sinh viên giảm dần theo tuổi (người nhiều tuổi hơn đứng trước).
public void sapTuoi() {
for (int i = 0; i < SOSV - 1; i++) {
for (int j = i + 1; j < SOSV; j++) {
if (ds[i].layTuoi() < ds[j].layTuoi()) { // Giảm dần theo tuổi
SV temp = ds[i];
ds[i] = ds[j];
ds[j] = temp;
}
}
}
}
Bổ sung: hiểu sâu từng dòng (Bài 4)
Từ mới trong bài này (giải từ số 0):
- String: chuỗi ký tự. Trong Java, String là object nên so sánh nội dung phải dùng
.equals(), dùng==là so sánh địa chỉ — bug kinh điển. - indexOf(' ') / lastIndexOf(' '): vị trí dấu cách đầu tiên / cuối cùng. "Nguyen Van Nam": indexOf = 6, lastIndexOf = 10.
- substring(a, b): cắt chuỗi từ a đến trước b. Tên =
substring(10 + 1)= "Nam"; họ =substring(0, 6)= "Nguyen"; đệm =substring(7, 10)= "Van". - Date / Calendar / SimpleDateFormat: bộ ba ngày tháng — Date giữ mốc thời gian, Calendar bóc ra năm/tháng/ngày, SimpleDateFormat đọc chuỗi "15/05/1979" thành Date.
- compareToIgnoreCase: so sánh chuỗi bỏ qua hoa/thường, trả về số âm/dương/0 — dùng để sắp xếp tên.
- Mảng + SOSV: mảng
SV[]cố định sức chứa,SOSVđếm số ô đang dùng. Thêm = đặt vào ôSOSVrồi tăng đếm; xóa = dồn các ô sau lên một bước rồi giảm đếm.
Mổ xẻ sắp xếp tên kiểu Việt Nam:
String s1 = ds[i].layTen() + " " + ds[i].layHo() + " " + ds[i].layDem();
String s2 = ds[j].layTen() + " " + ds[j].layHo() + " " + ds[j].layDem();
if (s1.compareToIgnoreCase(s2) > 0) { /* đổi chỗ */ }
Muốn "Tên → Họ → Đệm" thì ghép khóa so sánh đúng theo thứ tự đó: "Nam Nguyen Van" so với "Anh Pham Duc" — chữ N đứng sau A nên đổi chỗ. Nổi bọt lặp lại cho cả mảng là xong.
Mổ xẻ xóa theo tên:
for (int i = idx; i < SOSV - 1; i++) ds[i] = ds[i + 1]; // dồn lên
ds[SOSV - 1] = null; // xóa dấu vết ở ô cuối
SOSV--; // giảm số lượng
Quên = null thì object cũ vẫn bị mảng giữ (rác), quên SOSV-- thì lần hiển thị sau in ra cả ô đã xóa.
Trace tay: 5 SV sắp theo tên → Anh (Pham Duc Anh) lên đầu, rồi Hoa (Le Van Hoa), Hoa (Tran Thi Hoa), Nam (Hoang Van Nam), Nam (Nguyen Van Nam) — đúng thứ tự Tên trước, trùng tên thì so Họ. Xóa "Hoa" lần đầu gặp → Le Van Hoa bay, Tran Thi Hoa ở lại. khớp output.
Tự kiểm tra:
- Tên chỉ một từ ("Madonna") thì
layTen/layHo/layDemtrả về gì?
Đáp án
layTen trả về cả chuỗi (vì không có dấu cách, idx = -1); layHo và layDem trả về chuỗi rỗng. Code đã chặn trường hợp này.- Vì sao
layTuoi()chỉ trừ năm mà không tính tháng ngày?
Đáp án
Đề bài quy định vậy cho đơn giản (tuổi = năm hiện tại − năm sinh). Muốn chính xác phải so thêm tháng/ngày.xoa("Hoa")xóa mấy người?
Đáp án
Một người đầu tiên tìm thấy (cóbreak). Muốn xóa hết phải lặp xóa đến khi không còn.Chúng ta đã vượt qua Part 2!