1. 상속
▶ 상속관계
상속은 객체 지향 프로그래밍의 요소 중 하나로, 기존 클래스의 필드와 메서드를 새로운 클래스에서 재사용하게 해 줌.
이름 그대로 기존 클래스의 속성과 기능을 그대로 물려받는 것.
상속을 사용하려면 extends 키워드를 사용하면 되고, 대상은 하나만 선택할 수 있음.
● 용어정리 ●
- 부모클래스(슈퍼클래스) : 상속을 통해 자신의 필드와 메서드를 다른 클래스에 제공하는 클래스.
- 자식클래스(서브클래스) : 부모 클래스로부터 필드와 메서드를 상속받는 클래스.
package extends1.ex1;
public class ElectricCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.ex1;
public class GasCar {
public void move() {
System.out.println("차를 이동합니다.");
}
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.ex1;
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();
}
}
실행결과
차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.
[ElecricCar]
전기차는 이동(move()), 충천(charge()) 기능이 있음.
[GasCar]
가솔린차는 이동(move()), 주유(fillUp()) 기능이 있음.
전기차와 가솔린차는 자동차(Car)의 좀 더 구체적인 개념.
자동차(Car)는 전기차와 가솔린차를 포함하는 추상적인 개념.
그래서 둘의 공통기능인 이동(move())가 있는데 이런 경우 상속 관계를 사용하는 것이 효과적.
package extends1.ex2;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
}
package extends1.ex2;
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.ex2;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.ex2;
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();
}
}
실행결과
차를 이동합니다.
충전합니다.
차를 이동합니다.
기름을 주유합니다.
[Car]
부모클래스.
자동차의 공통 기능인 move()가 포함되어 있음.
[ElectricCar]
extends Car를 사용해서 부모 클래스인 Car를 상속받음.
덕분에 move() 메서드 사용 가능.
[GasCar]
가솔린차도 전기차와 마찬가지로 extends Car를 사용해서 부모 클래스인 Car를 상속받음.
덕분에 move() 메서드 사용 가능.

전기차와 가솔린차가 Car를 상속받은 덕분에 electricCar.move(), gasCar.move()를 사용할 수 있음.
상속은 부모의 기능을 자식이 물려받는 것으로, 자식이 부모의 기능을 물려받아서 사용할 수 있음.
하지만 부모클래스는 자식클래스에 접근 불가능. 자식클래스는 부모클래스의 기능을 물려받기 때문에 접근 가능하지만 그 반대는 불가능.
(부모코드를 보면 자식에 대한 정보를 하나도 알 수 없고, 자식코드를 보면 부모가 누구인지 알 수 있음.)
▶ 단일상속
자바는 다중상속을 지원하지 않음.
그래서 extend 대상은 하나만 선택 가능.
부모를 하나만 선택할 수 있고, 부모가 또 다른 부모를 하나 가지는 것도 괜찮음.
만약 다중상속을 하게 되면 어떤 부모의 메서드 사용할지 모르는 애매한 문제가 발생하기 때문인데, 이러한 문제를 다이아몬드 문제라고 함.
▶ 상속과 메모리 구조
상속관계를 객체로 생성할 때 메모리 구조 알아보기.

new ElectricCar()를 호출하면 ElectricCar 뿐만 아니라 상속 관계에 있는 Car까지 포함해서 인스턴스를 생성함.
참조값은 하나이지만 실제로 그 안에서는 Car, ElectricCar라는 두 가지 클래스 정보가 공존함.
상속이라고 해서 단순하게 부모의 필드와 메서드만 물려받는 게 아니라 부모클래스도 함께 포함해서 생성하게 되는 것.
외부에서 볼 때는 하나의 인스턴스를 생성하는 것 같지만 내부에서는 부모와 자식이 모두 생성되고 공간도 구분됨.

