공공부하자개발 · 영어 학습 노트
자바
중급객체지향과 코어 라이브러리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정리‹ 이전다음 ›

5. 자주 하는 실수 (Tip)

❌ 실수 1: @Override를 빠뜨려 오버로딩이 되어버림

java
class Animal {
    public void speak() { System.out.println("..."); }
}
class Dog extends Animal {
    public void Speak() { System.out.println("멍멍"); }   // 대문자 S — 새 메서드가 되어버림
}
Animal a = new Dog();
a.speak();   // 출력: ...   (Dog의 Speak는 호출되지 않음)

@Override가 없으면 컴파일러는 그냥 새 메서드로 받아들인다. 런타임에야 "왜 부모 것이 실행되지?"를 디버깅하게 된다.

java
class Dog extends Animal {
    @Override
    public void speak() { System.out.println("멍멍"); }   // 오타가 있으면 컴파일 에러로 즉시 발견
}

❌ 실수 2: 부모 생성자 호출 누락으로 컴파일 에러

java
class Employee {
    Employee(String name) { /* ... */ }
}
class Manager extends Employee {
    Manager(String name) {
        // 컴파일러가 super()를 삽입하지만 Employee()는 없음 → 컴파일 에러
    }
}

부모에 매개변수 있는 생성자만 존재하면 자식은 반드시 명시적으로 어떤 부모 생성자를 쓸지 골라야 한다.

java
class Manager extends Employee {
    Manager(String name) {
        super(name);   // 첫 줄에 명시
    }
}

❌ 실수 3: 필드에 다형성을 기대함

java
class Parent { String label = "parent"; }
class Child extends Parent { String label = "child"; }   // 필드 숨김

Parent p = new Child();
System.out.println(p.label);   // 출력: parent   (메서드처럼 child가 나오지 않음)

필드는 정적 바인딩이므로 변수 타입 기준으로 결정된다. 자식에서 같은 이름의 필드를 선언하는 것 자체가 거의 항상 실수다.

java
class Parent {
    protected String label = "parent";
    String getLabel() { return label; }
}
class Child extends Parent {
    Child() { label = "child"; }   // 필드를 새로 선언하지 않고 부모 필드의 값을 바꿈
}
Parent p = new Child();
System.out.println(p.getLabel());   // 출력: child

❌ 실수 4: 생성자에서 오버라이딩 가능한 메서드 호출

java
class Base {
    Base() { init(); }             // 자식이 오버라이딩할 수 있는 메서드를 생성자에서 호출
    void init() {}
}
class Derived extends Base {
    private final String config = "loaded";
    @Override
    void init() { System.out.println(config.length()); }   // NPE! config는 아직 null
}
new Derived();
// 출력: Exception in thread "main" java.lang.NullPointerException

부모 생성자가 실행되는 시점에 자식 필드는 아직 초기화되지 않았다(2.2절 순서 참고). 그런데 동적 바인딩으로 자식의 init()이 호출되어 초기화되지 않은 필드를 건드린다.

java
class Base {
    Base() { /* 생성자에서는 private 또는 final 메서드만 호출 */ }
    final void init() { /* ... */ }   // 또는 초기화를 생성자 밖의 별도 단계로 분리
}

❌ 실수 5: equals만 오버라이딩하고 hashCode를 빠뜨림

java
class Point {
    int x, y;
    @Override
    public boolean equals(Object o) {
        return o instanceof Point p && p.x == x && p.y == y;
    }
}
Map<Point, String> map = new HashMap<>();
map.put(new Point(1, 2), "A");
System.out.println(map.get(new Point(1, 2)));   // 출력: null  (equals는 true인데 못 찾음)

HashMap은 hashCode로 버킷을 먼저 찾는다. hashCode가 다르면 equals는 호출조차 되지 않는다.

java
record Point(int x, int y) {}   // equals + hashCode 자동 생성. 값 객체는 record로

❌ 실수 6: 코드 재사용만을 위해 무관한 클래스를 상속

java
class Stack<E> extends ArrayList<E> {   // Stack is-a ArrayList? 아니다
    void push(E e) { add(e); }
    E pop() { return remove(size() - 1); }
}
Stack<Integer> s = new Stack<>();
s.push(1); s.push(2);
s.add(0, 99);   // ArrayList의 메서드가 그대로 노출되어 스택 규칙이 깨짐

상속하면 부모의 public API가 전부 노출된다. 스택은 "중간 삽입"이 가능해서는 안 되지만 막을 방법이 없다.

java
class Stack<E> {
    private final ArrayList<E> items = new ArrayList<>();   // 컴포지션
    void push(E e) { items.add(e); }
    E pop() { return items.remove(items.size() - 1); }
    boolean isEmpty() { return items.isEmpty(); }
}
자주 하는 실수 (Tip)
  • ❌ 실수 1: @Override를 빠뜨려 오버로딩이 되어버림
  • ❌ 실수 2: 부모 생성자 호출 누락으로 컴파일 에러
  • ❌ 실수 3: 필드에 다형성을 기대함
  • ❌ 실수 4: 생성자에서 오버라이딩 가능한 메서드 호출
  • ❌ 실수 5: equals만 오버라이딩하고 hashCode를 빠뜨림
  • ❌ 실수 6: 코드 재사용만을 위해 무관한 클래스를 상속
이전 섹션4 응용 변형 예제5 / 7다음 섹션6 연습 문제