Java에서 상속은 extends 키워드를 사용하여
단순히 부모 클래스를 상속하는 것처럼 보이지만 내부에는 복잡한 사정이 있다.
그 복잡한 사정을 알아보면, 상속을 더 깊이 이해할 수 있다.
상속의 대표적인 규칙으로는
자식 클래스의 생성자 첫 줄에 부모 클래스 생성자를 호출해야 한다는 규칙이 있다.
왜 굳이 부모 클래스를 생성해야 할까?
참고로 부모 생성자는 다음과 같이 생성할 수 있다.
super(parameter1, ...)
class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extends Parent {
public void childMethod() {
// 메서드 바디
}
}
public void classMethod() {
Child child = new Child();
child.parentMethod(); // 부모 객체 메서드 호출
위 코드에서 parentMethod()를 수행하기 위해서는,
자식 객체에 해당 메서드가 없으므로, 부모 객체를 탐색해서 메서드가 있으면 호출한다.
즉, 탐색할 부모 객체가 있어야 한다는 것이다.
따라서 자식 객체가 생성자를 통해 생성될 때, 부모의 객체가 먼저 생성되어야 한다.
class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extends Parent {
public Child() {
super(); // ***추가***
}
public void childMethod() {
// 메서드 바디
}
}
public void classMethod() {
Child child = new Child();
child.parentMethod(); // 부모 객체 메서드 호출
그리고 자식 객체에 메서드가 없는 경우,
부모 객체를 탐색할 때 혼선을 방지하기 위하여
Java는 다중 상속이 불가능하다.
class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extends Parent {
int number;
public Child() {
this(10); // ** 첫 줄에 부모 생성자가 아닌 this() 메서드 호출 가능 **
super(); // ** 하지만 그 후에 super() 메서드는 반드시 호출 **
}
// 생성자 오버로딩
public Child(int number) {
this.number = number;
}
}
this() 메서드를 사용할 경우, 꼭 부모 생성자가 첫 줄에 등장할 필요는 없다.
하지만 생성자 내부에서 언젠가는 반드시 부모 객체를 생성해주어야 한다.
[A]
class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extends Parent {
public void childMethod() {
// 메서드 바디
}
}
================================
[B]
class Parent {
public void parentMethod() {
// 메서드 바디
}
}
class Child extends Parent {
public Child() {
super(); // ***추가***
}
public void childMethod() {
// 메서드 바디
}
}
사실 A와 B는 서로 같은 코드이다.
클래스는 기본 생성자를 자동으로 생성한다.
그리고 부모 클래스가 기본 생성자만을 가지고 있는 경우, super() 생략이 가능하다.
A는 B에 추가된 코드가 생략된 것이다.
class Parent {
int number;
// ** 부모 생성자가 기본 생성자가 아닌 경우 **
public Parent(int number) {
this.number = number;
}
}
class Child extends Parent {
public Child() {
super(20); // ** super()가 자동 생성되지 않으므로 반드시 명시 **
}
}
호출 객체의 필드나 메서드가 호출되면, 호출한 객체를 먼저 탐색한다고 했다.
그렇다면 부모 동일한 필드 이름과 메서드가 있다면 어떻게 될까?
항상 호출한 객체를 먼저 접근하기 때문에 부모의 메서드를 사용할 수 없다.
(호출한 객체가 부모일 경우, 부모에는 자식 객체에 대한 정보가 없다.
따라서 당연히 자식 객체에 접근할 수 없다.)
필드나 메서드 이름이 동일한데 부모 객체의 필드나 메서드를 이용하고 싶은 경우,
super 키워드를 사용하여 호출해야 한다.
class Parent {
int number;
public void sameMethod() {
System.out.println("부모 메서드 입니다.");
}
}
class Child extends Parent {
int number;
@Overriding
public void sameMethod() {
System.out.println("부모 메서드이지만 자식 메서드에서 재정의된 메서드 입니다.");
}
public void extendMethod() {
this.number = 1; // 자신 객체의 number 필드 접근
super.number = 2; // 부모 객체의 number 필드 접근
this.sameMethod(); // 자신 객체의 메서드 접근
super.sameMethod(); // 부모 객체의 메서드 접근
}
}
자식 클래스에서는 부모 클래스의 접근 제어자 중
어디까지 사용할 수 있을까?
package extend.parent
class Parent {
public int publicNum;
default int defaultNum;
protected int protectedNum;
private int privateNum;
}
==============================
package extend.child
class Child extends Parent {
publicNum = 1; // 접근 가능
defaultNum = 1; // 패키지가 다르지만 상속 관계로 접근 가능
protectedNum; = 1; // 접근 불가
privateNum = 1; // 접근 불가
}
참고로 오버로딩과 헷갈려서는 안된다.
오버라이딩이란, 부모의 메서드를 재정의하는 것을 말한다.
class Parent {
public void parentMethod() {
System.out.println("부모 메서드 입니다.");
}
}
class Child extends Parent {
@Overriding // 오버라이딩
public void parentMethod() {
System.out.println("부모 메서드이지만 자식 메서드에서 재정의된 메서드 입니다.");
}
}
public void extendMethod() {
Child child = new Child();
child.parentMethod(); // 출력 : 부모 메서드이지만 자식 메서드에서 재정의된 메서드 입니다.
final 키워드는 재정의할 수 없다는 것을 나타낸다.
따라서 당연하게도 다른 클래스가 final 클래스를 상속하여 사용할 수 없다.
하지만, final 클래스가 다른 클래스를 상속 받아 사용하는 것은 가능하다.
final 메서드는 재정의가 불가능 하므로 오버라이딩할 수 없다.
Java에서 상속은 아주 강력한 기능이기 때문에 깊게 이해해두어야 한다.
상속함으로써 발생하는 내부 구조를 이해하고,
오버라이딩의 규칙에 대해 고민하면
좀 더 깔끔한 상속 관계를 설계하는 것에 도움이 되겠다.