10. 상속(Inheritance)
1. 키워드
- 상속(Inheritance)
- 부모 클래스(Parent Class), 상위 클래스(Super Class), 기초 클래스(Base Class)
- 자식 클래스(Child Class), 하위 클래스(Sub Class), 파생 클래스(Derived Class)
Object
클래스super
와super()
- 메서드 오버라이딩(Method Overriding)
2. 상속(Inheritance)의 개념
- 상속이란 기존의 클래스에 기능을 추가하거나 재정의하여 새로운 클래스를 정의하는 것을 의미한다.
- 이러한 상속은 캡슐화, 추상화와 더불어 OOP를 구성하는 중요한 특징 중 하나이다.
- 상속을 이용하면 기존에 정의되어 있는 모든 필드와 메서드를 물려받아, 새로운 클래스를 생성할 수 있다.
- 이때 기존에 정의되어 있던 클래스를 부모 클래스(Parent Class) 또는 상위 클래스(Super Class), 기초 클래스(Base Class)라고도 한다.
- 그리고 상속을 통해 새롭게 작성되는 클래스를 자식 클래스(Child Class) 또는 하위 클래스(Sub Class), 파생 클래스(Derived Class)라고도 한다.
1) 상속의 장점
- 자바에서 클래스의 상속은 다음과 같은 장점을 가진다.
1] 기존에 작성된 클래스를 재활용할 수 있다.
2] 자식 클래스 설계 시 중복되는 멤버를 미리 부모 클래스에 작성해 놓으면, 자식 클래스에서는 해당 멤버를 작성하지 않아도 된다.
3] 클래스 간의 계층적 관계를 구성함으로써 다형성의 문법적 토대를 마련한다.
2) 자식 클래스(Child Class)
- 자식 클래스란 부모 클래스의 모든 특성을 물려받아 새롭게 작성된 클래스를 의미한다.
- 자바에서 자식 클래스는 다음과 같은 문법을 통해 선언한다.
- 다음 그림은 부모 클래스와 자식 클래스 간의 포함 관계를 나타낸 그림이다.
- 이처럼 부모 클래스는 자식 클래스에 포함된 것으로 볼 수 있다.
- 따라서 부모 클래스에 새로운 필드를 하나 추가하면, 자식 클래스에도 자동으로 해당 필드가 추가된 것처럼 동작한다.
- 자식 클래스에는 부모 클래스의 필드와 메서드만이 상속되며, 생성자와 초기화 블록은 상속되지 않는다.
- 또한, 부모 클래스의 접근 제어가
private
이나default
로 설정된 멤버는 자식 클래스에서 상속받지만 접근할 수는 없다.
class Parent {
private int a = 10; // private 필드
public int b = 20; // public 필드
}
class Child extends Parent {
public int c = 30; // public 필드
void display() {
① System.out.println(a); // 상속받은 private 필드 참조 -> 컴파일 오류 발생
② System.out.println(b); // 상속받은 public 필드 참조
③ System.out.println(c); // 자식 클래스에서 선언한 public 필드 참조
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
// 20
// 30
- 위 예제의 ②번 라인에서는 자식 클래스의 메서드에서 부모 클래스에서 상속받은
public
필드를 참조하고 있다. - 이처럼 자식 클래스에서 따로 선언하지 않은 필드라도 해당 이름의 필드를 부모 클래스에서 상속받았다면 문제가 없다.
- 하지만 ①번 라인처럼 해당 필드가 부모 클래스의
private
필드라면 접근할 수 없으므로, 주석 처리를 하지 않으면 컴파일 시 오류를 발생시킬 것이다. - 또한 자식 클래스에서는 ③번 라인처럼 자신만의 필드나 메서드를 선언하여 사용할 수 있다.
Note
- 자바에서 클래스는 단 한 개의 클래스만을 상속받는 단일 상속만 가능하다.
3) Object
클래스
- 자바에서
Object
클래스는 모든 클래스의 부모 클래스가 되는 클래스이다. - 따라서 자바의 모든 클래스는 자동으로
Object
클래스의 모든 필드와 메서드를 상속받게 된다. - 즉, 자바의 모든 클래스는 별도로
extends
키워드를 사용하여Object
클래스의 상속을 명시하지 않아도Object
클래스의 모든 멤버를 자유롭게 사용할 수 있다. - 자바의 모든 객체에서
toString()
이나clone()
과 같은 메서드를 바로 사용할 수 있는 이유가 해당 메서드들이Object
클래스의 메서드이기 때문이다.
3. super
와 super()
1) super
키워드
super
키워드는 부모 클래스로부터 상속받은 필드나 메서드를 자식 클래스에서 참조하는 데 사용하는 참조 변수이다.- 인스턴스 변수의 이름과 지역 변수의 이름이 같을 경우 인스턴스 변수 앞에
this
키워드를 사용하여 구분할 수 있었다. - 이와 마찬가지로 부모 클래스의 멤버와 자식 클래스의 멤버 이름이 같을 경우
super
키워드를 사용하여 구별할 수 있다. - 이렇게 자바에서는
super
참조 변수를 사용하여 부모 클래스의 멤버에 접근할 수 있다. this
와 마찬가지로super
참조 변수를 사용할 수 있는 대상도 인스턴스 메서드뿐이며, 클래스 메서드에서는 사용할 수 없다.
class Parent {
int a = 10;
}
class Child extends Parent {
void display() {
System.out.println(a); // 10
System.out.println(this.a); // 10
System.out.println(super.a); // 10
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
- 위의 예제에서
int
형 변수a
는 부모 클래스인Parent
클래스에서만 선언되어 있다. - 따라서 지역 변수와
this
참조 변수 그리고super
참조 변수 모두 같은 값을 출력한다.
class Parent {
int a = 10;
}
class Child extends Parent {
int a = 20;
void display() {
System.out.println(a); // 20
System.out.println(this.a); // 20
System.out.println(super.a); // 10
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
- 하지만 위의 예제에서
int
형 변수a
는 자식 클래스인Child
클래스에서도 선언되어 있다. - 따라서 지역 변수와
this
참조 변수는 자식 클래스에서 대입된 값을 출력하며,super
참조 변수만이 부모 클래스에서 대입된 값을 출력하게 된다.
2) super()
메서드
this()
메서드가 같은 클래스의 다른 생성자를 호출할 때 사용된다면,super()
메서드는 부모 클래스의 생성자를 호출할 때 사용된다.- 자식 클래스의 인스턴스를 생성하면, 해당 인스턴스에는 자식 클래스의 고유 멤버뿐만 아니라 부모 클래스의 모든 멤버까지도 포함되어 있다.
- 따라서 부모 클래스의 멤버를 초기화하기 위해서는 자식 클래스의 생성자에서 부모 클래스의 생성자까지 호출해야만 한다.
- 이러한 부모 클래스의 생성자 호출은 모든 클래스의 부모 클래스인
Object
클래스의 생성자까지 계속 거슬러 올라가며 수행된다.
- 따라서 자바 컴파일러는 부모 클래스의 생성자를 명시적으로 호출하지 않는 모든 자식 클래스의 생성자 첫 줄에 자동으로 다음과 같은 명령문을 추가하여, 부모 클래스의 멤버를 초기화할 수 있도록 해준다.
- 하지만 자바 컴파일러는 컴파일 시 클래스에 생성자가 하나도 정의되어 있지 않아야만, 자동으로 기본 생성자를 추가해준다.
- 만약 다음 예제처럼 부모 클래스에 매개변수를 가지는 생성자를 하나라도 선언했다면, 부모 클래스에는 기본 생성자가 자동으로 추가되지 않을 것이다.
- 따라서 다음 예제처럼
Parent
클래스를 상속받은 자식 클래스에서super()
메서드를 사용하여 부모 클래스의 기본 생성자를 호출하게 되면, 오류가 발생하게 될 것이다.
class Parent {
int a;
Parent(int n) {
a = n;
}
}
class Child extends Parent {
int b;
Child() {
super(); // 오류 발생
b = 20;
}
}
- 왜냐하면 부모 클래스인
Parent
클래스에는 기본 생성자가 추가되어 있지 않기 때문이다.
- 따라서 매개변수를 가지는 생성자를 선언해야 할 경우에는 되도록이면 다음 예제처럼 기본 생성자까지 명시적으로 선언하는 것이 좋다.
class Parent {
int a;
Parent() { // 매개변수를 가지지 않는 기본 생성자
a = 10;
}
Parent(int n) { // 매개변수를 가지는 생성자 -> 기본 생성자를 명시적으로 선언해야 함
a = n;
}
}
class Child extends Parent {
int b;
Child() {
super(); // 매개변수를 가지지 않는 기본 생성자 호출
b = 20;
}
}
- 다음 예제는
super()
메서드가 어떻게 호출되는지를 보여주는 예제이다.
class Parent {
int a;
Parent() {
a = 10;
}
Parent(int n) {
a = n;
}
}
class Child extends Parent {
int b;
Child() {
① // super(40);
b = 20;
}
void display() {
System.out.println(a);
System.out.println(b);
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
// 10
// 20
- 위의 예제를 그냥 실행하면, 자바 컴파일러는 주석 처리된 ①번 라인에 자동으로
super();
구문을 삽입할 것이다. - 따라서 변수
a
는10
으로 초기화된다.
class Parent {
int a;
Parent() {
a = 10;
}
Parent(int n) {
a = n;
}
}
class Child extends Parent {
int b;
Child() {
① super(40);
b = 20;
}
void display() {
System.out.println(a);
System.out.println(b);
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
}
}
// 40
// 20
- 하지만 ①번 라인의 주석 처리를 해제하고 실행하면, 부모 클래스인
Parent
클래스는 두 번째 생성자에 의해 초기화될 것이다. - 따라서 변수
a
는40
으로 초기화된다.
4. 메서드 오버라이딩(Method Overriding)
- 앞서 배운 오버로딩(Overloading)이란 서로 다른 시그니처를 갖는 여러 메서드를 하나의 이름으로 정의하는 것이었다.
- 오버라이딩이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메서드를 자식 클래스에서 같은 시그니처를 갖는 메서드로 다시 정의하는 것이라고 할 수 있다.
- 자바에서 자식 클래스는 부모 클래스의
private
멤버를 제외한 모든 메서드를 상속받는다. - 이렇게 상속받은 메서드는 그대로 사용해도 되고, 필요한 동작을 위해 재정의하여 사용할 수도 있다.
- 즉, 메서드 오버라이딩이란 상속받은 부모 클래스의 메서드를 재정의하여 사용하는 것을 의미한다.
1) 메서드 오버라이딩의 조건
- 자바에서 메서드를 오버라이딩하기 위한 조건은 다음과 같다.
1] 오버라이딩이란 메서드의 동작만을 재정의하는 것이므로, 메서드의 선언부는 기존 메서드와 완전히 같아야 한다. 하지만 메서드의 반환 타입은 부모 클래스의 반환 타입으로 타입 변환할 수 있는 타입이라면 변경할 수 있다.
2] 부모 클래스의 메서드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다.
3] 부모 클래스의 메서드보다 더 큰 범위의 예외를 선언할 수 없다.
- 자바에서는 메서드 오버라이딩을 통해 상속받은 부모 클래스의 메서드를 자식 클래스에서 직접 재정의할 수 있다.
- 다음 예제는 부모 클래스인
Parent
클래스의display()
메서드를 자식 클래스인Child
클래스에서 오버라이딩하는 예제이다.
class Parent {
void display() {
System.out.println("부모 클래스의 display() 메서드입니다.");
}
}
class Child extends Parent {
void display() {
System.out.println("자식 클래스의 display() 메서드입니다.");
}
}
class Test {
public static void main(String[] args) {
Parent pa = new Parent();
pa.display(); // 부모 클래스의 display() 메서드입니다.
Child ch = new Child();
ch.display(); // 자식 클래스의 display() 메서드입니다.
Parent pc = new Child();
pc.display(); // 자식 클래스의 display() 메서드입니다.
Child cp = new Parent(); // 컴파일 오류 발생
cp.display(); // 컴파일 오류 발생
}
}
- 위의 예제에서 세 번째와 같은 인스턴스의 참조가 허용되는 이유는 바로 자바에서의 다형성(Polymorphism) 때문이다.
2) 오버로딩과 오버라이딩
- 간단히 정의하면 오버로딩은 새로운 메서드를 정의하는 것이다.
- 하지만 오버라이딩은 상속받은 기존의 메서드를 재정의하는 것이다.
- 다음 예제는 부모 클래스인
Parent
클래스의display()
메서드를 자식 클래스인Child
클래스에서 오버라이딩과 오버로딩을 둘 다 수행하는 예제이다.
class Parent {
void display() {
System.out.println("부모 클래스의 display() 메서드입니다.");
}
}
class Child extends Parent {
void display() { // 오버라이딩된 display() 메서드
System.out.println("자식 클래스의 display() 메서드입니다.");
}
void display(String str) { // 오버로딩된 display() 메서드
System.out.println(str);
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
ch.display("오버로딩된 display() 메서드입니다.");
}
}
// 자식 클래스의 display() 메서드입니다.
// 오버로딩된 display() 메서드입니다.
3) @Override
어노테이션
- 메서드 오버라이딩을 통해 상속받은 부모 클래스의 메서드를 자식 클래스에서 직접 재정의할 때 개발자도 사람인지라 철자에 대한 실수를 하기 마련이다.
- 이러한 실수를 방지할 목적으로 자바에서는
@Override
어노테이션을 지원한다.
class Parent {
void display() {
System.out.println("부모 클래스의 display() 메서드입니다.");
}
}
class Child extends Parent {
@Override // 어노테이션 사용
void display() { // 오버라이딩된 display() 메서드
System.out.println("자식 클래스의 display() 메서드입니다.");
}
// @Override -> 오버로딩에는 어노테이션 사용 안 함
void display(String str) {
System.out.println(str);
}
}
class Test {
public static void main(String[] args) {
Child ch = new Child();
ch.display();
ch.display("오버로딩된 display() 메서드입니다.");
}
}
// 자식 클래스의 display() 메서드입니다.
// 오버로딩된 display() 메서드입니다.