Bài tập đa kiểu và đa luồng
Mười hai bài luyện bám Chương 2 và Chương 4: lambda, tham chiếu phương thức, Stream, generics, Comparator, rồi tới luồng, đồng bộ và ExecutorService. Hãy tự viết mã trước, chỉ mở gợi ý khi bí.
Bài 1: lambda và functional interface
Định nghĩa một functional interface PhepTinh với đúng một phương thức trừu tượng int ap(int a, int b). Dùng biểu thức lambda tạo ba phép: cộng, trừ, nhân, rồi in kết quả với a = 6, b = 7.
Gợi ý
Interface chỉ có một phương thức trừu tượng thì lambda gán được thẳng vào nó. Chú thích @FunctionalInterface giúp trình biên dịch chặn nếu ai thêm phương thức thứ hai.
public class LambdaDemo {
// A functional interface has exactly one abstract method
@FunctionalInterface
interface PhepTinh {
int ap(int a, int b);
}
public static void main(String[] args) {
PhepTinh cong = (a, b) -> a + b;
PhepTinh tru = (a, b) -> a - b;
PhepTinh nhan = (a, b) -> a * b;
System.out.println(cong.ap(6, 7)); // 13
System.out.println(tru.ap(6, 7)); // -1
System.out.println(nhan.ap(6, 7)); // 42
}
}
Xem lại bài Biểu thức Lambda.
Bài 2: tham chiếu phương thức
Cho một danh sách tên viết thường. Chuyển tất cả sang chữ hoa rồi in, nhưng thay vì viết lambda s -> s.toUpperCase() hãy dùng tham chiếu phương thức. In bằng System.out::println.
Gợi ý
String::toUpperCase là tham chiếu tới phương thức thể hiện của một đối tượng bất kỳ: phần tử stream chính là đối tượng nhận lời gọi. Nó ngắn gọn và tương đương lambda một dòng.
import java.util.List;
public class MethodRefDemo {
public static void main(String[] args) {
List<String> ten = List.of("an", "binh", "cuong");
// Instance method reference on the stream element
ten.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
}
}
Xem lại bài Lập trình chức năng.
Bài 3: gom nhóm bằng Stream
Cho danh sách sinh viên gồm tên và lớp. Dùng Stream gom sinh viên theo lớp và đếm số lượng mỗi lớp, trả về một Map từ tên lớp sang số lượng. Gợi ý: kết hợp groupingBy với counting.
Gợi ý
Collectors.groupingBy chia phần tử theo một khóa phân loại; truyền thêm collector con counting() để mỗi nhóm cho ra số lượng thay vì danh sách. Stream lười và chỉ chạy khi gặp thao tác kết thúc như collect.
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class GroupingDemo {
record SinhVien(String ten, String lop) {}
public static void main(String[] args) {
List<SinhVien> ds = List.of(
new SinhVien("An", "K1"),
new SinhVien("Binh", "K1"),
new SinhVien("Cuong", "K2"));
// Group and count in one pass
Map<String, Long> theoLop = ds.stream()
.collect(Collectors.groupingBy(SinhVien::lop, Collectors.counting()));
System.out.println(theoLop); // {K1=2, K2=1}
}
}
Xem lại bài Streams API.
Bài 4: generics với giới hạn kiểu
Viết một phương thức generic lonNhat tìm phần tử lớn nhất trong một danh sách. Ràng buộc kiểu sao cho mọi phần tử đều so sánh được, để hàm chạy với cả List số nguyên lẫn List chuỗi.
Gợi ý
Đặt giới hạn trên <T extends Comparable<T>> để trình biên dịch bảo đảm mọi T có compareTo. Nhờ vậy một định nghĩa phục vụ nhiều kiểu mà vẫn an toàn kiểu.
import java.util.List;
public class BoundedDemo {
// Bounded type: every T is guaranteed to be comparable
static <T extends Comparable<T>> T lonNhat(List<T> ds) {
T max = ds.get(0);
for (T x : ds) {
if (x.compareTo(max) > 0) max = x;
}
return max;
}
public static void main(String[] args) {
System.out.println(lonNhat(List.of(3, 9, 4))); // 9
System.out.println(lonNhat(List.of("an", "cuong"))); // cuong
}
}
Xem lại bài Lập trình tổng quát (generics).
Bài 5: Comparator với reversed và thenComparing
Sắp một danh sách nhân viên theo tuổi giảm dần, khi trùng tuổi thì theo tên tăng dần. Dựng Comparator bằng comparingInt, reversed và thenComparing, chú ý thứ tự gọi để chỉ tuổi bị đảo còn tên vẫn tăng.
Gợi ý
reversed() đảo toàn bộ comparator dựng tới điểm gọi, nên hãy gọi reversed ngay sau tiêu chí tuổi rồi mới thenComparing theo tên. Như vậy tên được thêm sau khi đảo và vẫn tăng dần.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class SortDemo {
record NhanVien(String ten, int tuoi) {}
public static void main(String[] args) {
List<NhanVien> ds = new ArrayList<>(List.of(
new NhanVien("An", 30),
new NhanVien("Binh", 30),
new NhanVien("Cuong", 25)));
// Age descending, then name ascending as tie-breaker
Comparator<NhanVien> c = Comparator
.comparingInt(NhanVien::tuoi).reversed()
.thenComparing(NhanVien::ten);
ds.sort(c);
System.out.println(ds); // An 30, Binh 30, Cuong 25
}
}
Xem lại bài Lập trình chức năng.
Bài 6: đếm an toàn đa luồng
Cho hai luồng cùng tăng một biến đếm mỗi luồng một nghìn lần. Nếu dùng count++ trên int thường thì kết quả có thể thiếu. Hãy dùng AtomicInteger để tổng cuối luôn bằng hai nghìn, và chờ cả hai luồng xong bằng join.
Gợi ý
count++ gồm đọc, tăng, ghi nên không nguyên tử; hai luồng xen kẽ sẽ làm mất số đếm. AtomicInteger.incrementAndGet gộp cả ba bước thành một thao tác nguyên tử. Nhớ join để luồng chính đọc kết quả sau khi mọi luồng con hoàn tất.
import java.util.concurrent.atomic.AtomicInteger;
public class CounterDemo {
public static void main(String[] args) throws InterruptedException {
AtomicInteger dem = new AtomicInteger(0);
Runnable job = () -> {
for (int i = 0; i < 1000; i++) {
dem.incrementAndGet(); // atomic read-modify-write
}
};
Thread t1 = new Thread(job);
Thread t2 = new Thread(job);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(dem.get()); // always 2000
}
}
Xem lại bài Đồng bộ hóa.
Bài 7: ExecutorService và Future
Thay vì tự tạo Thread, dùng một pool cố định hai luồng để chạy một tác vụ trả về giá trị. Nộp một Callable tính 6 * 7, lấy kết quả qua Future.get, rồi đóng pool bằng shutdown.
Gợi ý
ExecutorService quản lý vòng đời luồng thay bạn. submit một Callable trả về ngay một Future; get chặn tới khi tác vụ xong rồi trả giá trị. Luôn shutdown để chương trình không treo vì luồng pool còn sống.
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class ExecutorDemo {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
// Callable returns a value; Future carries the result
Future<Integer> kq = pool.submit(() -> 6 * 7);
System.out.println(kq.get()); // 42, blocks until done
pool.shutdown();
}
}
Xem lại bài java.util.concurrent.
Bài 8: reduce để tính tổng và tích
Cho một List số nguyên. Dùng Stream.reduce tính tổng và tích của cả danh sách trong hai câu riêng, không dùng vòng lặp thủ công.
Gợi ý
reduce gấp cả stream về một giá trị: đưa vào một giá trị khởi tạo (identity) và một hàm gộp hai phần tử. Tổng dùng identity 0, tích dùng identity 1.
import java.util.List;
public class ReduceDemo {
public static void main(String[] args) {
List<Integer> so = List.of(1, 2, 3, 4);
// reduce folds the stream into a single value
int tong = so.stream().reduce(0, Integer::sum);
int tich = so.stream().reduce(1, (a, b) -> a * b);
System.out.println(tong); // 10
System.out.println(tich); // 24
}
}
Xem lại bài Streams API.
Bài 9: flatMap làm phẳng danh sách lồng
Cho một danh sách chứa nhiều danh sách con các số. Dùng flatMap để trải phẳng thành một danh sách phẳng duy nhất theo đúng thứ tự.
Gợi ý
map giữ nguyên số phần tử, còn flatMap biến mỗi phần tử thành một stream rồi nối tất cả lại, nên một stream các danh sách trở thành một stream phẳng của mọi phần tử.
import java.util.List;
public class FlatMapDemo {
public static void main(String[] args) {
List<List<Integer>> nguon = List.of(
List.of(1, 2), List.of(3, 4), List.of(5));
// flatMap turns a stream of lists into one flat stream
List<Integer> phang = nguon.stream()
.flatMap(List::stream)
.toList();
System.out.println(phang); // [1, 2, 3, 4, 5]
}
}
Xem lại bài Streams API.
Bài 10: Optional với map và orElseGet
Cho một Optional chứa tên (có thể rỗng). Nếu có tên thì lấy độ dài, nếu rỗng thì trả về 0. Với một Optional rỗng khác, trả về một chuỗi mặc định. Dùng map và orElseGet.
Gợi ý
map chỉ biến đổi khi có giá trị, ngược lại giữ nguyên rỗng. orElse luôn dựng sẵn giá trị dự phòng, còn orElseGet chỉ gọi hàm khi thật sự rỗng, nên tránh tính toán thừa.
import java.util.Optional;
public class OptionalDemo {
public static void main(String[] args) {
Optional<String> ten = Optional.of("an");
// map transforms the value only when present
int doDai = ten.map(String::length).orElse(0);
System.out.println(doDai); // 2
Optional<String> rong = Optional.empty();
// orElseGet supplies a fallback lazily, only when empty
String ketQua = rong.orElseGet(() -> "khong co");
System.out.println(ketQua); // khong co
}
}
Xem lại bài Optional và xử lý ngoại lệ.
Bài 11: CompletableFuture chạy nền và nối tiếp
Chạy một tác vụ nền trả về số 6, rồi khi có kết quả thì nhân với 7, cuối cùng in ra 42. Dùng CompletableFuture với supplyAsync và thenApply.
Gợi ý
supplyAsync chạy tác vụ trên một luồng nền và trả về ngay một CompletableFuture. thenApply gắn một bước biến đổi chạy khi kết quả sẵn sàng, không chặn luồng chính cho tới khi gọi get.
import java.util.concurrent.CompletableFuture;
public class FutureChainDemo {
public static void main(String[] args) throws Exception {
// supplyAsync runs the task on a background thread
CompletableFuture<Integer> f = CompletableFuture
.supplyAsync(() -> 6)
.thenApply(x -> x * 7); // transform the result when ready
System.out.println(f.get()); // 42
}
}
Xem lại bài java.util.concurrent.
Bài 12: ghép điều kiện bằng Predicate.and và or
Dựng hai Predicate cho số: một kiểm tra dương, một kiểm tra chẵn. Ghép chúng bằng and để lọc số vừa dương vừa chẵn, và bằng or cho trường hợp thoả một trong hai.
Gợi ý
Predicate là hàm trả về boolean. and và or trả về một Predicate mới gộp điều kiện mà không đổi hai cái gốc, giúp ghép logic gọn gàng và tái dùng được.
import java.util.function.Predicate;
public class PredicateDemo {
public static void main(String[] args) {
Predicate<Integer> duong = x -> x > 0;
Predicate<Integer> chan = x -> x % 2 == 0;
// combine predicates with and / or
Predicate<Integer> duongVaChan = duong.and(chan);
System.out.println(duongVaChan.test(4)); // true
System.out.println(duongVaChan.test(-4)); // false
System.out.println(duong.or(chan).test(-2)); // true
}
}
Xem lại bài Lập trình chức năng.
Câu hỏi tự kiểm
- 1Comparator.comparing(A::x).thenComparing(A::y).reversed() sắp xếp thế nào?
- 2Nhiều luồng cùng chạy count++ trên một biến int volatile chung thì kết quả ra sao?
- 3Sau khi đã gọi collect trên một Stream, gọi tiếp forEach trên chính stream đó thì?
- 4Với khai báo List<? extends Number> ds, câu lệnh nào hợp lệ?