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가 없으면 컴파일러는 그냥 새 메서드로 받아들인다. 런타임에야 "왜 부모 것이 실행되지?"를 디버깅하게 된다.
class Dog extends Animal {
@Override
public void speak() { System.out.println("멍멍"); } // 오타가 있으면 컴파일 에러로 즉시 발견
}class Employee {
Employee(String name) { /* ... */ }
}
class Manager extends Employee {
Manager(String name) {
// 컴파일러가 super()를 삽입하지만 Employee()는 없음 → 컴파일 에러
}
}부모에 매개변수 있는 생성자만 존재하면 자식은 반드시 명시적으로 어떤 부모 생성자를 쓸지 골라야 한다.
class Manager extends Employee {
Manager(String name) {
super(name); // 첫 줄에 명시
}
}class Parent { String label = "parent"; }
class Child extends Parent { String label = "child"; } // 필드 숨김
Parent p = new Child();
System.out.println(p.label); // 출력: parent (메서드처럼 child가 나오지 않음)필드는 정적 바인딩이므로 변수 타입 기준으로 결정된다. 자식에서 같은 이름의 필드를 선언하는 것 자체가 거의 항상 실수다.
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()); // 출력: childclass 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()이 호출되어 초기화되지 않은 필드를 건드린다.
class Base {
Base() { /* 생성자에서는 private 또는 final 메서드만 호출 */ }
final void init() { /* ... */ } // 또는 초기화를 생성자 밖의 별도 단계로 분리
}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는 호출조차 되지 않는다.
record Point(int x, int y) {} // equals + hashCode 자동 생성. 값 객체는 record로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가 전부 노출된다. 스택은 "중간 삽입"이 가능해서는 안 되지만 막을 방법이 없다.
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(); }
}