상속(inheritance)이란?
기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것
상속이란 객체지향의 4대 핵심 개념 중 하나로, 상속을 통해 클래스를 작성하면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 용이하다.
상속을 구현하는 방법은 새로 작성하고자 하는 클래스의 이름 뒤에 extends 키워드를 붙이고, 상속받고자 하는 클래스의 이름을 쓰면 된다.
class Child extends Parent { }
이때 상속해 주는 Parent 클래스를 '조상 클래스'라 부르고, 상속을 받는 Child 클래스를 '자손 클래스'라고 한다.
아래는 조상 클래스와 자손 클래스를 표현하는 다양한 방법이다.
조상 클래스: 부모(parent) 클래스, 상위(super) 클래스, 기반(base) 클래스
자손 클래스: 자식(child) 클래스, 하위(sub) 클래스, 파생된(derived) 클래스
위와 같이 자손 클래스가 조상 클래스를 상속받으면, 자손 클래스는 자동으로 조상 클래스의 모든 멤버를 자신의 멤버로 가지게 된다. 다시 말해 자손 클래스가 조상 클래스를 포함하는 모습이 된다.
* 이때 모든 멤버란 생성자와 초기화 블럭을 제외한 모든 변수나 메서드를 말한다.
class Parent {
int age;
}
class Child extends Parent { }
예를 들어 위와 같은 코드가 있을 때, Child 클래스에는 아무런 멤버가 없지만 Parent 클래스를 상속받았기 때문에 Parent 클래스의 멤버인 age를 자연스럽게 사용할 수 있게 된다. 이를 다이어그램으로 표현하면 다음과 같다.
이때 Child 클래스에 새로운 멤버를 추가하더라도 조상인 Parent 클래스는 아무런 영향을 받지 않는다.
즉 조상 클래스에 변화를 주게 되면 자손 클래스도 영향을 받지만, 반대로 자손 클래스에 주는 변화는 조상 클래스에 영향을 주지 않는다는 것이다.
이를 통해 자손 클래스는 조상 클래스를 바탕으로 자유롭게 자신의 멤버를 늘려 나갈 수 있고, 이러한 형태가 마치 조상 클래스를 확장해 나가는 것과 같아서 상속을 할 때 extends라는 키워드를 사용하는 것이다.
* 위와 같은 이유로 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
여러 클래스 간의 상속
상속은 단지 1대1 관계만을 가지는 것은 아니다. 하나의 조상 클래스로부터 여러 자손 클래스가 상속을 받을 수도 있고, 자손 클래스를 상속받은 자손 클래스가 조상 클래스의 멤버를 사용할 수도 있다.
우선 하나의 조상 클래스로부터 여러 자손 클래스가 상속받는 경우는 주로 여러 클래스들이 공통적으로 가져야 하는 멤버가 있을 때이다. 공통된 멤버라면 각 클래스에 따로 선언하는 것보다는 조상 클래스를 만들고 이를 상속받는 것이 코드의 중복을 줄이고 코드를 변경하는 데 더 유리하다. 이때 각 클래스는 조상 클래스와는 상속 관계, 다시 말해 부모와 자식의 관계를 가지지만, 형제 관계라는 개념은 존재하지 않아서 자손 클래스는 서로 관계가 존재하지 않는다.
다음으로 자손 클래스의 자손 클래스까지 상속을 중첩하는 경우는 주로 계층적인 구조를 표현하고자 할 때 사용한다. 예를 들어 리트리버를 상속 관계로 표현하면 다음과 같다.
Retriever 클래스는 자신의 조상 클래스들이 가진 모든 멤버를 상속받는데, 이때 Dog 클래스는 Retriever 클래스의 직접 조상이라고 하고, Animal 클래스는 Retriever 클래스의 간접 조상이라고 한다.
이렇듯 상속 관계를 잘 활용하면 코드를 쉽게 확장할 수 있고, 조상 클래스의 변경으로 자손 클래스까지 영향을 줄 수 있으므로 유지보수도 용이해지는 효과를 볼 수 있다.
클래스 간의 관계 설정
지금까지는 상속의 이해를 돕기 위해 마치 상속 관계가 포함 관계와 동일한 것처럼 표현했지만, 실제로 포함 관계를 맺어주는 방법은 따로 존재한다. 어떠한 클래스 사이에 포함 관계를 맺어 주고 싶다면, 한 클래스의 멤버 변수로 다른 클래스의 참조변수를 선언하면 된다.
class Circle {
Point p = new Point(); // 점의 좌표
int r; // 반지름
}
그렇다면 상속 관계와 포함 관계는 무슨 차이가 있을까?
상속 관계 vs 포함 관계
상속 관계는 'A는 B이다(is-a)' 관계이고,
포함 관계는 'A는 B를 가지고 있다(has-a)' 관계이다.
위에서 예시로 들었던 리트리버의 경우, '리트리버는 개를 가지고 있다'보다는 '리트리버는 개이다'가 더 적합한 표현일 것이다. 때문에 Dog 클래스와 Retriever 클래스를 상속 관계로 묶어준 것이다.
반대로 '원은 점이다'보다는 '원은 점을 가지고 있다'가 더 적합한 표현이기 때문에, 앞선 예시에서 Circle 클래스의 멤버로 Point 클래스의 참조변수를 선언해서 포함 관계로 묶어준 것이다.
단일 상속(single inheritance)
C++과 같은 다른 객체지향 언어에서는 여러 조상 클래스를 상속받는 '다중 상속(multiple inheritance)'이 가능하지만, Java에서는 오직 하나의 조상 클래스만 상속받는 '단일 상속'만을 허용한다.
// 아래와 같이 코드를 작성하면 에러가 발생한다.
class Child extends Parent1, Parent2 { }
다중 상속은 여러 클래스를 상속받을 수 있기 때문에 복합적인 기능을 가진 클래스를 쉽게 만들 수 있다는 장점이 있지만, 클래스의 관계가 복잡해지고 조상 클래스 멤버의 이름이 겹칠 경우 구분할 수 없다는 문제를 가지고 있다.
Java는 다중 상속의 확장성을 포기하는 대신, 클래스 간의 명확한 관계와 중복 코드의 발생 가능성을 줄이는 것을 택하였기 때문에 단일 상속만을 허용하는 것이다.
super와 super()
super란?
super는 this와 비슷한 개념으로, 부모 클래스(직접 조상)의 인스턴스를 가리키는 참조변수이다. this가 지역변수와 인스턴스변수가 같은 이름을 가지고 있을 때 두 변수를 구분하기 위해 사용했다면, super는 자신의 인스턴스변수와 부모의 인스턴스변수가 같은 이름을 가지고 있을 때 두 변수를 구분하기 위해 사용한다.
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
void print() {
System.out.println(x); // 20
System.out.println(thils.x); // 20
System.out.println(super.x); // 10
}
}
또한 변수뿐만 아니라 부모 클래스의 메서드를 오버라이딩 했다면, super를 통해 부모 클래스의 원본 메서드를 호출하는 것도 가능하다. 오버라이딩에 대한 내용은 아래의 포스트에서 확인할 수 있다.
오버라이딩
오버라이딩(Overriding)이란? 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것 일반적으로 새로운 클래스를 생성할 때 상속받은 메서드를 그대로 사용하기 보다는, 새로운 클래스 자신의
doshiwa-dev.tistory.com
super()란?
super()도 마찬가지로 this()와 비슷한 개념으로, 조상 클래스의 생성자를 호출하는 데 사용된다. 또한 super()도 생성자이기 때문에 일반적인 생성자의 제약 조건을 똑같이 따른다. 때문에 반드시 하나 이상의 생성자가 필요한데 위에서 클래스에 일일이 super()를 호출하지 않은 이유는 기본 생성자와 마찬가지로 개발자가 직접 호출하지 않으면 컴파일러가 자동으로 호출해주기 때문이다.
사실 상속받은 조상 클래스의 인스턴스를 자손 클래스가 자유롭게 사용할 수 있었던 이유도 자손 클래스가 생성될 때 super()를 통해 조상 클래스의 인스턴스가 같이 생성되기 때문이며, 이렇게 생성된 두 인스턴스는 하나의 인스턴스로 합쳐져서 사용된다.
이와 같은 조상 클래스의 생성자 호출은 모든 클래스의 최고 조상인 Object 클래스의 생성자를 만날 때까지 계속 이어지고, 이로 인해 Object 클래스를 제외한 모든 클래스의 생성자는 반드시 첫 줄에 자신의 다른 생성자 또는 조상 클래스의 생성자를 호출하게 된다. 코드로 살펴보면 아래와 같다.
class Child extends Parent {
// 만약 생성자 호출을 생략하더라도
Child() {
super();
} // 컴파일러가 자동으로 위와 같은 코드를 추가해준다.
}
이때 주의할 점은 컴파일러가 자동으로 호출해주는 것은 조상 클래스의 '기본 생성자'이므로, 만약 조상 클래스에 기본 생성자가 없다면 컴파일 에러가 발생할 수 있다.