Post

02 Extends&Implements

02 Extends&Implements

1. extends

1. 상속

  • 한 클래스가 다른 클래스의 속성과 메서드를 물려받는 것
  • “하위 클래스 IS-A 상위 클래스” 관계

2. 목적

  • 공통 기능 재사용 : 상위 클래스에서 구현한 메서드를 하위 클래스가 그대로 사용 가능
  • 기능 특화/확장 : 하위 클래스에서 필요한 메서드를 오버라이드하거나 새로 추가
  • 코드 중복 최소화 : 공통 로직을 상위 클래스에 두고, 하위 클래스에서는 특화 기능만 구현

3. 특징

  • 하위 클래스는 상위 클래스의 모든 public/ protected 멤버 접근 가능
  • 하위 클래스는 상위 클래스의 메서드를 오버라이드 가능
  • 단일 상속만 가능 (Java 기준)
  • 생성자, private 멤버, static 멤버는 상속되지 않음

4. 예시

1
2
3
4
5
6
7
8
9
10
class 조류 {
void 먹다() { System.out.println("조류가 먹는다"); }
void 날다() { System.out.println("조류가 날다"); }
}

class 펭귄 extends 조류 {
@Override
void 날다() { System.out.println("펭귄은 못 날아요"); }
void 헤엄치다() { System.out.println("펭귄이 헤엄친다"); }
}

2. implements

1. 구현

  • 클래스가 반드시 구현해야 하는 행동 계약(메서드 시그니처)을 정의
  • “하위 클래스가 어떤 동작을 할 수 있다”라는 타입 규약을 제공

2. 목적

  • 공통 타입 정의 : 서로 다른 클래스라도 동일한 인터페이스를 구현하면 같은 타입으로 다룰 수 있음
  • 다형성 지원 : 상위 타입 변수로 다양한 객체를 처리 가능
  • 결합도 낮춤 : 구현 내용과 무관하게 행동 계약만 정의 → 하위 클래스가 자유롭게 구현

3. 특징

  • 구현 강제: 인터페이스에 정의된 모든 메서드를 구현해야 함
  • 다중 구현 가능: 한 클래스가 여러 인터페이스를 동시에 구현 가능
  • 구현 내용 없음(Java 8 이전 기준) → Java 8 이후부터 default 메서드 가능
  • 클래스 상속과 동시에 구현 가능

4. 예시

1
2
3
4
5
6
7
8
9
10
11
interface 먹을수있는 {
void 먹다();
}

class 펭귄 implements 먹을수있는 {
public void 먹다() { System.out.println("펭귄은 물고기를 먹는다"); }
}

class 타조 implements 먹을수있는 {
public void 먹다() { System.out.println("타조는 풀을 먹는다"); }
}

3. extends / implements

1. 비교

항목상속(extends)인터페이스(implements)
목적공통 기능 재사용 + 확장공통 타입/행동 계약 정의
구현상위 클래스 코드 재사용 가능구현 강제만, 내용 자유
다중 가능 여부단일 상속다중 구현 가능
결합도높음낮음
사용 예펭귄 extends 조류펭귄 implements 먹을수있는

2. 목적

1. 상속 (extends) – 공통 기능 재사용 + 확장

  • 조류라는 상위 클래스를 만들고, 공통 기능(예: 먹다(), 날다())을 정의
  • 하위 클래스(펭귄, 타조)는 상위 클래스 기능을 그대로 재사용하거나 오버라이드
  • 하위 클래스마다 공통 기능을 공유하고, 필요한 기능을 특화
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    
     class 조류 {
       void 먹다() { System.out.println("조류가 먹는다"); }
       void 날다() { System.out.println("조류가 날다"); }
     }
       
     class 펭귄 extends 조류 {
       @Override
       void 날다() { System.out.println("펭귄은 못 날아요"); }
       void 헤엄치다() { System.out.println("펭귄이 헤엄친다"); }
     }
       
     class 타조 extends 조류 {
       @Override
       void 날다() { System.out.println("타조도 못 날아요"); }
     }
    

2. 구현 (implements) – 공통 타입/행동 계약

  • 조류 타입 인터페이스를 만들어서,
  • “모든 조류는 먹을 수 있다”, “모든 조류는 날 수 있다”라는 행동 계약만 정의
  • 하위 클래스(펭귄, 타조)는 각자 방식으로 구현
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    
     interface 먹을수있는 {
          void 먹다();
     }
    
     class 펭귄 implements 먹을수있는 {
          public void 먹다() { System.out.println("펭귄은 물고기를 먹는다"); }
     }
    
     class 타조 implements 먹을수있는 {
         public void 먹다() { System.out.println("타조는 풀을 먹는다"); }
     }
    

3. 정리

항목상속인터페이스
목적공통 기능 공유 + 확장공통 타입/행동 규약 정의
코드 재사용OX (구현은 자유)
다형성제한적 (단일 상속)자유로운 다형성 (다중 구현 가능)
예시펭귄 extends 조류펭귄 implements 먹을수있는

4. JVM 업로드

1. extends

