부모 → 자식 순서로 초기화가 진행되고, 각 단계에서 필드 초기화 블록이 생성자보다 먼저 실행됨을 보여준다.
class Vehicle {
static { System.out.println("1. Vehicle static 블록"); }
{ System.out.println("3. Vehicle 인스턴스 블록"); }
protected String plate;
Vehicle(String plate) {
this.plate = plate;
System.out.println("4. Vehicle 생성자: " + plate);
}
}
class Truck extends Vehicle {
static { System.out.println("2. Truck static 블록"); }
{ System.out.println("5. Truck 인스턴스 블록"); }
private final int maxLoadKg;
Truck(String plate, int maxLoadKg) {
super(plate); // 부모 생성자 명시 호출 (기본 생성자가 없으므로 필수)
this.maxLoadKg = maxLoadKg;
System.out.println("6. Truck 생성자: " + maxLoadKg + "kg");
}
}
public class Main {
public static void main(String[] args) {
new Truck("12가3456", 5000);
// 출력:
// 1. Vehicle static 블록
// 2. Truck static 블록
// 3. Vehicle 인스턴스 블록
// 4. Vehicle 생성자: 12가3456
// 5. Truck 인스턴스 블록
// 6. Truck 생성자: 5000kg
}
}부모 타입 배열 하나로 여러 하위 타입을 처리하며, 오버라이딩된 calculatePay()가 실제 객체에 따라 다르게 실행됨을 보여준다.
class Employee {
protected final String name;
protected final long baseSalary;
Employee(String name, long baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}
long calculatePay() { return baseSalary; }
@Override
public String toString() {
return String.format("%s(%s) 급여: %,d원", name, getClass().getSimpleName(), calculatePay());
}
}
class Manager extends Employee {
private final long teamBonus;
Manager(String name, long baseSalary, long teamBonus) {
super(name, baseSalary);
this.teamBonus = teamBonus;
}
@Override
long calculatePay() { return super.calculatePay() + teamBonus; } // 부모 로직 재사용 + 확장
}
class Engineer extends Employee {
private final int overtimeHours;
Engineer(String name, long baseSalary, int overtimeHours) {
super(name, baseSalary);
this.overtimeHours = overtimeHours;
}
@Override
long calculatePay() { return baseSalary + overtimeHours * 30_000L; }
}
public class Main {
public static void main(String[] args) {
Employee[] staff = {
new Employee("김사원", 3_000_000),
new Manager("이팀장", 5_000_000, 1_000_000),
new Engineer("박개발", 4_000_000, 10)
};
long total = 0;
for (Employee e : staff) {
System.out.println(e); // toString은 Employee 것, calculatePay는 각자 것
total += e.calculatePay(); // 동적 바인딩
}
System.out.printf("총 인건비: %,d원%n", total);
// 출력:
// 김사원(Employee) 급여: 3,000,000원
// 이팀장(Manager) 급여: 6,000,000원
// 박개발(Engineer) 급여: 4,300,000원
// 총 인건비: 13,300,000원
}
}toString()은 Employee에만 정의되어 있지만 그 안에서 호출하는 calculatePay()는 동적 바인딩되므로 Manager 객체에서는 Manager.calculatePay()가 실행된다. 이것이 템플릿 메서드 패턴의 기초다.
봉인된 계층으로 주문 이벤트를 정의하고, default 없는 switch로 빠짐없이 처리하는 방법을 보여준다.
sealed interface OrderEvent permits OrderPlaced, OrderPaid, OrderCancelled {}
record OrderPlaced(String orderId, long amount) implements OrderEvent {}
record OrderPaid(String orderId, String method) implements OrderEvent {}
record OrderCancelled(String orderId, String reason) implements OrderEvent {}
public class Main {
static String describe(OrderEvent event) {
return switch (event) { // default 없음 — sealed이므로 컴파일러가 완전성 검사
case OrderPlaced p when p.amount() >= 100_000 -> "[VIP] 주문 접수 " + p.orderId();
case OrderPlaced p -> "주문 접수 " + p.orderId();
case OrderPaid p -> "결제 완료 " + p.orderId() + " (" + p.method() + ")";
case OrderCancelled c -> "취소 " + c.orderId() + ": " + c.reason();
};
}
public static void main(String[] args) {
var events = java.util.List.of(
new OrderPlaced("A-1", 150_000),
new OrderPlaced("A-2", 20_000),
new OrderPaid("A-1", "CARD"),
new OrderCancelled("A-2", "고객 요청")
);
events.forEach(e -> System.out.println(describe(e)));
// 출력:
// [VIP] 주문 접수 A-1
// 주문 접수 A-2
// 결제 완료 A-1 (CARD)
// 취소 A-2: 고객 요청
}
}hashCode를 빠뜨리면 HashSet이 중복을 못 잡는 것을 대조하고, record로 해결하는 방법을 보여준다.
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
class MemberBroken {
final String id;
MemberBroken(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
return o instanceof MemberBroken m && id.equals(m.id);
}
// hashCode 오버라이딩 누락!
}
class MemberFixed {
final String id;
MemberFixed(String id) { this.id = id; }
@Override
public boolean equals(Object o) {
return o instanceof MemberFixed m && id.equals(m.id);
}
@Override
public int hashCode() { return Objects.hash(id); }
}
record MemberRecord(String id) {} // equals/hashCode/toString 자동 생성
public class Main {
public static void main(String[] args) {
Set<MemberBroken> broken = new HashSet<>();
broken.add(new MemberBroken("u-100"));
broken.add(new MemberBroken("u-100"));
System.out.println("broken size = " + broken.size()); // 출력: broken size = 2 (버그!)
Set<MemberFixed> fixed = new HashSet<>();
fixed.add(new MemberFixed("u-100"));
fixed.add(new MemberFixed("u-100"));
System.out.println("fixed size = " + fixed.size()); // 출력: fixed size = 1
Set<MemberRecord> rec = new HashSet<>();
rec.add(new MemberRecord("u-100"));
rec.add(new MemberRecord("u-100"));
System.out.println("record size = " + rec.size()); // 출력: record size = 1
System.out.println(new MemberRecord("u-100")); // 출력: MemberRecord[id=u-100]
}
}HashSet을 상속하면 addAll이 내부적으로 add를 호출해 카운트가 두 배가 되는 문제와, 컴포지션으로 해결한 버전을 대조한다.
import java.util.*;
class CountingSetInherited<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) { addCount++; return super.add(e); }
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c); // 내부에서 add()를 다시 호출 → 이중 카운트
}
int getAddCount() { return addCount; }
}
class CountingSetComposed<E> {
private final Set<E> inner = new HashSet<>(); // 위임 대상
private int addCount = 0;
boolean add(E e) { addCount++; return inner.add(e); }
boolean addAll(Collection<? extends E> c) {
boolean changed = false;
for (E e : c) changed |= add(e); // 내 add를 통해서만 카운트
return changed;
}
int getAddCount() { return addCount; }
int size() { return inner.size(); }
}
public class Main {
public static void main(String[] args) {
var inherited = new CountingSetInherited<String>();
inherited.addAll(List.of("a", "b", "c"));
System.out.println("상속 버전 카운트: " + inherited.getAddCount()); // 출력: 상속 버전 카운트: 6
var composed = new CountingSetComposed<String>();
composed.addAll(List.of("a", "b", "c"));
System.out.println("컴포지션 버전 카운트: " + composed.getAddCount()); // 출력: 컴포지션 버전 카운트: 3
}
}