Bài tập OOP và cấu trúc tập hợp
Bộ bài này luyện lại nền tảng lớp, interface, lambda và các cấu trúc tập hợp trong Java. Tự làm trước, bí thì mở Gợi ý. Lời giải đầy đủ nằm trong từng bài học tương ứng.
Bài 1: lớp và đối tượng có đóng gói
Viết lớp TaiKhoan với thuộc tính riêng soDu, một phương thức napTien(double) và một phương thức rutTien(double) không cho phép rút quá số dư. Không để mã bên ngoài chạm thẳng vào soDu.
Gợi ý
Để soDu là private, dồn mọi thay đổi qua phương thức để kiểm soát bất biến. Trạng thái không hợp lệ (rút quá số dư) bị chặn ngay trong lớp.
public class TaiKhoan {
private double soDu; // encapsulated state
public void napTien(double tien) {
if (tien > 0) soDu += tien;
}
public boolean rutTien(double tien) {
if (tien > 0 && tien <= soDu) {
soDu -= tien;
return true;
}
return false; // reject invalid withdrawal
}
public double getSoDu() {
return soDu;
}
}
Xem lại bài Lớp và đối tượng.
Bài 2: interface và nhiều cài đặt
Định nghĩa interface HinhPhang có phương thức double dienTich(). Viết hai lớp HinhTron và HinhChuNhat cài đặt interface này, rồi tính tổng diện tích của một danh sách HinhPhang.
Gợi ý
Interface là hợp đồng: khai báo dienTich() mà không nói cách tính. Mỗi lớp tự lo phần thân, còn mã dùng chung chỉ cần biết kiểu HinhPhang.
public interface HinhPhang {
double dienTich();
}
// class HinhTron implements HinhPhang { ... return Math.PI * r * r; }
// class HinhChuNhat implements HinhPhang { ... return dai * rong; }
class TinhTong {
public static double tong(java.util.List<HinhPhang> ds) {
double s = 0;
for (HinhPhang h : ds) s += h.dienTich(); // polymorphic call
return s;
}
}
Xem lại bài Giao tiếp (interface).
Bài 3: lớp ẩn danh và lambda làm listener
Cho interface hàm đơn SuKien với phương thức void xuLy(String thongTin). Đăng ký một listener bằng lớp ẩn danh, rồi viết lại đúng listener đó bằng biểu thức lambda cho gọn.
Gợi ý
Interface chỉ có một phương thức trừu tượng thì lambda thay được cho lớp ẩn danh. Cả hai đều giữ tham chiếu ngầm tới ngữ cảnh bao quanh, nhưng lambda ngắn hơn và không sinh lớp phụ trên đĩa.
interface SuKien {
void xuLy(String thongTin);
}
public class Demo {
public static void main(String[] args) {
// anonymous class version
SuKien a = new SuKien() {
@Override
public void xuLy(String thongTin) {
System.out.println("nghe: " + thongTin);
}
};
// equivalent lambda
SuKien b = thongTin -> System.out.println("nghe: " + thongTin);
a.xuLy("mo");
b.xuLy("dong");
}
}
Xem lại bài Lớp ẩn danh.
Bài 4: từ mảng sang ArrayList
Cho một mảng int[] các điểm số. Chuyển dữ liệu sang ArrayList<Integer>, thêm một điểm mới vào cuối, rồi in kích thước danh sách. Giải thích vì sao ArrayList thêm phần tử được còn mảng thì không.
Gợi ý
Mảng có độ dài cố định lúc tạo. ArrayList bọc một mảng nội bộ và khi đầy thì cấp mảng lớn hơn (thường khoảng 1.5 lần) rồi chép sang, nên nhìn bên ngoài giống như co giãn được.
import java.util.ArrayList;
import java.util.List;
public class ChuyenMang {
public static void main(String[] args) {
int[] diem = {8, 6, 9};
List<Integer> ds = new ArrayList<>();
for (int d : diem) ds.add(d); // autoboxing int to Integer
ds.add(7); // array cannot grow, ArrayList can
System.out.println(ds.size()); // 4
}
}
Xem lại bài Mảng trong Java.
Bài 5: đếm tần suất từ bằng Map
Viết chương trình nhận một câu, tách thành các từ theo khoảng trắng, rồi dùng HashMap<String, Integer> đếm số lần xuất hiện của mỗi từ và in kết quả.
Gợi ý
Map không kế thừa Collection; nó lưu cặp khoá và giá trị. Dùng getOrDefault để cộng dồn gọn gàng. Khoá String bất biến nên rất an toàn khi làm khoá.
import java.util.HashMap;
import java.util.Map;
public class DemTu {
public static void main(String[] args) {
String cau = "mua roi mua tan mua";
Map<String, Integer> dem = new HashMap<>();
for (String tu : cau.split(" ")) {
dem.put(tu, dem.getOrDefault(tu, 0) + 1);
}
for (Map.Entry<String, Integer> e : dem.entrySet()) {
System.out.println(e.getKey() + " = " + e.getValue());
}
}
}
Xem lại bài Set và Map.
Bài 6: khử trùng lặp bằng Set
Cho một List<String> có thể chứa phần tử lặp. Loại bỏ trùng lặp mà vẫn giữ thứ tự xuất hiện lần đầu. Cho biết chọn cài đặt Set nào để giữ thứ tự.
Gợi ý
HashSet không giữ thứ tự, còn LinkedHashSet giữ đúng thứ tự chèn. Đưa cả danh sách vào LinkedHashSet là loại trùng mà vẫn nhớ thứ tự đầu tiên.
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
public class KhuTrung {
public static List<String> locTrung(List<String> nguon) {
// LinkedHashSet keeps first-seen order and drops duplicates
return new ArrayList<>(new LinkedHashSet<>(nguon));
}
}
Xem lại bài Set và Map.
Bài 7: equals và hashCode để làm khoá HashMap
Viết lớp ToaDo gồm x và y, cài equals và hashCode nhất quán để hai điểm cùng toạ độ được coi là một khoá trong HashMap. Kiểm chứng bằng cách tra cứu với một đối tượng ToaDo mới tạo cùng giá trị.
Gợi ý
equals và hashCode phải nhất quán: hai đối tượng bằng nhau bắt buộc có cùng mã băm. Chỉ tính chúng từ các trường bất biến để khoá không đổi băm sau khi đã nằm trong map. Dùng Objects.hash cho gọn.
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
public final class ToaDo {
private final int x, y; // immutable fields for a stable key
public ToaDo(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof ToaDo)) return false;
ToaDo t = (ToaDo) o;
return x == t.x && y == t.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y); // consistent with equals
}
public static void main(String[] args) {
Map<ToaDo, String> ten = new HashMap<>();
ten.put(new ToaDo(1, 2), "A");
System.out.println(ten.get(new ToaDo(1, 2))); // A
}
}
Xem lại bài Set và Map.
Bài 8: TreeMap giữ khoá theo thứ tự
Cho vài cặp tên và điểm, đưa vào một Map sao cho khi in ra các khoá tự sắp theo thứ tự chữ cái. Cho biết cài đặt Map nào làm được điều đó mà không cần sắp thủ công.
Gợi ý
HashMap không hứa thứ tự, còn TreeMap luôn giữ khoá theo thứ tự tự nhiên (hoặc theo Comparator bạn đưa vào). Mỗi lần chèn tốn O(log n) nhưng đổi lại duyệt ra đã sắp sẵn.
import java.util.Map;
import java.util.TreeMap;
public class TuDienSapXep {
public static void main(String[] args) {
// TreeMap keeps keys in natural sorted order
Map<String, Integer> diem = new TreeMap<>();
diem.put("cuong", 7);
diem.put("an", 9);
diem.put("binh", 8);
System.out.println(diem); // {an=9, binh=8, cuong=7}
}
}
Xem lại bài Set và Map.
Bài 9: so sánh HashSet và TreeSet
Đưa cùng một dãy số (có phần tử lặp) vào cả HashSet lẫn TreeSet. In ra để thấy cả hai đều loại trùng, nhưng chỉ một trong hai giữ thứ tự sắp xếp. Giải thích khác biệt.
Gợi ý
HashSet nhanh, thao tác trung bình O(1), nhưng không hứa thứ tự nào. TreeSet sắp phần tử theo thứ tự tự nhiên, thao tác O(log n). Cả hai đều bỏ phần tử trùng.
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
public class SoSanhSet {
public static void main(String[] args) {
// HashSet: fast, no ordering guarantee
Set<Integer> nhanh = new HashSet<>();
// TreeSet: sorted, O(log n) per operation
Set<Integer> sapXep = new TreeSet<>();
for (int x : new int[]{5, 1, 3, 1}) {
nhanh.add(x);
sapXep.add(x);
}
System.out.println(sapXep); // [1, 3, 5] sorted, no duplicate
System.out.println(nhanh.size()); // 3
}
}
Xem lại bài Set và Map.
Bài 10: dùng record làm khoá map
Cần một khoá gồm cặp x và y cho HashMap. Thay vì tự viết lớp có equals và hashCode, hãy dùng record. Kiểm chứng rằng một record mới tạo cùng giá trị vẫn tra ra đúng khoá cũ.
Gợi ý
record tự sinh equals và hashCode dựa trên tất cả thành phần, lại bất biến, nên là khoá map lý tưởng, khỏi viết tay như lớp thường.
import java.util.HashMap;
import java.util.Map;
public class KhoaBangRecord {
// record auto-generates equals and hashCode, ideal as a map key
record ToaDo(int x, int y) {}
public static void main(String[] args) {
Map<ToaDo, String> ten = new HashMap<>();
ten.put(new ToaDo(1, 2), "A");
// a fresh record with the same values matches the stored key
System.out.println(ten.get(new ToaDo(1, 2))); // A
}
}
Xem lại bài Set và Map.
Bài 11: chuyển mảng sang List và bẫy Arrays.asList
Chuyển một mảng String sang List bằng Arrays.asList, rồi thử thêm một phần tử. Giải thích vì sao thao tác thêm ném lỗi, và cách lấy một danh sách co giãn được từ mảng.
Gợi ý
Arrays.asList trả về một danh sách kích thước cố định gắn thẳng vào mảng gốc, nên add ném UnsupportedOperationException. Muốn danh sách thêm bớt được thì bọc nó trong new ArrayList để sao ra bản riêng.
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class BayAsList {
public static void main(String[] args) {
String[] mang = {"an", "binh", "cuong"};
// Arrays.asList returns a fixed-size list backed by the array
List<String> coDinh = Arrays.asList(mang);
// coDinh.add("x"); // would throw UnsupportedOperationException
// wrap in a new ArrayList to get a resizable copy
List<String> mem = new ArrayList<>(coDinh);
mem.add("dung");
System.out.println(mem.size()); // 4
}
}
Xem lại bài Tổng quan Collections Framework.
Bài 12: xoá phần tử an toàn khi đang duyệt
Cho một List số nguyên, xoá mọi số chẵn ngay trong lúc duyệt. Làm sao xoá được mà không ném ConcurrentModificationException.
Gợi ý
Đừng gọi list.remove trong vòng for-each vì cơ chế fail-fast sẽ ném lỗi. Muốn xoá trong lúc duyệt, hãy dùng Iterator rồi gọi it.remove(), hoặc gọn hơn là ds.removeIf(dieuKien).
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class XoaAnToan {
public static void main(String[] args) {
List<Integer> ds = new ArrayList<>(List.of(1, 2, 3, 4, 5));
Iterator<Integer> it = ds.iterator();
while (it.hasNext()) {
// it.remove() safely deletes the element just returned by next()
if (it.next() % 2 == 0) it.remove();
}
System.out.println(ds); // [1, 3, 5]
// removeIf is the shorter equivalent, safe for the same reason
List<Integer> ds2 = new ArrayList<>(List.of(1, 2, 3, 4, 5));
ds2.removeIf(x -> x % 2 == 0);
System.out.println(ds2); // [1, 3, 5]
}
}
Xem lại bài List và Queue.
Câu hỏi tự kiểm
- 1Quan hệ giữa Map và Collection trong Java là gì?
- 2Vì sao đối tượng dùng làm khoá HashMap nên bất biến và cài equals/hashCode nhất quán?
- 3Sửa cấu trúc một ArrayList (thêm hoặc xoá phần tử) ngay khi đang duyệt bằng vòng for-each thường dẫn tới điều gì?
- 4Một file .java được phép chứa bao nhiêu kiểu public cấp cao nhất?