공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리0/9 완료
  • 01상속, 다형성, 오버라이딩
  • 02추상 클래스 vs 인터페이스
  • 03예외 처리
  • 04java.lang 심화
  • 05컬렉션 프레임워크 딥다이브
  • 06메서드 활용 패턴 (중급)
  • 07Object 메서드와 비교
  • 08java.time 실무 날짜 계산
  • 09HTTP 와 JSON 기초
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 중급 › 01 / 9

상속, 다형성, 오버라이딩

섹션 7진행 0 / 9
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

3. 코드 예제

예제 1: 생성자 호출 순서와 초기화 블록

부모 → 자식 순서로 초기화가 진행되고, 각 단계에서 필드 초기화 블록이 생성자보다 먼저 실행됨을 보여준다.

java
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
    }
}

예제 2: 급여 계산의 다형성 — Employee/Manager/Engineer

부모 타입 배열 하나로 여러 하위 타입을 처리하며, 오버라이딩된 calculatePay()가 실제 객체에 따라 다르게 실행됨을 보여준다.

java
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()가 실행된다. 이것이 템플릿 메서드 패턴의 기초다.

예제 3: sealed + record + switch 패턴 매칭 (JDK 21)

봉인된 계층으로 주문 이벤트를 정의하고, default 없는 switch로 빠짐없이 처리하는 방법을 보여준다.

java
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: 고객 요청
    }
}

예제 4: equals/hashCode 규약 — 회원 ID로 중복 제거

hashCode를 빠뜨리면 HashSet이 중복을 못 잡는 것을 대조하고, record로 해결하는 방법을 보여준다.

java
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]
    }
}

예제 5: 상속 대신 컴포지션 — 삽입 횟수 세는 Set

HashSet을 상속하면 addAll이 내부적으로 add를 호출해 카운트가 두 배가 되는 문제와, 컴포지션으로 해결한 버전을 대조한다.

java
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
    }
}

예제 직접 실행

아래 폴더를 JDK 21 로 컴파일하고 실행합니다.

cd java-src\intermediate\01_inheritance
javac -encoding UTF-8 *.java && java Main
코드 예제
  • 예제 1: 생성자 호출 순서와 초기화 블록
  • 예제 2: 급여 계산의 다형성 — Employee/Manager/Engineer
  • 예제 3: sealed + record + switch 패턴 매칭 (JDK 21)
  • 예제 4: equals/hashCode 규약 — 회원 ID로 중복 제거
  • 예제 5: 상속 대신 컴포지션 — 삽입 횟수 세는 Set
이전 섹션2 핵심 원리3 / 7다음 섹션4 응용 변형 예제