Đây là mini-challenge khép lại Module 6. Bạn sẽ thiết kế một sở thú nhỏ với 4 loài động vật: Lion, Elephant, Penguin, Snake. Mỗi loài có dữ liệu riêng (cân nặng, tuổi, đặc trưng) và cách tính khẩu phần ăn khác nhau. Yêu cầu dùng sealed interface + record + switch pattern matching — công cụ mới của Java 21 để làm ADT (algebraic data type) đầy đủ.
Mục tiêu không chỉ "chạy được" — mà làm cho compiler giúp bạn: thêm loài mới → compiler tự bắt mọi switch chưa update; không bao giờ miss case.
🎯 Đề bài
Thiết kế domain sở thú với yêu cầu:
1. sealed interface Animal
permits Lion, Elephant, Penguin, Snake.- Có abstract method
String name()— tên đặt cho cá thể. - Có default method
String description()trả "Unknown animal" — mặc định, mỗi loài override khi cần.
2. 4 record implement Animal
record Lion(String name, double weightKg, int age, boolean isMale)— sư tử.record Elephant(String name, double weightKg, int age, double tuskLengthCm)— voi với độ dài ngà.record Penguin(String name, double weightKg, int age, int colonyId)— chim cánh cụt thuộc đàn ID.record Snake(String name, double weightKg, int age, boolean isVenomous)— rắn có/không độc.
3. class Zoo quản lý animal
- Field
private final List<Animal> animals. - Constructor khởi tạo list rỗng.
void addAnimal(Animal a)— thêm vào list, throw nếunull.int totalAnimals(),double totalWeight().List<Animal> olderThan(int minAge)— trả snapshot immutable, filter theo tuổi.Map<String, Long> countBySpecies()— đếm mỗi loài (key là tên loài: "Lion", "Elephant"...).
4. Utility static method
-
static double dailyFoodKg(Animal a)— khẩu phần ăn hàng ngày (kg):- Lion:
weight * 0.05(5% cân nặng). - Elephant:
weight * 0.02 + tuskLength * 0.1(2% + bonus cho ngà lớn). - Penguin:
weight * 0.1(10%). - Snake:
weight * 0.01nếu không độc,weight * 0.015nếu độc.
- Lion:
-
Dùng switch pattern matching không có
default— để compiler đảm bảo exhaustive. -
static String describeAnimal(Animal a)— mô tả 1 dòng kèm thông tin riêng của loài, cũng dùng switch pattern matching + deconstruction.
Output mẫu:
Added Lion(Simba, 180.0kg, 5yo, male)
Added Elephant(Dumbo, 3500.0kg, 10yo, tusk=80cm)
Added Penguin(Pingu, 15.0kg, 3yo, colony=42)
Added Snake(Slither, 4.0kg, 2yo, venomous)
Zoo: 4 animals, total weight 3699.0kg
Daily food:
Simba: 9.00 kg
Dumbo: 78.00 kg
Pingu: 1.50 kg
Slither: 0.06 kg
Older than 4 years old:
Lion Simba (5 yo)
Elephant Dumbo (10 yo)
Species count: {Lion=1, Elephant=1, Penguin=1, Snake=1}
📦 Concept dùng trong bài
| Concept | Bài | Dùng ở đây |
|---|---|---|
| extends / implements | Module 6, bài 1 | Record implements Animal |
| Override + dynamic dispatch | Module 6, bài 2 | name() override mỗi record |
| Interface + default method | Module 6, bài 4 | description() default trong interface |
| Sealed class | Module 6, bài 5 | sealed interface Animal permits ... |
| Composition over inheritance | Module 6, bài 6 | Zoo chứa list animal, không extends |
| Record + compact constructor | Module 5, bài 6 | Validate cân nặng, tuổi dương |
| Switch pattern matching | Module 3, bài 2 | dailyFoodKg, describeAnimal |
| Stream API | (preview) | filter, collect, groupingBy |
▶️ Starter code
import java.util.*;
import java.util.stream.Collectors;
public class ZooApp {
// TODO: sealed interface Animal permits Lion, Elephant, Penguin, Snake
// voi abstract String name() va default String description()
// TODO: 4 record implement Animal voi compact constructor validate
// weightKg > 0, age >= 0
// TODO: class Zoo voi field private final List<Animal> animals
public static void main(String[] args) {
Zoo zoo = new Zoo();
Lion simba = new Lion("Simba", 180.0, 5, true);
Elephant dumbo = new Elephant("Dumbo", 3500.0, 10, 80);
Penguin pingu = new Penguin("Pingu", 15.0, 3, 42);
Snake slither = new Snake("Slither", 4.0, 2, true);
zoo.addAnimal(simba);
zoo.addAnimal(dumbo);
zoo.addAnimal(pingu);
zoo.addAnimal(slither);
System.out.println("Added 4 animals");
System.out.printf("Zoo: %d animals, total weight %.1fkg%n",
zoo.totalAnimals(), zoo.totalWeight());
System.out.println("Daily food:");
zoo.animals().forEach(a ->
System.out.printf(" %s: %.2f kg%n", a.name(), dailyFoodKg(a)));
System.out.println("Older than 4 years old:");
zoo.olderThan(4).forEach(a -> System.out.println(" " + describeAnimal(a)));
System.out.println("Species count: " + zoo.countBySpecies());
}
// TODO: static double dailyFoodKg(Animal a) — switch pattern matching
// TODO: static String describeAnimal(Animal a) — switch pattern matching with deconstruction
}
javac --enable-preview --release 21 ZooApp.java
java --enable-preview ZooApp
Java 21+ không cần --enable-preview cho switch pattern matching — đã final.
Dành 25–30 phút tự làm. Gợi ý phía dưới.
💡 Gợi ý
💡 💡 Gợi ý — đọc khi bị kẹt
Sealed interface với default method:
public sealed interface Animal permits Lion, Elephant, Penguin, Snake {
String name();
default String description() {
return "Unknown animal";
}
}
Record với compact constructor validate:
public record Lion(String name, double weightKg, int age, boolean isMale) implements Animal {
public Lion {
if (name == null || name.isBlank())
throw new IllegalArgumentException("name must not be blank");
if (weightKg <= 0)
throw new IllegalArgumentException("weightKg must be positive");
if (age < 0)
throw new IllegalArgumentException("age must be non-negative");
}
}
// lap lai tuong tu cho Elephant, Penguin, Snake
Zoo class:
public class Zoo {
private final List<Animal> animals = new ArrayList<>();
public void addAnimal(Animal a) {
Objects.requireNonNull(a);
animals.add(a);
}
public int totalAnimals() { return animals.size(); }
public double totalWeight() {
return animals.stream()
.mapToDouble(a -> switch (a) {
case Lion l -> l.weightKg();
case Elephant e -> e.weightKg();
case Penguin p -> p.weightKg();
case Snake s -> s.weightKg();
})
.sum();
}
public List<Animal> animals() {
return List.copyOf(animals); // immutable snapshot
}
public List<Animal> olderThan(int minAge) {
return animals.stream()
.filter(a -> switch (a) {
case Lion l -> l.age() > minAge;
case Elephant e -> e.age() > minAge;
case Penguin p -> p.age() > minAge;
case Snake s -> s.age() > minAge;
})
.toList();
}
public Map<String, Long> countBySpecies() {
return animals.stream()
.collect(Collectors.groupingBy(
a -> a.getClass().getSimpleName(),
Collectors.counting()
));
}
}
dailyFoodKg — exhaustive switch:
static double dailyFoodKg(Animal a) {
return switch (a) {
case Lion l -> l.weightKg() * 0.05;
case Elephant e -> e.weightKg() * 0.02 + e.tuskLengthCm() * 0.1;
case Penguin p -> p.weightKg() * 0.1;
case Snake s -> s.isVenomous() ? s.weightKg() * 0.015 : s.weightKg() * 0.01;
};
}
describeAnimal với deconstruction:
static String describeAnimal(Animal a) {
return switch (a) {
case Lion l -> "Lion " + l.name() + " (" + l.age() + " yo)";
case Elephant e -> "Elephant " + e.name() + " (" + e.age() + " yo, tusk " + e.tuskLengthCm() + "cm)";
case Penguin p -> "Penguin " + p.name() + " from colony " + p.colonyId();
case Snake s -> "Snake " + s.name() + (s.isVenomous() ? " (venomous)" : " (harmless)");
};
}
✅ Lời giải
ℹ️ ✅ Lời giải — xem sau khi đã thử
import java.util.*;
import java.util.stream.Collectors;
public class ZooApp {
public sealed interface Animal permits Lion, Elephant, Penguin, Snake {
String name();
default String description() {
return "Unknown animal";
}
}
public record Lion(String name, double weightKg, int age, boolean isMale) implements Animal {
public Lion {
validateCommon(name, weightKg, age);
}
}
public record Elephant(String name, double weightKg, int age, double tuskLengthCm) implements Animal {
public Elephant {
validateCommon(name, weightKg, age);
if (tuskLengthCm < 0)
throw new IllegalArgumentException("tuskLengthCm must be non-negative");
}
}
public record Penguin(String name, double weightKg, int age, int colonyId) implements Animal {
public Penguin {
validateCommon(name, weightKg, age);
if (colonyId < 0)
throw new IllegalArgumentException("colonyId must be non-negative");
}
}
public record Snake(String name, double weightKg, int age, boolean isVenomous) implements Animal {
public Snake {
validateCommon(name, weightKg, age);
}
}
private static void validateCommon(String name, double weightKg, int age) {
if (name == null || name.isBlank())
throw new IllegalArgumentException("name must not be blank");
if (weightKg <= 0)
throw new IllegalArgumentException("weightKg must be positive, got " + weightKg);
if (age < 0)
throw new IllegalArgumentException("age must be non-negative, got " + age);
}
public static class Zoo {
private final List<Animal> animals = new ArrayList<>();
public void addAnimal(Animal a) {
Objects.requireNonNull(a, "animal must not be null");
animals.add(a);
}
public int totalAnimals() {
return animals.size();
}
public double totalWeight() {
return animals.stream()
.mapToDouble(a -> switch (a) {
case Lion l -> l.weightKg();
case Elephant e -> e.weightKg();
case Penguin p -> p.weightKg();
case Snake s -> s.weightKg();
})
.sum();
}
public List<Animal> animals() {
return List.copyOf(animals);
}
public List<Animal> olderThan(int minAge) {
return animals.stream()
.filter(a -> switch (a) {
case Lion l -> l.age() > minAge;
case Elephant e -> e.age() > minAge;
case Penguin p -> p.age() > minAge;
case Snake s -> s.age() > minAge;
})
.toList();
}
public Map<String, Long> countBySpecies() {
return animals.stream()
.collect(Collectors.groupingBy(
a -> a.getClass().getSimpleName(),
Collectors.counting()
));
}
}
public static double dailyFoodKg(Animal a) {
return switch (a) {
case Lion l -> l.weightKg() * 0.05;
case Elephant e -> e.weightKg() * 0.02 + e.tuskLengthCm() * 0.1;
case Penguin p -> p.weightKg() * 0.1;
case Snake s -> s.isVenomous() ? s.weightKg() * 0.015 : s.weightKg() * 0.01;
};
}
public static String describeAnimal(Animal a) {
return switch (a) {
case Lion l -> String.format("Lion %s (%d yo)", l.name(), l.age());
case Elephant e -> String.format("Elephant %s (%d yo, tusk %.0fcm)",
e.name(), e.age(), e.tuskLengthCm());
case Penguin p -> String.format("Penguin %s from colony %d", p.name(), p.colonyId());
case Snake s -> String.format("Snake %s (%s)",
s.name(), s.isVenomous() ? "venomous" : "harmless");
};
}
public static void main(String[] args) {
Zoo zoo = new Zoo();
Lion simba = new Lion("Simba", 180.0, 5, true);
Elephant dumbo = new Elephant("Dumbo", 3500.0, 10, 80);
Penguin pingu = new Penguin("Pingu", 15.0, 3, 42);
Snake slither = new Snake("Slither", 4.0, 2, true);
zoo.addAnimal(simba);
zoo.addAnimal(dumbo);
zoo.addAnimal(pingu);
zoo.addAnimal(slither);
System.out.println("Added 4 animals");
System.out.printf("Zoo: %d animals, total weight %.1fkg%n",
zoo.totalAnimals(), zoo.totalWeight());
System.out.println("Daily food:");
zoo.animals().forEach(a ->
System.out.printf(" %s: %.2f kg%n", a.name(), dailyFoodKg(a)));
System.out.println("Older than 4 years old:");
zoo.olderThan(4).forEach(a -> System.out.println(" " + describeAnimal(a)));
System.out.println("Species count: " + zoo.countBySpecies());
}
}
Giải thích từng phần:
-
sealed interface Animal permits Lion, Elephant, Penguin, Snake— 4 loài đóng kín. Thêm loài mới phải updatepermits→ compiler bắt mọi switch chưa phủ case mới. Đây là ưu điểm lớn nhất của sealed. -
Record cho mỗi loài — immutable, tự sinh constructor/accessor/equals/hashCode. Validate trong compact constructor: tên không blank, cân nặng dương, tuổi không âm.
validateCommonlà helperstaticđể không lặp code. -
class Zoo— composition vớiList<Animal>, KHÔNG extendsArrayList(theo bài 6). Methodanimals()trảList.copyOf— snapshot immutable, bảo vệ encapsulation. -
Switch pattern matching trong 4 chỗ (
totalWeight,olderThan,dailyFoodKg,describeAnimal) — không códefaultvì compiler biết 4 case phủ đủ nhờ sealed. ThêmParrotvào permits → 4 switch compile error ngay lập tức, IDE chỉ vị trí cần update. -
dailyFoodKgvàdescribeAnimal— logic khác nhau theo loài, giữ ngoài record. Lựa chọn thiết kế: có thể đặt là method trongAnimalinterface (mỗi record override) — cả 2 cách đều hợp lý. Đặt ngoài vì:- Giữ record thuần data holder.
- Business logic (công thức food) có thể thay đổi, không muốn entity chịu trách nhiệm.
-
countBySpecies— dùngCollectors.groupingByvớigetClass().getSimpleName()làm key vàCollectors.counting()làm value. Output{Lion=1, Elephant=1, ...}.
🎓 Mở rộng
Mức 1 — Thêm loài mới (Parrot):
Thêm Parrot(String name, double weightKg, int age, boolean canTalk):
- Thêm vào
permits Lion, Elephant, Penguin, Snake, Parrot. - Tạo record.
- Compiler ngay lập tức báo lỗi 4 switch — update từng chỗ. Đây là điểm mạnh sealed.
Mức 2 — Chuyển logic vào record với polymorphism:
public sealed interface Animal permits Lion, Elephant, Penguin, Snake {
String name();
double weightKg();
int age();
double dailyFoodKg(); // thay vi static function
}
public record Lion(...) implements Animal {
@Override public double dailyFoodKg() { return weightKg() * 0.05; }
}
Trade-off: nếu logic là domain behaviour, đặt trong record OK. Nếu là application logic (tính phí, report), tách ra ngoài.
Mức 3 — Unit test:
@Test
void lionFoodIsFivePercent() {
assertEquals(5.0, ZooApp.dailyFoodKg(new Lion("X", 100, 5, true)), 0.001);
}
@Test
void venomousSnakeEatsMore() {
double v = ZooApp.dailyFoodKg(new Snake("V", 10, 5, true));
double h = ZooApp.dailyFoodKg(new Snake("H", 10, 5, false));
assertTrue(v > h);
}
@Test
void negativeWeightRejects() {
assertThrows(IllegalArgumentException.class,
() -> new Lion("X", -1, 5, true));
}
Mức 4 — Thêm abstract method vào sealed (ràng buộc polymorphism):
public sealed interface Animal permits ... {
String name();
int age();
default String species() {
return getClass().getSimpleName();
}
}
Mỗi record tự có species() qua reflection — tiện cho log/report.
Mức 5 — Visitor pattern không cần:
Với sealed + pattern matching, visitor pattern cổ điển không còn cần. Mỗi "visitor" là một static method với switch exhaustive — ngắn hơn, type-safe hơn. Đây là lý do pattern matching đổi cách code OOP.
✨ Điều bạn vừa làm được
Hoàn thành mini-challenge này, bạn đã:
- Thiết kế ADT đầy đủ trong Java 21: sealed interface + record + switch pattern matching. Tập subtype đóng kín, compiler kiểm tra exhaustive.
- Áp dụng composition over inheritance cho Zoo — class chứa list animal, không extends
ArrayList. Encapsulation quaList.copyOfsnapshot. - Validate input trong compact constructor record, throw
IllegalArgumentExceptioncho input sai. - Dùng switch pattern matching không có
default— compiler buộc phủ mọi case. Thêm loài mới → mọi switch chưa update compile error ngay. - Tách domain data (record) khỏi business logic (static function) — record immutable, logic có thể thay đổi độc lập.
- Stream API cơ bản:
filter,mapToDouble,sum,groupingBy,counting,toList()immutable.
Chúc mừng — bạn đã hoàn thành Module 6! Bạn đã có đầy đủ công cụ OOP + kế thừa + pattern matching để mô hình domain phức tạp. Module 7 sẽ bước sang Exception handling — try/catch/finally, checked vs unchecked, try-with-resources, và vì sao "nuốt exception" luôn là anti-pattern.