electricCar.charge()를 호출하면 참조값을 확인해서 charge() 메서드 호출함.
따라서 참조값의 chage()를 호출하면 되는데, 상속관계의 경우에는 내부에 부모와 자식이 모두 존재함.
부모인 Car를 통해서 charge()를 찾을지 아니면 ElectricCar를 통해서 charge()를 찾을지 선택해야 함.
이때는 호출하는 변수의 타입(클래스)을 기준으로 선택해야 함.
electricCar 변수의 타입이 ElecricCar이므로 인스턴스 내부에 같은 타입인 ElectricCar를 통해 charge()를 호출함.
만약 부모에서도 해당 기능을 찾지 못하면 더 상위 부모에서 필요한 기능을 찾아보는데 계속 올라가면서 찾아봐도 없으면 컴파일 에러가 발생.
● 정리 ●
- 상속 관계의 객체를 생성하면 그 내부에는 부모와 자식이 모두 생성.
- 상속 관계의 객체를 호출할 때, 대상 타입을 정해야 함. 이때 호출자의 타입을 통해 대상 타입을 찾음.
- 현재 타입에서 기능을 찾지 못하면 상위 부모 타입으로 기능을 찾아서 실행하고, 기능을 찾지 못하면 컴파일 오류가 발생.
▶ 상속과 기능 추가
이번에는 상속 관계의 장점을 알아보기 위해, 상속 관계에 다음 기능을 추가해 보기.
- 모든 차량에 문 열기(openDoor()) 기능 추가
- 새로운 수소차(HydrogenCar)를 추가.
- 수소차는 fillHydrogen() 기능을 통해 수소 충전 가능
package extends1.ex3;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
package extends1.ex3;
public class ElectricCar extends Car {
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.ex3;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.ex3;
public class HydrogenCar extends Car {
public void fillHydrogen() {
System.out.println("수소를 충전합니다.");
}
}
package extends1.ex3;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
electricCar.charge();
electricCar.openDoor();
GasCar gasCar = new GasCar();
gasCar.move();
gasCar.fillUp();
gasCar.openDoor();
HydrogenCar hydrogenCar = new HydrogenCar();
hydrogenCar.move();
hydrogenCar.fillHydrogen();
hydrogenCar.openDoor();
}
}
실행결과
차를 이동합니다.
충전합니다.
문을 엽니다.
차를 이동합니다.
기름을 주유합니다.
문을 엽니다.
차를 이동합니다.
수소를 충전합니다.
문을 엽니다.
[Car]
모든 차량에 문 열기 기능을 추가할 때는 상위부모인 Car에 openDorr() 기능을 추가하면 됨.
이러면 Car의 자식들은 해당 기능을 모두 물려받게 됨.
상속관계가 아니었다면 각각의 차량에 해당 기능을 모두 추가해야 하는 불편함이 생김.
[HydrogenCa]
수소차 추가.
Car 상속받은 덕분에 move(), openDoor()와 같은 기능을 바로 사용 가능.

