상속?
자식 클래스가 부모 클래스의 '필드(변수)와 메소드(기능)'를 물려받는 것
즉, 이미 만들어진 클래스를 재사용해서 새로운 클래스를 만드는 것
상속 구조 그림
Animal (부모)
↑
Dog (자식)
class 부모클래스 {
// 부모의 변수와 메소드
}
class 자식클래스 extends 부모클래스 {
// 부모의 멤버를 모두 사용 가능
// 필요한 기능은 추가하거나 수정할 수 있음
}
상속의 이점
클래스 상속
public class 자식클래스 extends 부모클래스 {
}
extends는 한 번만!)부모 생성자 호출
자식 생성자가 호출됐을 때 제일 먼저 할 일은 부모 클래스의 생성자를 호출하는 것이다 -> 부모 클래스의 필드가 초기화 코드가 먼저 실행돼야 자식 클래스도 제대로 동작할 수 있기 때문에
자식 객체를 생성하면 부모 객체가 먼저 생성된 다음에 자식 객체가 생성
자식 클래스 변수 = new 자식클래스();
부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출
// 자식 생성자 선언
public 자식클래스(...) {
super(); // super(매개값, ...);
...
}
-> 부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 super(매개값, ...) 코드를 직접 넣어야 한다. 이 코드는 매개값의 타입과 개수가 일치하는 부모 생성자를 호출한다.
자식 클래스 생성자 안에서는
super()키워드를 사용해서 부모 클래스의 생성자를 명시적으로 호출할 수 있음
class Parent {
Parent() {
System.out.println("부모 생성자 호출됨");
}
}
class Child extends Parent {
Child() {
super(); // 생략 가능하지만, 맨 첫 줄에 있어야 함
System.out.println("자식 생성자 호출됨");
}
}
두 코드 모두 SmartPhone 클래스에서 Phone 클래스를 상속하고 있는데, 부모 생성자를 호출하는 방식이 다르다!
📌 두 코드의 차이점
| 항목 | 첫 번째 코드 | 두 번째 코드 |
|---|---|---|
super() 호출 | super() → 매개변수 없는 기본 생성자 호출 | super(model, color) → 매개변수 생성자 호출 |
| 전제 조건 | 부모 클래스에 기본 생성자가 정의되어 있어야 함 | 부모 클래스에 Phone(String model, String color) 생성자가 정의되어 있어야 함 |
this.model = model 등 | 자식 클래스에서 직접 필드 초기화 | 부모 클래스에서 생성자에서 필드를 초기화 |
| 역할 분담 | 필드 초기화 책임이 자식 클래스에 있음 | 필드 초기화 책임이 부모 클래스에 있음 |
1️⃣ 첫 번째 코드
public class Phone {
String model;
String color;
public Phone() {
System.out.println("Phone() 생성자 실행");
}
}
public class SmartPhone extends Phone {
public SmartPhone(String model, String color) {
super(); // 부모의 기본 생성자 호출
this.model = model; // 자식이 필드를 직접 초기화
this.color = color;
System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
}
}
➡ Phone 클래스에 기본 생성자가 있어야 하고,
model과 color는 자식 클래스에서 직접 초기화함.
2️⃣ 두 번째 코드
public class Phone {
String model;
String color;
public Phone(String model, String color) {
this.model = model;
this.color = color;
System.out.println("Phone(String model, String color) 생성자 실행");
}
}
public class SmartPhone extends Phone {
public SmartPhone(String model, String color) {
super(model, color); // 부모의 매개변수 생성자 호출
System.out.println("SmartPhone(String model, String color) 생성자 실행됨");
}
}
➡ Phone 클래스에 매개변수 생성자가 있어야 하고,
필드 초기화는 부모 생성자에서 처리함.
✅ 결론
| 상황 | 추천 방식 |
|---|---|
| 부모 클래스가 기본 생성자만 가질 때 | super() |
| 부모 클래스가 매개변수 생성자를 통해 필드 초기화하도록 설계되었을 때 | super(…) 사용하여 부모에게 초기화 책임 위임 |
| 항목 | 1번 방식 | 2번 방식 |
|---|---|---|
| 초기화 위치 | 자식 클래스 | 부모 클래스 |
| 권장도 | ❌ (비추) | ✅ (추천) |
| 객체지향 원칙 | 위반 가능성 있음 | 원칙에 부합 |
| 유지보수 | 어렵다 | 쉽다 |
| 가독성 | 다소 복잡 | 명확하고 간결 |
부모 생성자가 필드를 초기화하도록 설계되어 있다면,
자식 클래스는super(...)를 통해 부모의 생성자를 호출
하는 2번 방식이 더 바람직하다!
메소드 오버라이딩
<메소드를 오버라이딩할 때의 규칙>
✅ 왜 오버라이딩을 할까?
✅ 오버라이딩의 조건
| 조건 | 설명 |
|---|---|
| 메소드 이름 | 부모 메소드와 같아야 함 |
| 매개변수 | 개수, 순서, 타입 모두 같아야 함 |
| 리턴 타입 | 동일하거나, 더 좁은 타입(covariant) |
| 접근 제어자 | 부모보다 같거나 더 넓은 범위여야 함 (예: protected → public 가능) |
| 예외 | 부모보다 더 많은 예외를 던지면 안 됨 |
✅ 오버로딩(overloading)과 차이점
| 항목 | 오버라이딩 (Overriding) | 오버로딩 (Overloading) |
|---|---|---|
| 뜻 | 부모 클래스의 메소드를 재정의 | 같은 클래스 내에서 메소드 이름은 같고, 매개변수가 다르게 여러 개 정의 |
| 관계 | 상속 관계에서만 가능 | 상속 관계 필요 없음 |
| 메소드 이름 | 같음 | 같음 |
| 매개변수 | 완전히 같아야 함 (타입, 순서, 개수) | 다를 수 있음 (타입, 개수, 순서 중 하나 이상) |
| 리턴 타입 | 같거나, 부모보다 좁은 타입 | 달라도 상관 없음 |
| 사용 목적 | 부모의 기능을 자식이 바꾸고 싶을 때 | 같은 이름으로 다양한 인자 처리를 하고 싶을 때 |
| 실행 시점 | 런타임(실행 시간)에 결정됨 (동적 바인딩) | 컴파일 시간에 결정됨 (정적 바인딩) |
✅ 오버라이딩은 "상속받은 걸 바꾸는 것",
✅ 오버로딩은 "같은 이름으로 여러 개 만들기"
@Override // 컴파일 시 정확히 오버라이딩이 되었는지 체크 해줌
@Override 어노테이션
@Override를 붙이는 것이 좋음!부모 메소드 호출
super().메소드() : 자식 메소드 안에서 부모의 메소드를 명시적으로 호출하는 방법
final 클래스
public final class 클래스 { ... }
final 메소드
public final 리턴타입 메소드(매개변수, ...) { ... }
자동 타입 변환(업캐스팅, Upcasting)
용어 | 설명
자동 타입 변환 (업캐스팅) | 자식 → 부모로 자동 변환
언제? | 자식 객체를 부모 변수에 담을 때
왜? | 자식은 부모의 모든 특징을 물려받기 때문에, 부모처럼 행동할 수 있음
결과 | 부모 타입으로는 부모가 알고 있는 기능만 접근 가능
정보 손실? ❌ 실제 객체는 그대로
접근 제한? ✅ 부모 타입으로는 자식 기능에 접근 불가
자동 타입 변환 + 메소드 오버라이딩 -> 다형성
필드 다형성
class Animal {
void sound() {
System.out.println("동물이 소리를 냅니다");
}
}
class Dog extends Animal {
void sound() {
System.out.println("멍멍!");
}
}
class Cat extends Animal {
void sound() {
System.out.println("야옹~");
}
}
-> 하나의 타입(Animal)인데, 다른 행동(Dog, Cat의 sound)을 하는 것!
public class Main {
public static void main(String[] args) {
-> 상속 기반 참조 다형성 또는 업캐스팅 다형성
Animal myAnimal1 = new Dog(); // Animal 타입이지만 Dog 객체
Animal myAnimal2 = new Cat(); // Animal 타입이지만 Cat 객체
myAnimal1.sound(); // 멍멍!
myAnimal2.sound(); // 야옹~
}
}
매개변수 다형성
| 종류 | 설명 | 예시 |
|---|---|---|
| ① 매개변수 다형성(메소드 오버로딩) | 같은 이름의 메소드가 매개변수 개수나 타입에 따라 다르게 동작 | print(String s) vs print(int i) |
| ② 상속 기반 다형성(메소드 오버라이딩 + 업캐스팅) | 부모 타입 참조변수로 자식 객체를 다루고, 자식의 메소드를 호출 | Animal a = new Dog(); |
| ③ 필드 다형성 | 부모 타입의 필드로 자식 타입 객체를 할당하여 다양한 객체 저장 | Animal[] animals = {new Dog(), new Cat()}; |
instanceof 연산자
boolean result = 객체 instanceof 타입;
⇒ 객체가 특정 클래스의 인스턴스인지, 또는 그 클래스의 자식 클래스인지도 포함해서 확인해주는 논리 연산자
추상 클래스
추상 클래스 선언
추상 메소드와 재정의
abstract 리턴타입 메소드명(매개변수, ...);
sealed 클래스
❗️더보기
1. 추상 클래스 vs 인터페이스 차이 간단 비교
| 항목 | 추상 클래스 | 인터페이스 |
|---|---|---|
| 키워드 | abstract | interface |
| 다중 상속 | ❌ 안됨 (단일 상속만 가능) | ✅ 다중 구현 가능 |
| 생성자 | ✅ 가질 수 있음 | ❌ 없음 |
| 필드 | ✅ 일반 변수, 상태 보관 가능 | ✅ (Java 8+) 상수, static, default 메서드만 |
| 메서드 | ✅ 일반 메서드 + 추상 메서드 혼용 가능 | ✅ 추상 메서드(기본), default/static 사용 가능 |
| 사용 목적 | 공통 기능 제공 + 확장 | 행위 정의 (기능 명세) |
2. 추상 클래스 사용 시점 (언제 쓰면 좋을까?)
3. 템플릿 메서드 패턴과의 관계
abstract class Game {
abstract void start();
abstract void play();
abstract void end();
public final void run() {
start();
play();
end();
}
}
run()은 변하지 않는 공통 로직, 나머지는 자식이 구현4. 추상 클래스도 생성자는 가질 수 있다!
abstract class Animal {
String name;
Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
Dog(String name) {
super(name); // 부모 생성자 호출
}
}
5. 추상 클래스도 필드와 메서드 접근 제한자 사용 가능
private, protected, public 등 접근 제어자 모두 사용 가능