1. 클래스 로딩

  • JVM의 클래스 로더(ClassLoader)가 조류.class, 타조.class를 읽음
  • 각 클래스의 바이트코드(.class 파일)가 메서드 영역(Method Area)에 로드
    • 클래스 메타데이터 (클래스 이름, 부모 클래스 정보, 메서드 테이블, 필드 정보 등)
    • static 변수와 상수 풀(Constant Pool)
  • 타조는 조류를 상속했으므로, JVM은 상위 클래스 정보(조류.class)도 메서드 영역에 반드시 로드.

2. 인스턴스 생성 시점

  • new 키워드를 만나면 JVM은 힙(Heap)에 객체 메모리를 할당
  • new 타조()는 타조 인스턴스지만, 내부에는 조류로부터 상속받은 필드 공간 + 타조 자신이 정의한 필드 공간이 모두(☆) 들어감.
  • 힙에 올라간 객체는 “조류 부분”과 “타조 부분”이 합쳐진 구조로 올라감.
    1
    2
    3
    
    [타조 인스턴스 - Heap]
    ├─ 조류의 필드(예: String 날개)
    └─ 타조의 필드(예: int 다리길이)
    

    3. 스택에 참조값 저장

    1
    
    조류 bird = new 타조();
    
  • 스택에는 변수 bird가 생기고, 여기에는 힙에 있는 타조 인스턴스의 참조값(주소)이 저장
  • 타입 선언은 조류지만, 실제 가리키는 대상은 타조 객체

4. 메서드 호출 시 (동적 디스패치)

  • 메서드를 호출하면 JVM은 메서드 영역의 메서드 테이블(vtable)을 참조
  • bird.날다() 호출 → 스택에서 bird 참조값 확인 → 힙에 있는 타조 객체 접근 → 타조 클래스의 메서드 테이블 확인.
  • 만약 타조가 날다()를 오버라이드했다면 타조.날다() 실행.
  • 오버라이드 안 했으면 상위 클래스(조류.날다()) 실행.
  • 따라서 실행 시점에는 참조 변수 타입이 아니라, 실제 힙에 있는 객체 타입(런타임 타입)이 기준이 됨

2. implement

1. 클래스 로딩 시점

  • JVM의 클래스 로더(ClassLoader)가 .class 파일을 읽음
  • 인터페이스.class와 구현 클래스.class 모두 메서드 영역(Method Area)에 올라감.
    • 인터페이스: 추상 메서드 시그니처, static final 상수
    • 구현 클래스: 필드 정보, 메서드 구현체, 어떤 인터페이스를 구현했는지에 대한 메타데이터

2. 객체생성

1
2
3
4
5
6
7
8
interface Swim {
  void swim();
}

class Penguin implements Swim {
  int legCount = 2;
  public void swim() { System.out.println("펭귄 수영"); }
}
  • new Penguin() 호출 → 힙(Heap)에 Penguin 인스턴스 생성
  • 힙에는 Penguin 클래스의 필드만 올라감. (인터페이스에는 인스턴스 필드 자체가 없기 때문)
  • JVM은 이 객체가 Swim 인터페이스를 구현했다는 사실을 메타정보로 연결

3. 스택에 참조값 저장

1
  Swim s = new Penguin();
  • 스택에는 s라는 변수 슬롯이 잡히고, 거기에는 힙에 있는 Penguin 객체의 참조값(주소)이 저장
  • 단, 타입은 Swim으로 선언되었기 때문에, s를 통해 호출할 수 있는 메서드는 Swim 인터페이스에 정의된 것들만 허용

4. 메서드 호출 흐름

  • s.swim() 호출 → JVM은 스택에 있는 참조값 s를 보고, 힙에서 Penguin 객체를 찾음.
  • Penguin 클래스의 메서드 테이블(vtable)에 접근해서 swim() 구현체를 찾아 실행.
  • 인터페이스 타입(Swim)은 “이 객체가 반드시 swim()을 구현했음”을 보장하는 역할만함.

5. note

1. 조류 bird = new 타조(); 다운캐스팅!

1
2
3
4
5
6
7
8
9
class Bird {
    void fly() { System.out.println("날다"); }
}

class Ostrich extends Bird {
    void run() { System.out.println("타조가 뛴다"); }
}

Bird bird = new Ostrich();

1. 컴파일러 관점 (정적 타입 체크)

  • bird의 타입은 Bird라서, 컴파일러는 Bird 클래스에 정의된 메서드만 호출 가능하다고 판단
  • 따라서 bird.run(); 은 컴파일 에러 발생. (Bird 타입에는 run() 없음)

2. JVM 실행 시 관점 (동적 디스패치)

  • 실행 시에는 실제 힙에 있는 객체가 Ostrich라서, bird.fly(); 호출하면 Ostrich의 fly()가 실행될 수도 있음(오버라이드한 경우).
  • 하지만 run()처럼 상위 클래스에 선언조차 없는 메서드는, 참조 변수 타입에서 보이지 않으므로 호출 자체가 불가능함.

3. 해결 방법 (다운캐스팅)

1
 ((Ostrich) bird).run(); 
  • 이렇게 캐스팅하면 컴파일러에게 “bird가 실제로는 Ostrich 객체야” 라고 알려주기 때문에, run() 메서드를 사용할 수 있음.
This post is licensed under CC BY 4.0 by the author.