▶ 상속과 메서드 오버라이딩
부모 타입의 기능을 자식에서는 다르게 재정의 하고 싶을 수 있음.
예를 들어, 자동차의 경우 Car.move()라는 기능이 있는데 이 기능을 단순히 "차를 이동합니다."라고 출력한다면,
전기차의 경우 보통 더 빠르기 때문에 move()를 호출했을 때 "전기차를 빠르게 이동합니다."라고 출력을 변경하고 싶을 수 있음.
이렇게 부모에게 상속받은 기능을 자식이 재정의 하는 것을 메서드 오버라이딩이라고 함.
package extends1.overriding;
public class Car {
public void move() {
System.out.println("차를 이동합니다.");
}
// 추가
public void openDoor() {
System.out.println("문을 엽니다.");
}
}
package extends1.overriding;
public class GasCar extends Car {
public void fillUp() {
System.out.println("기름을 주유합니다.");
}
}
package extends1.overriding;
public class ElectricCar extends Car {
@Override
public void move() {
System.out.println("전기차를 빠르게 이동합니다.");
}
public void charge() {
System.out.println("충전합니다.");
}
}
package extends1.overriding;
public class CarMain {
public static void main(String[] args) {
ElectricCar electricCar = new ElectricCar();
electricCar.move();
GasCar gasCar = new GasCar();
gasCar.move();
}
}
실행결과
전기차를 빠르게 이동합니다.
차를 이동합니다.
[ElectricCar]
부모인 Car의 move() 기능을 그대로 사용하고 싶지 않고 변경함.
부모의 기능을 자식이 새로 재정의하는 것을 메서드 오버라이딩이라고 하는데,
이제는 ElectricCar의 move()를 호출하면 Car의 move()가 아니라 ElectricCar의 ove()가 호출됨.
@Overrice
@이 붙은 부분을 어노테이션이라고 하는데, 주석과 비슷하지만 프로그램이 읽을 수 있는 특별한 주석이라고 생각하면 됨.
[실행순서]
- electricCar.move()를 호출하면,
- 호출한 electricCar의 타입은 ElectricCar이므로 인스턴스 내부의 ElectricCar 타입에서 시작.
- ElectricCar 타입에 move() 메서드가 있으므로 해당 메서드 실행하게 되고, 실행할 메서드 찾았으므로 부모에서 찾지 않고 실행 종료.
▶ 오버로딩(Overloaing)과 오버라이딩(Overriding)
- 메서드 오버로딩 : 메서드 이름이 같고 매개변수(파라미터)가 다른 메서드를 여러 개 정의하는 것.
- 메서드 오버라이딩(=재정의) : 하위클래스에서 상위클래스의 메서드를 재정의하는 과정. 상속관계에서 사용하고, 부모의 기능을 자식이 다시 재정의 하는 것.
● 메서드 오버라이딩 조건 ●
- 메서드 이름이 같아야 함.
- 메서드 매개변수(파라미터) : 매개변수(파라미터) 타입, 순서, 개수가 같아야 함.
- 반환타입이 같아야 함. (반환타입이 하위 클래스 타입일 수 있음.)
- 접근제어자 : 오버라이딩 메서드의 접근 제어자는 상위 클래스의 메서드보다 더 제한적이어서는 안 됨.
- 예외 : 오버라이딩 메서드는 상위 클래스의 메서드보다 더 많은 체크 예외를 throws로 선언할 수 없음.
- static, final, private 키워드가 붙은 메서드는 오버라이딩 할 수 없음.
- static : 클래스 레벨에서 작동하므로 인스턴스 레벨에서 사용하는 오버라이딩이 의미가 없음. 쉽게 말해 그냥 클래스 이름을 통해 필요한 곳에 직접 접근하면 됨.
- final : 재정의 금지.
- private : 해당 클래스에서만 접근 가능하므로 하위클래스에서 보이지 않음. 따라서 오버라이딩 할 수 없음.
- 생성자 오버라이딩 : 생성자는 오버라이딩 불가능.
접근제어자 종류
- private : 모든 외부 호출을 막음.
- default : 같은 패키지안에서 호출은 허용.
- protected : 같은 패키지 안에서 호출은 허용. 패키지 달라도 상속 관계의 호출은 허용.
- public : 모든 외부 호출을 허용.
(private → default → protected → public)
+ : pubic
# : protected
~ : default
- : private
package extends1.access.parent;
public class Parent {
public int publicValue;
protected int protectValue;
int defaultValue;
private int privateValue;
public void publicMethod() {
System.out.println("Parent.publicMethod");
}
protected void protectedMethod() {
System.out.println("Parent.protectedMethod");
}
void defaultMethod() {
System.out.println("Parent.defaultMethod");
}
private void privateMethod() {
System.out.println("Parent.privateMethod");
}
public void printParent() {
System.out.println("==Parent 메서드 안==");
System.out.println("publicValue = " + publicValue);
System.out.println("protectedValue = " + protectValue);
System.out.println("defaultValue = " + defaultValue);
System.out.println("privateValue = " + privateValue);
// 부모 메서드 안에서 모두 접근 가능
defaultMethod();
privateMethod();
}
}
package extends1.access.child;
import extends1.access.parent.Parent;
public class Child extends Parent {
public void call() {
publicValue = 1;
protectValue = 1; // 상속 관계 or 같은 패키지 (여기서는 패키지는 다르지만 상속관계이므로 해당됨.)
//defaultValue = 1; // 다른 패키지 접근 불가. compile error
//privateValue = 1; // 접근 불가. compile error
publicMethod();
protectedMethod(); // 상속 관계 or 같은 패키지
//defaultMethod(); // 다른 패키지 접근 불가. compile error
//privateMethod(); // 접근 불가. compile error
printParent(); // 접근제어자가 public이기 때문.
}
}
package extends1.access;
import extends1.access.child.Child;
public class ExtendsAccessMain {
public static void main(String[] args) {
Child child = new Child();
child.call();
}
}
실행결과
Parent.publicMethod
Parent.protectedMethod
==Parent 메서드 안==
publicValue = 1
protectedValue = 1
defaultValue = 0
privateValue = 0
Parent.defaultMethod
Parent.privateMethod
[Parent]
부모클래스인 Parent에는 public, protected, default, private과 같은 모든 접근제어자가 필드와 메서드에 모두 존재.
둘의 패키지가 다름을 유의
[Child]
자식클래스인 Child에는 부모클래스인 Parent에 얼마나 접근할 수 있는지 보기.
- publicValue = 1 : 부모의 public 필드에 접근. public이므로 접근 가능.
- protectedValue = 1 : 부모의 protected 필드에 접근. 자식과 부모는 다른 패키지이지만, 상속관계이므로 접근 가능.
- defaultValue = 1 : 부모의 default 필드에 접근. 자식과 부모가 다른 패키지이므로 접근 불가능.
- privateValue = 1 : 부모의 private 필드에 접근. private은 모든 외부 접근을 막으므로 자식이라도 출 불가능.
[코드실행]
코드를 실행하면 Child.call() → Parent.printParent() 순서로 호출.
Child는 부모의 public, protected 필드나 메서드만 접근 가능.
반면에 Parent.printParent()의 경우 Parent 안에 있는 메서드이기 때문에 Parent 자신의 모든 필드와 메서드에 얼마든지 접근 가능.

