Do You Coding?
[Java] 상속 (Inheritance) 본문

Java 상속(Inheritance) 완전 정리
1. 상속이란?
상속(Inheritance)은 OOP의 핵심 개념 중 하나다. (OOP Is A PIE — Polymorphism, Inheritance, Encapsulation)
기존(상위) 클래스의 자산(멤버)을 자식(하위) 클래스에서 재사용하기 위한 것이다.
- 상위 클래스의 생성자와 초기화 블록은 상속하지 않는다.
- 상위 클래스의 멤버를 물려받기 때문에 코드가 절감된다.
- 상위 클래스의 코드를 변경하면 모든 하위 클래스에도 적용되므로 유지 보수성이 향상된다.
1-1. 상속의 적용
extends 키워드를 사용하여 상속을 선언한다.
public class Person {
String name;
void eat() {}
void jump() {}
}
// SpiderMan은 Person을 상속받는다
public class SpiderMan extends Person {
boolean isSpider;
void fireWeb() {}
}
SpiderMan이 가진 멤버는 총 5개다. 고유 멤버 2개(isSpider, fireWeb()) + 상속받은 멤버 3개(name, eat(), jump()).
상속 관계는 is a 관계(또는 kind of)라고도 한다. 예) SpiderMan is a Person.
1-2. Object 클래스
모든 클래스의 조상 클래스는 Object이다.
- 별도의
extends선언이 없는 클래스는 자동으로extends Object가 생략되어 있다. - 따라서 모든 클래스에는
Object클래스에 정의된 메서드가 있다.
2. 단일 상속과 포함 관계
2-1. 단일 상속 (Single Inheritance)
다중 상속의 경우 여러 클래스의 기능을 물려받을 수 있으나 매우 복잡해진다.
예를 들어, 동일한 이름의 메서드가 두 상위 클래스에 모두 있다면 하위 클래스는 어떤 메서드를 써야 할지 모호해진다.
Spider Person
+jump(): void +jump(): void
+fireWeb(): void +eat(): void
\ /
\ /
SpiderMan ← jump()가 겹친다!
그래서 Java에서는 다중 상속을 지원하지 않고, 단일 상속만 지원한다.
대신 interface와 포함 관계(has a)로 단점을 극복한다.
2-2. 포함 관계 (has a)
상속 이외에 클래스를 재활용하는 방법이다. 2개 이상의 클래스에서 특성을 가져올 때,
하나는 상속하고 나머지는 멤버 변수로 포함시킨다.
// Person을 상속하고, Spider는 멤버 변수로 포함
public class SpiderMan2 extends Person {
Spider spider = new Spider(); // 포함 관계
boolean isSpider;
void fireWeb() { /* ... */ }
void jump() {
if (isSpider) {
spider.jump(); // Spider의 jump 사용
} else {
System.out.println("두 다리로 힘껏 점프");
}
}
}
3. Sealed Class
"나는 자식을 선택할 수 없는가?" 라는 질문에서 출발한 개념이다.
봉인된 클래스로, 내가 관리하는 특정 클래스에게만 상속을 허용할 수 있다.
3-1. 작성법
class(interface 포함)에sealed키워드를 사용하고, 상속을 허락하는 클래스를permits뒤에 나열한다.sealed가 선언된 클래스는permits가 반드시 필요하다.- 구현 클래스는 다음 키워드 중 하나를 반드시 사용해야 한다.
| 키워드 | 의미 |
|---|---|
sealed |
여전히 봉인된 클래스로, 추가적으로 permits로 하위 클래스 나열 필요 |
final |
더 이상 상속 받을 수 없는 클래스 |
non-sealed |
봉인이 해제된 클래스로, 자유롭게 상속 가능 |
3-2. 코드 예시
// sealed 클래스: Algo, CS, Java만 상속 가능
sealed class SealedStudyGroup permits Algo, CS, Java {
public void study() {
System.out.println("공부 공부 공부!!");
}
}
// final: 더 이상 상속 불가
final class Algo extends SealedStudyGroup {}
// sealed: sealed 상태에서 상속 가능 (Spring만 허용)
sealed class Java extends SealedStudyGroup permits Spring {}
// non-sealed: 이후로는 자유롭게 상속 가능
non-sealed class CS extends SealedStudyGroup {}
// non-sealed의 하위 클래스는 자유롭게 생성 가능
class Network extends CS {}
// sealed Java의 유일한 허용 하위 클래스
final class Spring extends Java {}
4. 메서드 오버라이딩
조상 클래스에 정의된 메서드를 자식 클래스에서 적합하게 재정의하는 것이다.
4-1. 오버라이딩의 조건
- 메서드 이름이 같아야 한다.
- 매개 변수의 개수, 타입, 순서가 같아야 한다.
- 리턴 타입이 같아야 한다.
- 접근 제한자는 부모보다 범위가 넓거나 같아야 한다.
- 조상보다 더 큰 예외를 던질 수 없다.
4-2. 오버라이딩 예시
public class Person {
void jump() {
System.out.println("두 다리로 힘껏 점프"); // 물려받은 jump(). 성능이 좋지 않다.
}
}
public class Spider {
void jump() {
System.out.println("키 * 1000만큼 엄청난 점프"); // 탐나는 jump()를 가지고 있다.
}
}
public class SpiderMan2 extends Person {
Spider spider = new Spider();
boolean isSpider;
void fireWeb() { /* ... */ }
void jump() { // Person의 jump()를 오버라이딩
if (isSpider) {
spider.jump();
} else {
System.out.println("두 다리로 힘껏 점프");
}
}
}
5. super 키워드
this를 통해 멤버에 접근했듯이, super를 통해 조상 클래스의 멤버에 접근할 수 있다.super.을 이용해 조상의 메서드를 호출하면 조상의 코드를 재사용할 수 있다.
// super 사용 전
void jump() {
if (isSpider) {
spider.jump();
} else {
System.out.println("뛰기"); // 직접 작성
}
}
// super 사용 후 — 조상의 코드를 재사용
void jump() {
if (isSpider) {
spider.jump();
} else {
super.jump(); // Person의 원래 jump() 호출
}
}
5-1. 변수의 scope
변수는 사용된 위치에서 점점 확장해가며 처음 만난 선언부에 연결된다. 탐색 순서는 다음과 같다.
method 내부 → 해당 클래스 멤버 변수 → 조상 클래스 멤버 변수
class Parent {
String x = "parent";
}
class Child extends Parent {
String x = "child";
void method() {
String x = "method";
System.out.println("x : " + x); // "method" (메서드 내부 지역변수)
System.out.println("this.x : " + this.x); // "child" (Child 클래스의 멤버 변수)
System.out.println("super.x : " + super.x); // "parent" (Parent 클래스의 멤버 변수)
}
}
public class ScopeTest {
public static void main(String[] args) {
Child child = new Child();
child.method();
}
}
만약 method의
x와 Child의x가 모두 없으면, 세 출력 모두 Parent의"parent"가 출력된다.
5-2. super() 생성자
this()가 해당 클래스의 다른 생성자를 호출하듯,super()는 조상 클래스의 생성자를 호출한다.- 조상 클래스에 선언된 멤버들은 조상 클래스의 생성자에서 초기화하고,
자식 클래스에서 선언된 멤버들은 자식 클래스 생성자에서 초기화한다. super()는 자식 클래스 생성자의 맨 첫 줄에서만 호출 가능하다.
super()와this()생성자는 하나만 첫 줄에 쓸 수 있다. 동시에 사용할 수 없다!
- 명시적으로
this()또는super()를 호출하지 않는 경우, 컴파일러가 자동으로super()를 삽입한다. - 결과적으로 맨 상위의
Object까지 각각의 생성자가 만들어지는 구조다.
class Person2 {
String name;
Person2(String name) {
// super(); → Object의 기본 생성자 호출 (컴파일러가 자동 삽입)
this.name = name;
}
}
public class SpiderMan3 extends Person2 {
Spider spider;
boolean isSpider;
SpiderMan3(String name, Spider spider, boolean isSpider) {
super(name); // 반드시 부모의 생성자를 호출해야 한다
this.spider = spider;
this.isSpider = isSpider;
}
SpiderMan3(String name) {
this(name, new Spider(), true); // 같은 클래스의 다른 생성자 호출
}
public static void main(String[] args) {
SpiderMan3 sman = new SpiderMan3("피터 파커");
}
}
부모 클래스에 기본 생성자(매개변수 없는 생성자)가 없으면,
자식 클래스에서super()에 인자를 넣어 부모의 생성자를 명시적으로 호출해야 한다. 그렇지 않으면 컴파일 오류가 발생한다.
public class A extends Object {
// 기본 생성자가 없으므로, SubA에서 super(0)으로 호출해야 한다
public A(int a) {
super();
}
}
class SubA extends A {
SubA() {
super(0); // 부모가 가진 생성자를 호출
}
}
6. Annotation
6-1. Annotation이란?
- 사전적 의미는 주석이다.
- 사람이 아닌 컴파일러, JVM, 프레임워크 등이 보는 주석이다.
- 소스 코드에 메타 데이터를 삽입하는 형태로, 소스 코드에 붙여 놓은 라벨이라고 보면 된다.
- 코드에 대한 정보를 추가하여 소스 코드의 구조 변경, 환경 설정 정보 추가 등의 작업을 진행할 수 있다.
6-2. 기본 Annotation의 예
| Annotation | 설명 |
|---|---|
@Deprecated |
컴파일러에게 해당 메서드가 deprecated 되었다고 알려준다. (취소선이 그어진다. 쓸 수는 있지만 쓰지 말 것) |
@Override |
컴파일러에게 해당 메서드는 override한 메서드임을 알려준다. 반드시 super class에 선언된 메서드여야 한다. |
@SuppressWarnings |
컴파일러에게 사소한 warning을 신경 쓰지 말라고 알려준다. |
@Override를 붙이면, 재정의가 제대로 안 된 경우 오류로 표시해준다. 사용을 적극 권장한다!@Override를 빼면 오버라이딩이 아닌 새로운 메서드 정의로 인식되어 오류가 발생하지 않는다.
7. Package와 Import
7-1. Package
- PC에서 많은 파일을 관리할 때, 유사한 목적의 파일을 폴더 기준으로 작성하고 계층적으로 접근하는 것처럼,
프로그램의 많은 클래스도 패키지를 이용하여 관리한다. - 패키지의 이름은 의미 있는 이름으로 만들고, 통해 계층적 접근을 한다.
- 물리적으로 패키지는 클래스 파일을 담고 있는 디렉터리다.
package name+class name으로 클래스를 구분한다. 이것을 fully qualified name이라고 한다.
package com.doyoucode.day03; // 가장 첫 번째 문장에 하나의 패키지만 선언
- 주석, 공백을 제외한 첫 번째 문장에 하나의 패키지만 선언해야 한다.
- 모든 클래스는 반드시 하나의 패키지에 속한다.
- 생략 시 default package에 속하는데, 가급적 사용하지 않는다.
.java파일 위에package가 써 있으면 컴파일 시 물리적인 폴더가 만들어진다.
패키지명에 쓰지 말아야 하는 것: 예약어, 키워드,
java,javax,oracle,example등.
도메인을 거꾸로 쓰는 것을 권장한다. (예:com.doyoucode.day03)
7-2. Import
다른 패키지에 선언된 클래스를 사용하기 위한 키워드다. 패키지 선언과 클래스 선언 사이에 위치한다.
- 패키지와 달리 여러 번 선언 가능하다.
import java.io.InputStream; // 네이밍 기법: 특정 클래스 하나만 import
import java.util.*; // 그룹핑 기법: 해당 패키지의 클래스를 import (하위 패키지는 포함하지 않음)
java.util.*은 util 패키지의 모든 클래스를 가져오는 것이 아니라, 실제로 소스에서 사용한 것만 가져온다.java.lang.*은 자동으로 import 된다. (System,String등 자주 쓰이는 클래스)
import한 package의 클래스 이름이 동일하여 구분해야 할 때는, 클래스 이름 앞에 전체 패키지 명을 입력한다.
java.util.List list = new java.util.ArrayList(); // fully qualified name으로 구분
Eclipse에서는 Ctrl + Shift + O로 자동 import를 사용할 수 있다!
static import
import static을 사용하면 클래스명 없이 static 멤버를 바로 사용할 수 있다.
import static java.lang.Math.PI; // Math.PI 대신 PI로 바로 사용 가능
8. 제한자 (Modifier)
클래스, 변수, 메서드 선언부에 함께 사용되어 부가적인 의미를 부여하는 키워드다.
8-1. 제한자의 종류
| 구분 | 종류 |
|---|---|
| 접근 제한자 | public, protected, (default = package), private |
| 그 외 제한자 | static, final, abstract, synchronized 등 |
- 하나의 대상에 여러 제한자를 조합 가능하나, 접근 제한자는 하나만 사용 가능하다.
- 순서는 무관하지만, 일반적으로 접근 제한자를 맨 앞으로 작성한다.
8-2. 접근 제한자 (Access Modifier)
멤버 등에 사용되며 해당 요소를 외부에서 사용할 수 있는지를 설정한다.
| 한정자 | 클래스 내부 | 동일 패키지 | 하위 클래스 | 그 외의 영역 |
|---|---|---|---|---|
public |
O | O | O | O |
protected |
O | O | O | X |
(default) |
O | O | X | X |
private |
O | X | X | X |
접근 영역 크기: public > protected > default > private
protected는 하위 클래스에서 접근 가능하다.default는 하위 클래스에서 접근할 수 없고,
같은 패키지 내에서만 접근 가능하다. 이 차이를 잘 구분해야 한다.
접근 제한자의 용도별 사용 가능 범위는 다음과 같다.
| 제한자 | 클래스 | 생성자 | 멤버 | 같은 클래스 | 같은 패키지 | 다른 패키지의 자손 클래스 | 전체 |
|---|---|---|---|---|---|---|---|
public |
O | O | O | O | O | O | O |
protected |
O | O | O | O | O | ||
(default) |
O | O | O | O | O | ||
private |
O | O | O |
접근 제한자와 오버라이딩
메서드를 오버라이딩할 때, 부모의 제한자 범위와 같거나 넓은 범위로만 사용 가능하다.
class Parent {
protected void method() {}
}
public class OverrideRule extends Parent {
// void method() {} // default → 범위가 좁아지므로 불가!
protected void method() {} // protected → 같은 범위이므로 가능
// public void method() {} // public → 더 넓은 범위이므로 가능
}
9. final 키워드
"마지막, 더 이상 바뀔 수 없음" 이라는 의미의 키워드다.
9-1. final class
더 이상 확장할 수 없다. 즉, 상속 금지 → 오버라이드 방지다.
final class PerfectClass { /* ... */ }
// 컴파일 오류! final class는 상속할 수 없다
// public class FinalClassTest extends PerfectClass { }
대표적인 final class:
String,Math등
9-2. final method
더 이상 재정의할 수 없다. 즉, 오버라이딩 금지다.
class ParentClass {
public final void finalMethod() {}
}
public class FinalClassTest extends ParentClass {
// 컴파일 오류! final method는 override할 수 없다
// public void finalMethod() {}
}
9-3. final variable
더 이상 값을 바꿀 수 없다.
public void finalParameterTest(final String name) {
System.out.println(name);
// name = "hong"; // 컴파일 오류! final 변수는 재할당 불가
}
9-4. Blank final
값이 할당되지 않은 final 멤버 변수다.
final멤버 변수에 초기 값이 할당되어 버리면, 모든 객체는 같은 값을 사용해야 한다.- 객체가 생성되면 값을 변경할 기회가 없기 때문에, 반드시 생성자에서 1회 초기화가 가능하다.
- 즉, 생성자에서 초기화를 강제하는 역할을 한다.
public class BlankFinalTest {
private final String bloodType; // Blank final: 선언 시 값을 할당하지 않음
public BlankFinalTest(String type) {
this.bloodType = type; // 생성자에서 최초 1회 할당 → 오류 없음
}
}
final변수는 값이 바뀌지 않으므로 대문자 + 언더바로 작성하는 것을 권장한다. (예:MAX_NUMBER)
9-5. static final
- 단지
final만 있으면, 객체마다 같은 값으로 공용성이 없다. - 진정한 상수는 객체와 무관하게 모두가 공용하는 값이다. (
Math.PI,Math.E등)
public class A extends Object {
static final int MAX_NUMBER = 100; // 바로 할당하는 것이 맞다
}
10. 일반적인 클래스 레이아웃
package structure; // 패키지 선언부
import java.io.*; // 외부 패키지 import
public class ClassStructure { // class 선언부
String name; // 멤버 변수
int age;
{ name = "andy"; } // 초기화 블록
public ClassStructure(String name, int age) { // 생성자
this.name = name;
this.age = age;
}
public void setName(String name) { // 멤버 메서드
this.name = name;
}
public static void main(String[] args) {
ClassStructure cs = new ClassStructure("hong", 10);
}
}
핵심 정리
- 상속은
extends키워드로 선언하며, 상위 클래스의 멤버를 재사용할 수 있다. 생성자와 초기화 블록은 상속되지 않는다. - Java는 단일 상속만 지원한다. 다중 상속의 모호함은 interface와 포함 관계(has a)로 해결한다.
- Sealed class를 사용하면 상속을 허용할 클래스를 명시적으로 제한할 수 있다.
- 오버라이딩 시 메서드 시그니처가 동일해야 하며, 접근 제한자는 부모보다 좁아질 수 없다.
- super 키워드로 조상 클래스의 멤버에 접근하고, super() 생성자로 조상의 생성자를 호출한다.
생성자 첫 줄에만 쓸 수 있으며,this()와 동시 사용은 불가하다. - Annotation은 컴파일러에게 보여주는 주석이다.
@Override는 오버라이딩이 올바르게 되었는지 검증해주므로 적극 사용하자. - package는 클래스의 물리적 그룹핑이며,
import로 다른 패키지의 클래스를 사용한다.java.lang.*은 자동 import 된다. - 접근 제한자는
public > protected > default > private순으로 범위가 넓다.protected는 하위 클래스 접근 가능,default는 같은 패키지만 접근 가능하다. - final은 class(상속 금지), method(오버라이딩 금지), variable(재할당 금지)에 사용된다.
Blank final은 생성자에서 초기화를 강제한다.
'Backend > Java' 카테고리의 다른 글
| [Java] 추상화 (Abstraction) (0) | 2026.03.17 |
|---|---|
| [Java] Java 기본 문법 - 변수, 타입, 배열, 문자열 (0) | 2026.03.17 |