앞 장에서 클래스로 객체를 만드는 법을 배웠어요. 이번엔 이미 만든 클래스를 물려받아(상속) 재사용하고 확장하는 법을 배웁니다. 같은 코드를 두 번 쓰지 않게 해 주는, 객체 지향의 핵심 무기예요.
extends 키워드로 부모–자식 클래스를 만들 수 있어요.super로 부모의 필드·메서드·생성자에 접근하고, 생성자 실행 순서를 이해해요.상속은 "기존 클래스가 가진 필드와 메서드를 그대로 물려받아, 재사용하고 확장하는 것"이에요. 새 클래스를 맨바닥부터 만드는 대신, 이미 잘 만들어 둔 클래스를 이어받아 필요한 부분만 더하는 거죠.
Dog(개)와 Cat(고양이) 클래스를 각각 만든다고 해봐요. 둘 다 이름(name)이 있고 먹기(eat)·자기(sleep)를 해요. 이 공통 코드를 양쪽에 똑같이 두 번 쓰면 낭비고, 나중에 고칠 때도 두 군데를 손봐야 해요.Dog·Cat이 그걸 물려받게 하면 됩니다. 공통은 한 곳에서 관리하고, 각자의 특징만 따로 추가해요.
💡 상속의 핵심 이득: ① 코드 중복 제거 · ② 재사용 · ③ 확장 편의. "공통은 위에, 차이는 아래에" 라고 기억하세요.
상속 관계에는 용어가 있어요. 물려주는 쪽을 부모(상위 클래스, 슈퍼클래스, super class), 물려받는 쪽을 자식(하위 클래스, 서브클래스, sub class)이라고 불러요.
class 자식 extends 부모.상속은 extends(확장하다) 키워드로 표현해요. 자식 클래스 이름 뒤에 extends 부모클래스를 붙이면, 그 순간 자식은 부모의 필드·메서드를 모두 물려받습니다.
class 자식클래스 extends 부모클래스 {
// 부모에게 물려받은 것 + 자식만의 새로운 것
}
아래 예제를 볼게요. Animal이 부모, Dog과 Cat이 자식이에요. 자식 안에는 eat()을 쓰지 않았는데도 부모에게서 물려받았기 때문에 그대로 호출할 수 있어요.
// 부모 클래스
class Animal {
String name;
void eat() {
System.out.println(name + "가 먹이를 먹습니다.");
}
void sleep() {
System.out.println(name + "가 잠을 잡니다.");
}
}
// 자식 클래스 — Animal의 것을 물려받고, 자기 것을 추가
class Dog extends Animal {
void bark() { // Dog만의 새로운 메서드
System.out.println(name + ": 멍멍!");
}
}
class Cat extends Animal {
void scratch() { // Cat만의 새로운 메서드
System.out.println(name + ": 스크래치!");
}
}
public class Main {
public static void main(String[] args) {
Dog d = new Dog();
d.name = "바둑이"; // 물려받은 필드
d.eat(); // 물려받은 메서드 → 바둑이가 먹이를 먹습니다.
d.bark(); // 자기 메서드 → 바둑이: 멍멍!
Cat c = new Cat();
c.name = "나비";
c.sleep(); // 물려받은 메서드 → 나비가 잠을 잡니다.
c.scratch(); // 자기 메서드 → 나비: 스크래치!
}
}
Dog 객체 하나에는 물려받은 name·eat()·sleep()과 직접 만든 bark()가 모두 들어 있어요.Animal은 자식이 추가한 bark()를 모릅니다. 물려주는 방향은 부모 → 자식(아래로) 한 방향이에요.
즉, 자식은 부모보다 더 많은 걸 가진 존재예요. "물려받은 것 + 새로 더한 것"이니까요.
private 필드·메서드는 물려받긴 해도 자식이 직접 이름으로 접근할 수 없어요. 공통으로 열어줄 것은 보통 protected나 public으로 두거나, getter를 통해 씁니다.private(이 클래스 안에서만)과 public(어디서나)의 중간이에요. 자식 클래스에서는 물려받은 멤버에 직접 접근할 수 있게 열어 주되, 상관없는 외부에는 감추고 싶을 때 써요.super는 "부모 것"을 가리켜요. 필드·메서드·생성자 세 가지로 나눠 봐요.super는 "부모(상위 클래스)"를 가리키는 키워드예요. 자식 안에서 부모의 필드나 메서드를 콕 집어 쓰고 싶을 때, 그리고 부모의 생성자를 호출할 때 사용해요.
super.필드, super.메서드(). 특히 자식이 부모 메서드를 재정의(오버라이딩)했을 때, 원래 부모 버전을 호출하려면 super.메서드()를 써요.super(값...). 자식 생성자 안에서 맨 첫 줄에 써서 부모 생성자를 먼저 실행시켜요.
class Animal {
String name;
Animal(String name) { // 부모 생성자
this.name = name;
System.out.println("Animal 생성자 실행");
}
void info() {
System.out.println("이름: " + name);
}
}
class Dog extends Animal {
String breed; // 품종 (Dog만의 필드)
Dog(String name, String breed) {
super(name); // ① 부모 생성자 먼저 호출 (반드시 첫 줄)
this.breed = breed;
System.out.println("Dog 생성자 실행");
}
void info() {
super.info(); // ② 부모의 info() 를 그대로 호출
System.out.println("품종: " + breed);
}
}
public class Main {
public static void main(String[] args) {
Dog d = new Dog("바둑이", "진돗개");
d.info();
}
}
new Dog(...)를 실행하면 아래 순서로 출력돼요.Animal 생성자 실행Dog 생성자 실행이름: 바둑이품종: 진돗개
👉 자식 객체를 만들면 부모 생성자가 먼저 실행되고, 그다음 자식 생성자가 실행돼요. "집을 지을 때 기초(부모)부터 올리고 그 위에 내 방(자식)을 얹는다"고 생각하면 쉬워요.
super(...)를 안 쓰면, 자바가 자동으로 부모의 기본 생성자 super()를 몰래 넣어줘요. 그래서 부모에 기본 생성자가 없고 매개변수 생성자만 있으면, 자식에서 super(값)을 직접 호출해 줘야 오류가 안 나요.오버라이딩(재정의)은 "부모에게 물려받은 메서드를, 자식이 같은 이름·같은 모양으로 새로 정의하는 것"이에요. 물려받은 대로 쓰는 게 아니라, 자식에 맞게 내용을 바꿔치기 하는 거죠.
class Animal {
void sound() {
System.out.println("동물이 소리를 냅니다.");
}
}
class Dog extends Animal {
@Override // 재정의라고 표시하는 애너테이션
void sound() { // 이름·매개변수·반환형이 부모와 똑같음
System.out.println("멍멍!"); // 내용만 자식 방식으로 교체
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("야옹!");
}
}
public class Main {
public static void main(String[] args) {
new Animal().sound(); // 동물이 소리를 냅니다.
new Dog().sound(); // 멍멍! ← 재정의된 버전이 실행
new Cat().sound(); // 야옹!
}
}
@Override는 "나 지금 부모 메서드를 재정의하는 거야"라고 컴파일러에게 알리는 표시예요. 필수는 아니지만 강력 추천합니다.@로 붙이는 표시(메모)예요. 프로그램 동작을 직접 바꾸진 않지만, 컴파일러나 도구에게 "이건 이런 코드야"라고 알려 줘요. @Override는 "이건 부모 걸 재정의한 메서드야"라는 표시라서, 실수를 잡아 주는 거예요.public을 private으로 못 낮춤).여기서 자주 헷갈리는 게 오버로딩(Overloading)과의 차이예요. 이름이 비슷해서 섞이기 쉬운데, 완전히 다른 개념이에요. 표로 확실히 갈라 둡시다.
| 구분 | 오버라이딩 (Overriding · 재정의) | 오버로딩 (Overloading · 중복정의) |
|---|---|---|
| 뜻 | 부모 메서드를 자식이 다시 정의 | 같은 이름 메서드를 여러 개 정의 |
| 관계 | 부모–자식 상속 관계에서 발생 | 한 클래스 안에서 발생 (상속 무관) |
| 메서드 이름 | 같아야 함 | 같아야 함 |
| 매개변수 | 똑같아야 함 | 달라야 함 (개수·타입) |
| 반환형 | 같아야 함 | 상관없음 (구분 기준 아님) |
| 한 줄 요약 | "내용을 바꿔치기" | "이름은 같고 매개변수만 다른 형제들" |
class Calculator {
int add(int a, int b) { return a + b; } // ①
int add(int a, int b, int c) { return a + b + c; } // ② 매개변수 개수 다름
double add(double a, double b){ return a + b; } // ③ 매개변수 타입 다름
}
세 add는 이름은 같지만 매개변수가 다르므로 각각 별개의 메서드예요(오버로딩). 반면 오버라이딩은 부모의 메서드 하나를 자식이 똑같은 모양으로 갈아끼우는 것이라 매개변수가 같아야 하죠. "오버로딩 = 매개변수 다름, 오버라이딩 = 매개변수 같음"으로 외워두면 편해요.
자바의 모든 클래스는 Object 클래스를 맨 꼭대기 부모로 둡니다. extends를 안 써도, 자바가 자동으로 extends Object를 붙여줘요. 즉 세상의 모든 자바 클래스는 Object의 자손이에요.
// 이렇게 쓰면
class Person { }
// 자바 내부적으로는 이것과 같음
class Person extends Object { }
그 덕분에 우리가 만든 모든 객체는 Object에게 물려받은 메서드를 기본으로 가지고 있어요. 대표적인 두 가지를 봐요.
| Object 메서드 | 하는 일 | 보통 어떻게? |
|---|---|---|
toString() | 객체를 문자열로 표현. System.out.println(객체) 할 때 자동 호출돼요. | 사람이 읽기 좋게 오버라이딩해서 사용 |
equals() | 두 객체가 같은지 비교 | 내용 비교하려면 오버라이딩 |
hashCode() | 객체의 해시값 반환 (컬렉션에서 사용) | equals와 함께 재정의 |
getClass() | 객체의 실제 클래스 정보 반환 | 그대로 사용 |
class Person {
String name;
Person(String name) { this.name = name; }
@Override
public String toString() { // Object의 toString() 재정의
return "Person{name=" + name + "}";
}
}
public class Main {
public static void main(String[] args) {
Person p = new Person("홍길동");
System.out.println(p); // Person{name=홍길동} ← 재정의한 toString() 자동 호출
}
}
toString()·equals()가 모든 객체에 이미 있는 이유가 바로 상속이에요. 우리는 그걸 재정의(오버라이딩)해서 우리 클래스에 맞게 바꿔 쓸 뿐이죠. "상속이 실제로 이렇게 쓰인다"는 걸 보여주는 대표 사례예요.자바 클래스는 부모를 딱 하나만 가질 수 있어요. 이걸 단일 상속(Single Inheritance)이라고 해요. 즉 extends 뒤에는 클래스 하나만 올 수 있고, 여러 클래스를 동시에 상속받는 다중 상속은 불가능합니다.
class A { }
class B { }
// ✅ 가능 — 부모 하나
class C extends A { }
// ❌ 불가능 — 자바는 클래스 다중 상속을 지원하지 않음 (컴파일 오류)
// class D extends A, B { }
A와 B가 이름이 같은 메서드 run()을 각각 가졌다고 해봐요. D가 둘 다 상속받으면, D.run()이 A의 것인지 B의 것인지 모호해져요.extends 하지만, 인터페이스는 여러 개를 implements 할 수 있어요. 인터페이스는 다음 챕터들(CH 13)에서 자세히 배웁니다. 지금은 "클래스 다중 상속은 안 되지만, 인터페이스로 여러 개를 구현할 수 있다"만 기억하세요.class Animal { }
class Pet extends Animal { } // Pet 의 부모는 Animal
class Dog extends Pet { } // Dog 의 부모는 Pet, 조상은 Animal → Object
Dog은 Pet을 물려받고, Pet은 Animal을 물려받아요. 그러니 Dog은 Pet·Animal·Object의 것을 모두 물려받죠. 이렇게 사슬처럼 이어지는 걸 상속 계층(계층 구조)이라고 해요. 그래도 각 클래스의 직속 부모는 여전히 하나랍니다.
class 자식 extends 부모 — 자식은 부모 것 + 자기 것을 모두 가져요.super: 부모 멤버 접근(super.메서드())과 부모 생성자 호출(super(...)). 생성자는 부모 먼저 → 자식 순서로 실행돼요.@Override). 오버로딩(이름 같고 매개변수 다름)과는 달라요.toString·equals 등을 물려줌).