[JAVA] 상속
상속이란?
상속(Inheritance)
은 부모 클래스의 필드와 메서드를 자식 클래스가 물려받아 사용하는 기능이다.- 중복 코드를 줄이고, 공통된 기능을 재사용할 수 있음
extends
키워드를 사용하여 상속- 자식 클래스는 부모의 기능을 사용할 수 있지만, 부모는 자식의 기능을 모름
- 자바는 단일 상속만 허용함 (하나의 부모만 상속 가능)
상속을 왜 쓰는지?
전기차와 가솔린차 클래스가 있다고 해보자. 각각 move()
라는 공통 기능이 있음에도 따로 구현되어 있다면, 중복을 줄이고 구조를 더 깔끔하게 만들 방법이 필요함. 바로 이럴 때 상속이 등장한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ElectricCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
public class GasCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
}
}
차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.
move() ← 코드 중복이 확인된다.
이런경우 상속을 사용하는 것이 좋다. 부모 클래스 Car
을 만들어 자식 클래스에서 상속 받으면 된다.
상속 적용
1
2
3
4
5
6
//부모 클래스
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//자식 클래스가 부모 클래스를 상속 받음 (extend)
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
//자식 클래스가 부모 클래스를 상속 받음 (extend)
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
전기차와 가솔린차가 Car 를 상속 받은 덕분에 electricCar.move() , gasCar.move() 를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
ElectricCar electricCar = new ElectricCar();
electricCar.move(); // 부모의 move() 호출
electricCar.charge();
GasCar gasCar = new GasCar();
gasCar.move(); // 부모의 move() 호출
gasCar.fillUp();
차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.
- 부모 클래스 (Super Class): 상속을 제공하는 클래스 (
Car
) - 자식 클래스 (Sub Class): 상속을 받는 클래스 (
ElectricCar
,GasCar
) - 자식 —> 부모 호출은 가능
-
부모 —> 자식 호출은 불가능
(
Car
클래스는ElectricCar
나GasCar
를 모른다!)
자바는 다중 상속을 허용하지 않음
다중 상속을 허용하면 move() 같은 메서드가 어떤 부모의 것을 써야 할지 모호해짐 → 다이아몬드 문제
그래서 자바는 하나의 클래스만 상속 가능. 물론 부모가 또 다른 부모를 하나 가지는 것은 괜찮다.
대신 인터페이스는 여러 개 구현 가능해서, 다중 상속의 일부 장점을 인터페이스로 보완한다.
상속과 메모리 구조
1
ElectricCar electricCar = new ElectricCar();
ElectricCar
인스턴스를 생성하면, 내부적으로는 부모 클래스인Car
까지 포함되어 생성된다.- 하나의 참조값(
x001
) 안에 부모(Car) + 자식(ElectricCar) 구조가 겹쳐져 존재한다.
electricCar
의 타입은ElectricCar
ElectricCar
내부에서charge()
메서드를 찾음- 자식 클래스에
charge()
존재 → 바로 실행
ElectricCar
내부에move()
없음- 상속 관계에 따라 부모 클래스(Car) 로 올라감
Car
에move()
존재 → 부모의 메서드 실행
자식 → 부모 순서대로 메서드 탐색
상속 시 객체 구조 : 부모 + 자식 하나의 인스턴스에 같이 생성
메서드 탐색 순서 : 자식 → 부모 → 조부모..
참조 변수 타입 : 메서드 탐색 기준이 됨 ElectricCar electricCar
호출 불가능한 경우 : 컴파일 에러
상속과 기능 추가
1
2
3
4
5
6
7
8
9
10
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 공통 기능 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
새로운 자식 클래스 추가
1
2
3
4
5
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ElectricCar electricCar = new ElectricCar();
electricCar.move(); // 부모 기능
electricCar.charge(); // 자식 고유 기능
electricCar.openDoor(); // 부모에 추가된 새 기능
HydrogenCar hydrogenCar = new HydrogenCar();
hydrogenCar.move();
hydrogenCar.fillHydrogen();
hydrogenCar.openDoor();
차를 이동합니다.
충전합니다.
문을 엽니다.
차를 이동합니다.
수소를 충전합니다.
문을 엽니다.
상속과 메서드 오버라이딩(Overriding)
부모에게 상속받은 메서드를 자식 클래스에서 새로운 방식으로 재정의하는 것
1
2
3
4
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
- 부모
Car
의move()
는 “차를 이동합니다.” - 자식
ElectricCar
에서move()
를 오버라이딩 → “전기차를 빠르게 이동합니다.”
부모클래스
1
2
3
4
5
6
7
8
9
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
자식클래스
1
2
3
4
5
6
7
8
9
10
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
메인
1
2
3
4
ElectricCar electricCar = new ElectricCar();
electricCar.move(); // 오버라이딩된 메서드 호출
전기차를 빠르게 이동합니다.
electricCar
는ElectricCar
타입ElectricCar
내부에서move()
찾음 → 있음 → 그 메서드 실행- 부모까지 올라가지 않음
@Override 애노테이션
- 상위 클래스의 메서드를 정확히 오버라이드하는지 컴파일 타임에 체크
- 실수 방지 및 코드 명확성 향상
- 안 붙여도 되지만 무조건 붙이는 습관 권장
1
2
3
4
@Override
public void move() {
// ...
}
오버라이딩 조건
부모 메서드와 같은 메서드를 오버라이딩 할 수 있다 정도로 이해하면 충분하다.
- 메서드 이름: 부모와 같아야 함
- 매개변수: 타입, 개수, 순서 동일
- 반환 타입: 동일 or 하위 타입
-
접근 제어자: 더 좁으면 안 됨
상위 클래스의 메서드가 protected 로 선언되어 있으면 하위 클래스에서 이를 public 또는 protected 로 오버라이드할 수 있지만, private 또는 default 로 오버라이드 할 수 없다.
- 예외: 더 많은 체크 예외 선언 ❌
static
,final
,private
메서드: 오버라이딩 ❌- 생성자: 오버라이딩 불가
@Override
를 반드시 붙이자!
상속과 접근제어자
상속 관계에서 자식 클래스는 부모 클래스의 필드와 메서드에 접근 제어자에 따라 접근할 수 있다.
접근 제어자는 정보 은닉과 캡슐화의 핵심!
접근 제어자 | 의미 | 같은 클래스 | 같은 패키지 | 자식 클래스(다른 패키지) | 외부 |
---|---|---|---|---|---|
private |
완전 차단 | ✅ | ❌ | ❌ | ❌ |
(default) | 패키지 전용 | ✅ | ✅ | ❌ | ❌ |
protected |
패키지 + 상속 허용 | ✅ | ✅ | ✅ | ❌ |
public |
전부 허용 | ✅ | ✅ | ✅ | ✅ |
부모 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Parent {
public int publicValue;
protected int protectedValue;
int defaultValue;
private int privateValue;
public void publicMethod() { ... }
protected void protectedMethod() { ... }
void defaultMethod() { ... }
private void privateMethod() { ... }
public void printParent() {
// 모든 필드/메서드 접근 가능
}
}
자식 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Child extends Parent {
public void call() {
publicValue = 1; // 가능
protectedValue = 1; // 가능 (상속 관계이므로)
// defaultValue = 1; // ❌ 다른 패키지 접근 불가
// privateValue = 1; // ❌ 접근 불가
publicMethod(); // 가능
protectedMethod(); // 가능
// defaultMethod(); // ❌
// privateMethod(); // ❌
printParent(); // public 메서드는 호출 가능
}
}
Parent.publicMethod
Parent.protectedMethod
==Parent 메서드 안==
publicValue = 1
protectedValue = 1
defaultValue = 0
privateValue = 0
Parent.defaultMethod
Parent.privateMethod
필드/메서드 | 자식 클래스에서 접근 | 이유 |
---|---|---|
public |
✅ 가능 | 외부 포함 모두 접근 가능 |
protected |
✅ 가능 | 상속 관계이므로 가능 |
default (패키지 전용) |
❌ 불가 | 다른 패키지라서 불가 |
private |
❌ 불가 | 클래스 내부에서만 사용 가능 |
- 자식 클래스는 부모의
public
,protected
만 접근 가능 default
,private
는 자식이라도 다른 패키지면 접근 불가- 부모 클래스 내부에서는 자신의 모든 멤버에 자유롭게 접근 가능
public
, protected
만 접근 가능하며, 부모 클래스 내부에서는 자신의 모든 필드와 메서드에 접근 할 수 있다.
접근제어자 UML 표기법
+
: public#
: protected~
: default (package-private)-
: private
접근제어자와 메모리 구조
상속 관계에서 객체가 생성되면, 부모 클래스와 자식 클래스가 하나의 인스턴스 안에 함께 존재 하지만 메모리 내부에서는 구분 되어 있다.
자식에서 부모의 기능을 호출하는 건 부모 입장에서는 외부에서 접근하는 것과 같음
즉, 아무리 내부에 있어도
private
,default
는 부모 외부 접근 차단이므로 → 자식 클래스에서 접근 불가protected
,public
만 접근 가능
super
부모의 필드/메서드 참조
자식 클래스에서 부모의 필드나 메서드와 이름이 같을 때, 부모 쪽 기능을 호출하고 싶을 때 사용
부모 클래스
1
2
3
4
5
6
public class Parent {
public String value = "parent";
public void hello() {
System.out.println("Parent.hello");
}
}
자식 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Child extends Parent {
public String value = "child";
@Override
public void hello() {
System.out.println("Child.hello");
}
public void call() {
System.out.println("this value = " + this.value); // child
System.out.println("super value = " + super.value); // parent
this.hello(); // Child.hello
super.hello(); // Parent.hello
}
}
메인 클래스
1
2
3
4
5
6
7
8
9
10
11
12
public class Super1Main {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
-- 실행결과 --
this value = child
super value = parent
Child.hello
Parent.hello
super
는 부모 클래스의 필드나 메서드를 참고할 때 사용!
이름이 겹칠 경우, 자식이 우선이므로 super
로 명시적으로 부모 접근 필요!
부모 생성자 호출
자식 객체를 생성하면, 부모 객체도 함께 생성됨 따라서 자식 생성자에서 반드시 부모 생성자도 호출해야 함
생성자 규칙
- 자식 생성자의 첫 줄에서
super(...)
를 호출해야 함 - 생략 시 → 컴파일러가 자동으로
super()
추가 - 부모에 기본 생성자 없으면, 반드시 명시적으로
super(...)
호출해야 함
1
2
3
4
5
public class ClassA {
public ClassA() {
System.out.println("ClassA 생성자"); //5
}
}
1
2
3
4
5
public class ClassB extends ClassA {
public ClassB(int a, int b) { //3
super(); // 부모의 기본 생성자 호출 4
System.out.println("ClassB 생성자 a=" + a + " b=" + b); //6
}
1
2
3
4
5
6
public class ClassC extends ClassB {
public ClassC() {
super(10, 20); // 부모 생성자 직접 선택 2
System.out.println("ClassC 생성자"); //7
}
}
1
2
3
4
5
6
7
8
9
10
public class Super2Main {
public static void main(String[] args) {
ClassC classC = new ClassC(); //1
}
}
-- 출력 결과 --
ClassA 생성자
ClassB 생성자 a=10 b=20
ClassC 생성자
ClassC()
가 호출됨- → 가장 먼저
super(10, 20)
호출 →ClassB(int a, int b)
- → 그 안에서 다시
super()
호출 →ClassA()
this(…) 와 super(…)의 관계
- 생성자에서
this(...)
로 같은 클래스의 다른 생성자 호출 가능 - 하지만 결국에는 한 번은 반드시
super(...)
호출해야 함
1
2
3
4
5
6
7
8
9
10
11
12
public class ClassB extends ClassA {
public ClassB(int a) {
this(a, 0); // 자기 생성자 호출
System.out.println("ClassB 생성자 a=" + a);
}
public ClassB(int a, int b) {
super(); // 부모 생성자 호출
System.out.println("ClassB 생성자 a=" + a + " b=" + b);
}
}
1
2
3
4
5
6
7
8
9
10
public class Super2Main {
public static void main(String[] args) {
ClassB classB = new ClassB(100);
}
}
-- 출력 결과 --
ClassA 생성자
ClassB 생성자 a=100 b=0
ClassB 생성자 a=100
this(...)
는 같은 클래스의 다른 생성자 호출super(...)
는 부모 클래스 생성자 호출- 생성자 호출은 반드시 최상위 부모 → 자식 순서로 끝까지 호출됨
정리
- 상속은 부모 클래스의 필드와 메서드를 자식 클래스가 물려받아 사용하는 기능
- 중복 코드를 줄이고, 공통된 기능을 효율적으로 재사용할 수 있음
- 상속 구조에서는 객체 내부에 부모와 자식이 함께 생성되며, 메서드 탐색은 자식 → 부모 순서로 이루어짐
- 상속을 통해 기능을 편리하게 확장할 수 있고, 오버라이딩을 통해 자식만의 동작을 정의할 수 있음
- 자식 클래스는 부모의
public
,protected
멤버만 접근 가능 super
키워드를 사용하면 부모의 필드, 메서드, 생성자에 접근 가능함
상속은 자바 객체지향의 핵심 기초 개념 중 하나이며, 앞으로 배울 다형성, 추상 클래스, 인터페이스 개념의 기반이 된다.
출처: 김영한의 실전 자바 - 기본편