본인 타입에 없으면 부모 타입에서 기능을 찾는데, 이 때 접근 제어자가 영향을 줌.
그 이유는 객체 내부에서는 자식과 부모가 구분되어 있기 때문임. 결국 자식 타입에서 부모 타입의 기능을 호출할 때, 부모 입장에서 보면 외부에서 호출한 것과 같기 때문.
▶ super_부모참조
부모와 자식의 필드명이 같거나 메서드가 오버라이딩 되어 있으면, 자식에서 부모의 필드나 메서드 호출 불가능.
이때 super 키워드 사용하면 부모를 참조할 수 있음. super는 이름 그대로 부모 클래스에 대한 참조를 나타냄.
package extends1.super1;
public class Parent {
public String value = "parent";
public void hello() {
System.out.println("Parent.hello");
}
}
package extends1.super1;
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); // this 생략 가능
System.out.println("super value = " + super.value);
this.hello(); // this 생략 가능
super.hello();
}
}
package extends1.super1;
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
[Child]
call() 메서드를 보면,
- this는 자기 자신의 참조를 뜻함. this는 생략 가능.
- super는 부모 클래스에 대한 참조를 뜻함.
- 필드 이름과 메서드 이름이 같지만 super를 사용해서 부모 클래스에 있는 기능을 사용할 수 있음.
실행결과 보면 super를 사용한 경우 부모 클래스의 기능을 사용한 것을 확인할 수 있음.
super는 본인 타입의 상위를, this는 본인타입을 나타냄.
▶ super_생성자
상속관계 인스턴스 생성하면 결국 메모리 내부에는 자식과 부모클래스가 각각 다 생성됨.
Child를 만들면 부모인 Parent까지 함께 만들어지는 것. 따라서 각각의 생상자도 모두 호출되어야 함.
상속 관계를 사용하면 자식 클래스의 생성자에서 부모 클래스의 생성자를 반드시 호출해야 함.(규칙)
package extends1.super2;
public class ClassA {
public ClassA() {
System.out.println("ClassA 생성자");
}
}
package extends1.super2;
public class ClassB extends ClassA{
public ClassB(int a) {
super(); // 기본생성자. 생략가능(기본생성자인 경우에만.)
System.out.println("ClassB 생성자 a = " + a);
}
public ClassB(int a, int b) {
super(); // 기본생성자. 생략가능.
System.out.println("ClassB 생성자 a = " + a + ", b = " + b);
}
}
package extends1.super2;
public class ClassC extends ClassB{
public ClassC() {
super(10, 20);
System.out.println("ClassC 생성자");
}
}
package extends1.super2;
public class Super2Main {
public static void main(String[] args) {
ClassC classC = new ClassC();
}
}
실행결과
ClassA 생성자
ClassB 생성자 a = 10, b = 20
ClassC 생성자
[ClassA]
최상의 부모 클래스
[ClassB]
ClassA 상속받음. 상속받으면 생성자 첫줄에 super(...)를 사용해서 부모 클래스의 생성자 호출해야 함.
(예외로 생성자 첫줄에 this(...)를 사용할 수 있지만 super(...)는 자식의 생성자 안에서 언젠가는 반드시 호출해야 함.)
부모클래스의 생성자가 기본생성자(파라미터가 없는)인 경우 super()를 생략할 수 있음.
- 상속관계에서 첫 줄에 super(...)를 생략하면 자바는 부모의 기본생성자를 호출하는 super()를 자동으로 생성해 줌.
- 참고로 기본 생성자를 많이 사용하기 때문에 편의상 이런 기능 제공하는 것.
[ClassC]
ClassB 상속받음. 여기서 ClassB는 생성자 두 개가 있음 → ClassB(int a) / ClassB(int a, int b)
생성자는 하나만 호출 가능. 여기서 super(10, 20)을 통해 부모클래스의 ClassB(int a, int b) 생성자를 선택함.
ClassC의 부모인 ClassB에는 기본생성자가 없으므로 부모의 기본 생성자를 호출하는 super()를 사용하거나 생략 가능.
[Super2Main]
실행해 보면 ClassA → ClassB → ClassC 순서로 실행되는 것을 알 수 있음.
생성자의 실행순서가 결과적으로 최상위 부모부터 실행되어서 하나씩 아래로 내려오는 것인데, 초기화는 최상이 부모부터 이루어짐.
그 이유는 자식 생성자의 첫 줄에서 부모의 생성자를 호출해야 하기 때문.
!정리해보면!
상속관계의 생성자 호출은 결과적으로 부모에서 자식 순서로 실행되는 것. 따라서 부모의 데이터를 먼저 초기화하고 그다음에 자식 데이터를 초기화해야 함.
상속관계에서 자식 클래스의 생성자 첫 줄에 반드시 super(...)를 호출해야 함. 단, 기본 생성자인 경우 생략 가능.
그리고 this(...)를 코드의 첫줄에 사용하더라도 반드시 한 번은 super(...)를 호출해야 함.
package extends1.super2;
public class ClassB extends ClassA{
public ClassB(int a) {
this(100, 0); // 기본생성자. 생략가능(기본생성자인 경우에만.)
System.out.println("ClassB 생성자 a = " + a);
}
public ClassB(int a, int b) {
super(); // 기본생성자. 생략가능.
System.out.println("ClassB 생성자 a = " + a + ", b = " + b);
}
}
package extends1.super2;
public class Super2Main {
public static void main(String[] args) {
//ClassC classC = new ClassC();
ClassB classB = new ClassB(100);
}
}
실행결과
ClassA 생성자
ClassB 생성자 a = 100, b = 0
ClassB 생성자 a = 100
1-1 상속문제
[Item, Book, Album, Movie, ShopMain]
코두 중복 없이 상속 관계 사용. 부모 클래스는 Item이라는 이름 사용하면 됨.
공통속성 : name, price
- Book : 저자(Author), isbn(isbn)
- Album : 아티스트(artist)
- Movie : 감독(director), 배우(actor)
package extends1.ex;
public class ShopMain {
public static void main(String[] args) {
Book book = new Book("JAVA", 10000, "han", "12345");
Album album = new Album("앨범1", 15000, "seo");
Movie movie = new Movie("영화1", 18000, "감독1", "배우1");
book.print();
album.print();
movie.print();
int sum = book.getPrice() + album.getPrice() + movie.getPrice();
System.out.println("상품 가격의 합 : " + sum);
}
}
실행결과
이름 : JAVA, 가격 : 10000
- 저자 : han, isbn : 12345
이름 : 앨범1, 가격 : 15000
- 아티스트 : seo
이름 : 영화1, 가격 : 18000
- 감독 : 감독1, 배우 : 배우1
상품 가격의 합 : 43000
package extends1.ex;
public class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public void print() {
System.out.println("이름 : " + name + ", 가격 : " + price);
}
public int getPrice() {
return price;
}
}
package extends1.ex;
public class Book extends Item {
private String author;
private String isbn;
public Book(String name, int price, String author, String isbn) {
super(name, price);
this.author = author;
this.isbn = isbn;
}
public void print() {
super.print();
System.out.println("- 저자 : " + author + ", isbn : " + isbn);
}
}
package extends1.ex;
public class Album extends Item{
private String artist;
public Album(String name, int price, String artist) {
super(name, price);
this.artist = artist;
}
public void print() {
super.print();
System.out.println("- 아티스트 : " + artist);
}
}
package extends1.ex;
public class Movie extends Item {
private String director;
private String actor;
public Movie(String name, int price, String director, String actor) {
super(name, price);
this.director = director;
this.actor = actor;
}
public void print() {
super.print();
System.out.println("- 감독 : " + director + ", 배우 : " + actor);
}
}
2. 정리
클래스와 메서드에 사용되는 fianl
1) 클래스에 fianl
- 상속 끝.
- final로 선언된 클래스는 확장될 수 없음. 다른 클래스가 fianl로 선언된 클래스를 상속받을 수 없음.
2) 메서드에 final
- 오버라이딩 끝.
- final로 선언된 메서드는 오버라이드 될 수 없음. 상속받은 서브 클래스에서 이 메서드를 변경할 수 없음.
'Studying > JAVA' 카테고리의 다른 글
[JAVA]_Polymorphism2(다형성2) (0) | 2025.03.19 |
---|---|
[JAVA]_Polymorphism1(다형성1) (0) | 2025.03.12 |
[JAVA]_final (0) | 2025.02.18 |
[JAVA]_자바 메모리 구조와 static (1) | 2025.02.11 |
[JAVA]_Access Modifier(접근제어자) (0) | 2025.01.31 |