상속이란 상위클래스에서 정의한 필드와 메서드를 하위클래스도 동일하게 사용할 수 있게 물려받는 것이다.
👉 특징
Cat은 동물 Animal이다👉 언제 사용할까?
상속은 코드 재사용성을 높이는 기능이지만 항상 최선은 아니다. 그러나 이펙티브 자바에서는 상속은 아래와 같은 경우 안전하다고 말한다.
👉 단점
👉 자바는 왜 클래스 간 단일 상속만 지원할까?
위 클래스 다이어그램과 같은 상속 구조에서 발생되는 문제가 다이아몬드 문제이다. 예를 들어 Person이라는 클래스가 myMethod() 라는 이름의 메소드를 가지고 있다고 가정하자. 그리고 Father과 Mother가 각각 오버라이딩하여 구현하였다면, Father과 Mother을 모두 상속받은 child 클래스 입장에서는 어떤 부모의 myMethod()를 사용해야 할까❓
여기서 충돌이 발생한다. C++에서는 이런 문제가 있음에도 불구하고 개발자에게 일임하고, 자바는 내부적으로 구현이 불가하도록 막아둔다 (C나 C++은 개발자들을 믿고 자유를 주는 반면에 JAVA는 개발 편의성을 생각하는 언어라는 사실을 알 수 있다.)
인터페이스는 실질적인 구현이 이루어지지 않고 메소드에 대한 정의만 하고 있기 때문에 다중 상속이 허용된다. 결국엔 메소드가 겹치더라도 최종 구현 부분은 구현 객체(Concrete class)에서 이루어질 것이기 때문에 문제가 없게 된다.
기존 클래스 상속을 통한 확장 대신에 필드로 클래스의 인스턴스를 참조하게 만드는 것이다.
자동차(Car)와 엔진 종류(Engine)으로 예를 들 수 있다.
🖥️ Car 클래스
interface Engine {
void start();
}
class GasolineEngine implements Engine {
@Override
public void start() {
System.out.println("Gasoline engine starts");
}
}
class ElectricEngine implements Engine {
@Override
public void start() {
System.out.println("Electric engine starts");
}
}
class Car {
private Engine engine;
Car(Engine engine) {
this.engine = engine;
}
void setEngine(Engine engine) { // 런타임에 엔진 교체 가능
this.engine = engine;
}
void drive() {
engine.start();
System.out.println("Car drives");
}
}
🖥️ Main 클래스
public class Main {
public static void main(String[] args) {
Engine gasolineEngine = new GasolineEngine();
Car car = new Car(gasolineEngine);
car.drive(); // "Gasoline engine starts", "Car drives"
// 런타임에 엔진 교체
Engine electricEngine = new ElectricEngine();
car.setEngine(electricEngine);
car.drive(); // "Electric engine starts", "Car drives"
}
}
Car 클래스가 Engine 클래스의 기능이 필요하다고 해서 무조건 상속하지 말고, 따로 클래스 인스턴스 변수에 저장하여 가져다 쓰는 원리이다. 이 방식을 🌟 포워딩(forwarding) 이라고 하며 필드의 인스턴스를 참조해 사용하는 메소드를 포워딩 메소드라고 부른다. 그래서 클래스 간의 합성 관계를 사용하는데 다른 말로 has-a 관계라고도 한다. 객체 지향에서 다른 클래스를 활용하는 방법 중 하나라고 할 수 있다.
합성은 구현에 의존하지 않는 점에서 상속과 다르다. SOLID의 DIP원칙은 "추상화에 의존하고 구체화에 의존하면 안된다"는 원칙이다. 합성은 객체의 내부는 공개되지 않고 인터페이스를 통해 코드를 재사용하기 때문에, 구현에 대한 의존성을 인터페이스에 대한 의존성으로 변경하여 결합도를 낮춘다.
상속 관계는 클래스 사이의 정적인 관계지만 합성 관계는 객체 사이의 동적인 관계이다. 즉, 코드 작성 시점에 결정한 상속 관계는 변경이 불가능하지만(컴파일 타임), 합성 관계는 실행 시점에 동적으로 변경할 수 있다(런타임). 그래서 합성을 사용하고 인터페이스 타입을 사용한다면 런타임시에 외부에서 필요한 전략에 따라 교체하며 사용할 수 있으므로 유연한 설계가 가능하다.
ex) 코드에서 Car 클래스가 Engine 객체를 필드로 가지고 있는데, 다른 종류의 Engine(예: ElectricEngine 또는 GasolineEngine)을 런타임에 동적으로 변경할 수 있다. 만약 Car가 GasolineEngine 또는 ElectricEngine 클래스에 직접 의존한다면, 새로운 종류의 엔진이 추가될 때마다 Car 클래스도 수정해야하지만 Engine 인터페이스에 의존한다면 새로운 엔진 구현체를 추가하기만 하면 호환이 되는 것이다.
그러나 상속과는 달리 클래스 간의 관계를 파악하는데 시간이 더 걸리고 코드가 복잡해질 수 있다는 단점도 있다.
생성자 호출
자식 클래스의 생성자에서 부모 클래스의 생성자를 명시적으로 호출할 수 있다.
class Parent {
Parent(String name) {
System.out.println("Parent: " + name);
}
}
class Child extends Parent {
Child(String name) {
super(name); // 부모 클래스 생성자 호출
}
}
만약 Child person1 = new Child();과 Parent person2 = new Person(); 두가지 객체를 main함수에서 만든다고 가정하자. 먼저 person1 객체를 선언할 때 메모리 구조 stack영역에는 참조 변수 person1가 생성되며, Child 클래스의 인스턴스 뿐만 아니라 Parent 클래스의 인스턴스도 함께 Heap영역에 생성된다. 이때 person1 객체 참조 변수는 Child 클래스를 가르키고 있다. 이후 person2 객체가 선언되면 person2 객체 참조 변수가 가르키고 있는 것은 Child가 아닌 Parent 인스턴스이다.
멤버 변수/메서드 호출
자식 클래스에서 부모 클래스의 메서드나 변수와 동일한 이름이 있을 경우, super를 사용하여 부모 멤버를 호출한다.
class Parent {
int value = 100;
}
class Child extends Parent {
int value = 200;
void printValue() {
System.out.println(super.value); // 부모 클래스의 value
}
}
모든 클래스의 부모 클래스이며, 자바의 최상위 클래스이다.
자바의 모든 클래스는 Object 클래스의 모든 필드와 메소드를 상속 받게 되고, 별도의 extends 키워드를 사용하지 않아도 Object 클래스의 모든 멤버를 자유롭게 사용 가능하다.
아래는 Object 클래스의 주요 메서드이다.
부모 클래스의 메서드를 자식 클래스에서 재정의하여 구현하는 것이다.
자바에서 자식 클래스는 부모 클래스의 private 멤버를 제외한 모든 메소드를 상속받는다. 이렇게 상속받은 메소드는 그대로 사용해도 되고 재정의하여 사용할 수도 있다.
class Parent {
void display() {
System.out.println("Parent display");
}
}
class Child extends Parent {
@Override
void display() {
System.out.println("Child display");
}
}
메소드 디스패치는 어떤 메소드를 호출할 지 결정하여 실제로 실행시키는 과정을 말한다.
다이나믹 메소드 디스패치란 참조 변수는 부모 타입으로 선언하고, 실제 객체는 자식 타입인 경우 실행 시점에서 어떤 메서드가 호출될지 결정되는 방식이다.
자바에서 오버라이드된 메서드는 참조 변수의 타입이 아니라 실제 객체의 타입에 따라 호출된다. 이 결정은 컴파일 타임이 아닌 런타임에 이루진다. 즉, 컴파일 타임에는 참조 변수의 타입을 기준으로 유효성을 검사하고 런타임에는 실제 참조하는 객체의 타입을 기준으로 메서드가 호출되는 것이다. 그래서 런타임 다형성이라고도 불린다.
class Parent {
void display() {
System.out.println("Parent method");
}
}
class Child extends Parent {
@Override
void display() {
System.out.println("Child method");
}
}
public class Test {
public static void main(String[] args) {
Parent obj = new Child(); // 부모 타입으로 자식 객체 참조
obj.display(); // Child method 출력 (다이나믹 디스패치)
}
}
만약 Child p = new Child(); p.display(); 라는 코드가 작성되었다면 우리는 Child클래스의 오버라이딩 된 함수 display가 불릴 것을 알고 있다. 우리가 이미 알고 있는 것과 같이 컴파일러 역시도 이 메소드를 호출하고 실행시켜야되는 것을 명확하게 알고 있는데 우리는 이를 정적 메소드 디스패치라고 부른다.
즉, 동적 메소드 디스패치는 정적 디스패치와는 다르게 컴파일러가 어떤 메소드를 호출해야되는지 모르는 것을 말한다.
구현되지 않은 추상메서드를 포함하거나 단독으로 선언된 클래스
abstract 키워드로 선언된다. abstract class Shape {
abstract void draw();
}
class Circle extends Shape {
@Override
void draw() {
System.out.println("Drawing Circle");
}
}
추상 메소드가 포함된 추상 클래스를 상속 받은 모든 자식 클래스는 추상 메소드를 구현해야만 인스턴스를 생성할 수 있으므로 반드시 구현하게 된다. 또한 동일한 부모를 가지는 클래스를 묶는 개념으로 상속을 받아서 기능을 확장시키는 것으로 볼 수 있다.
⚠️ 추상 클래스에는 일반 메서드 구현이 가능하다.
abstract class Animal {
// 추상 메서드: 구현 없음
abstract void sound();
// 일반 메서드: 구현 있음
void eat() {
System.out.println("This animal eats food.");
}
}
class Dog extends Animal {
// 추상 메서드 구현
@Override
void sound() {
System.out.println("Woof Woof");
}
}
public class Main {
public static void main(String[] args) {
Animal myDog = new Dog();
myDog.sound(); // 출력: Woof Woof
myDog.eat(); // 출력: This animal eats food.
}
}
⚠️ 클래스와 인터페이스 사이 관계

➡️ 공통점
extends로 상속받은 자식들과 / 인터페이스를 implements하고 구현한 자식들만 객체를 생성할 수 O➡️ 차이점
클래스, 메서드, 변수에서 사용되며 특정 요소의 변경을 제한한다.
final 클래스 : 상속 X
final class Parent {}
// class Child extends Parent {} // 에러 발생
final 메서드 : 오버라이딩 X
class Parent {
final void display() {
System.out.println("Cannot Override");
}
}
final 변수 : 한번 값이 할당되면 변경 X
인스턴스 변수나 static으로 선언된 클래스 변수는 선언과 함께 값을 지정해야한다.
final int MAX = 100;
// MAX = 200; // 에러 발생