자바의 상속에 대해 학습하세요
학습할 것
- 자바 상속의 특징
- super 키워드
- 메소드 오버라이딩
- 다이나믹 메소드 디스패치 (Dynamic method Dispatch)
- 추상 클래스
- final 키워드
- Object 클래스
상속은 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해서 클래스를 작성하면 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다.
자바에서 상속의 구현은 다음과 같다. 새로 작성하고자 하는 클래스의 이름 뒤에 상속받고자 하는 클래스의 이름을 키워드 ‘extends’와 함께 써 주기만 하면 된다.
class Child extends Parent {
// ...
}
새로 작성하려는 클래스는 Child이고 기존 클래스는 Parent이다. 두 클래스는 서로 상속 관계에 있다고 하며, 상속해주는 클래스(Parent)를 ‘조상 클래스’라 하고 상속 받는 클래스(Child)를 ‘자손 클래스’라고 한다.
자손 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에 조상 클래스에 멤버변수가 추가되면 자손 클래스에 자동적으로 멤버변수가 추가된 것과 같은 효과를 얻는다.
반대로 자손 클래스에 새로운 무언가가 추가되어도 조상 클래스에는 아무런 영향을 주지 않는다.
자손 클래스는 조상 클래스의 모든 멤버를 상속받으므로 항상 조상클래스보다 같거나 많은 멤버를 가진다. 즉, 상속을 거듭할수록 상속받는 클래스의 멤버 개수는 점점 늘어나게 된다.
상속을 받는다는 것은 조상 클래스를 확장(extends)한다는 의미로 해석할 수도 있으며 상속에 사용되는 키워드가 'extends'인 이유이기도 하다.
다음과 같이 하나의 조상 클래스와 다수의 자손 클래스가 있다.
class Parent { }
class Child1 extends Parent { }
class Child2 extends Parent { }
클래스 Child1과 Child2가 모두 Parent클래스를 상속받고 있으므로 Parent와 Child1, Parent와 Child2는 서로 상속관계에 있지만, 자손 클래스 간에는 아무런 관계도 성립하지 않는다. 클래스 간의 관계에서 형제 관계와 같은 것은 없다.
Child1 클래스로부터 상속받는 GrandChild라는 새로운 클래스를 추가해보자.
class Parent { }
class Child1 extends Parent { }
class Child2 extends Parent { }
class GrandChild extends Child1 { }
자손 클래스는 조상 클래스의 모든 멤버를 물려받으므로 GrandChild 클래스는 Child1 클래스의 모든 멤버와 Parent 클래스로부터 상속받은 멤버까지 상속받게 된다. 즉, GrandChild 클래스는 Parent 클래스와 간접적인 상속관계를 가지게 된다.
다른 객체지향언어인 C++에서는 여러 조상 클래스로부터 상속받는 다중상속을 허용하지만 자바에서는 단일 상속만을 허용한다.
class Child extends Father, Mother { // Error. 조상은 하나만 허용
// ...
}
다중상속을 허용하면 여러 클래스로부터 상속받을 수 있기 때문에 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있다는 장점이 있지만, 클래스간의 관계가 복잡해지고 서로 다른 클래스로부터 상속받은 멤버의 이름이 같은 경우 구별할 수 있는 방법이 없다는 단점을 가지고 있다.
자바에서는 다중상속의 이러한 문제점을 해결하기 위해 다중상속의 장점을 포기하고 단일상속만을 허용한다.
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다. 멤버변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 클래스에 정의된 멤버의 이름이 같을 때는 super를 붙여서 구별할 수 있다.
조상 클래스로부터 상속받은 멤버도 자손 클래스 자신의 멤버이므로 this를 사용할 수 있다. 조상의 멤버와 자신의 멤버를 구별하는데 사용된다는 점을 제외하고 super와 this는 근본적으로 같다. 모든 인스턴스메소드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조변수인 this와 super의 값이 된다.
static 메소드(클래스 메소드)는 인스턴스와 관련이 없기 때문에 this와 마찬가지로 super 역시 static 메소드에서는 사용할 수 없고 인스턴스 메소드에서만 사용할 수 있다.
조상 클래스에 선언된 멤버변수와 같은 이름의 멤버변수를 자손 클래스에서 중복해서 정의하는 것이 가능하며 참조변수 super를 이용해서 서로 구별할 수 있다.
class App {
public static void main(String[] args) {
Child c = new Child();
c.method;
}
}
class Parent {
int x = 10;
}
class Child extends Parent {
int x = 20;
void method() {
System.out.println("x = " + x);
System.out.println("this.x = " + this.x);
System.out.println("super.x = " + super.x);
}
}
위 예제에서 super.x는 조상 클래스로부터 상속받은 멤버변수 x를 뜻하며, this.x는 자손 클래스에 선언된 멤버변수를 뜻한다.
변수만이 아니라 메소드 역시 super를 써서 호출할 수 있다. 조상 클래스의 메소드를 자손 클래스에서 오버라이딩한 경우에 super를 사용한다.
class Point {
int x;
int y;
String getLocation() {
return "x : " + x + ", y : " + y;
}
}
class Point3D extends Point {
int z;
String getLocation() {
return super.getLocation() + ", z : " + z;
}
}
this()와 마찬가지로 super() 역시 생성자이다. super()는 조상 클래스의 생성자를 호출하는데 사용된다.
자손 클래스의 인스턴스를 생성하면, 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 그래서 자손 클래스의 인스턴스가 조상 클래스의 멤버들을 사용할 수 있는 것인데, 이 때 조상 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.
생성자의 첫 줄에서 조상클래스의 생성자를 호출해야하는 이유는 자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으므로 조상의 멤버들이 먼저 초기화되어 있어야 하기 때문이다.
이러한 조상 클래스 생성자의 호출은 클래스의 상속관계를 거슬러 올라가서 모든 클래스의 최고 조상인 Object 클래스의 생성자인 Object()까지 가서 끝난다. 그래서 Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다. 그렇지 않다면 컴파일러는 생성자의 첫 줄에 super();를 자동적으로 추가한다.
조상클래스로부터 상속받은 메소드의 내용을 변경하는 것을 오버라이딩이라고 한다. 상속받은 메소드를 그대로 사용하기도 하지만, 자손 클래스 자신에 맞게 변경해야하는 경우가 많다. 이럴 때 조상의 메소드를 오버라이딩한다.
오버라이딩은 메소드의 내용만을 새로 작성하는 것이므로 메소드의 선언부는 조상의 것과 완전히 일치해야 한다. 따라서 다음 조건을 만족해야한다.
오버라이딩은 메소드의 내용만을 새로 작성하는 것이므로 메소드의 선언부는 조상의 것과 완전히 일치해야 한다. 따라서 다음 조건을 만족해야한다.
자손 클래스에서 오버라이딩하는 메소드는 조상 클래스의 메소드와
여기서 반환타입의 경우 JDK1.5부터 공변 반환타입(covariant return type)이 추가되어, 반환타입을 자손 클래스의 타입으로 변경하는 것이 가능하도록 되었다.
위의 조건들을 간단히 요약하면 선언부가 서로 일치해야 한다는 것이다. 단 접근 제어자(access modifier)와 예외(exception)는 제한된 조건 하에서만 다르게 변경할 수 있다.
접근 제어자는 조상 클래스의 메소드보다 좁은 범위로 변경할 수 없다.
만일 조상 클래스에 정의된 메소드의 접근 제어자가 protected라면, 이를 오버라이딩하는 자손 클래스의 메소드는 접근 제어자가 protected나 public이어야 한다. 대부분의 경우 같은 범위의 접근 제어자를 사용한다.
조상 클래스의 메소드보다 많은 수의 예외를 선언할 수 없다.
아래의 코드는 자손 클래스의 메소드에 선언된 예외의 개수가 조상 클래스의 메소드에 선언된 예외의 개수보다 적으므로 바르게 오버라이딩 되었다.
class Parent {
void parentMethod() { }
}
class Child extends Parent {
void parentMethod() { } // 오버라이딩
void parentMethod(int i) { } // 오버로딩
void childMethod() { }
void childMethod(int i) { } // 오버로딩
}
상속과 다형성은 객체지향개념의 중요한 특징으로 서로 깊은 관계에 있다.
객체지향개념에서 다형성이란 여러가지 형태를 가질 수 있는 능력을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
구체적으로 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 한 것이다. 다음과 같은 코드가 있다.
class Parent {
String strP;
int intP;
void methodP() { }
}
class Child extends Parent {
String strC;
void methodC() { }
}
두 클래스는 상속관계에 있고, 두 클래스의 인스턴스를 생성하고 사용하려면 다음과 같이 해야한다.
Parent p = new Parent();
Child c = new Child();
생성된 인스턴스를 다루기 위해서는 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다. 이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만, 서로 상속관계에 있을 경우 다음과 같이 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조하도록 하는 것도 가능하다.
Parent p = new Child();
그렇다면 인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조하는 것은 어떤 차이가 있을까?
Parent p = new Child();
Child c = new Child();
Parent타입의 참조변수로는 Child인스턴스 중에서 Parent클래스의 멤버들(상속받은 멤버 포함)만 사용할 수 있다. 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라지는 것이다.
메소드 디스패치(method dispatch)는 어떤 메소드를 호출할지 결정하여 실행시키는 과정을 말한다. 이 과정은 static(정적)과 dynamic(동적)이 있다.
Static Dispatch
컴파일 시점에서, 컴파일러가 특정 메소드를 호출할 것이라고 명확하게 알고있는 경우이다.
Dynamic Dispatch
정적 디스패치와 반대로 컴파일러가 어떤 메소드를 호출하는지 모르는 경우이다. 동적 디스패치는 호출할 메서드를 런타임 시점에서 결정한다.
클래스가 설계도라면, 추상 클래스는 미완성 설계도라고 할 수 있다. 클래스가 미완성이라는 뜻은 멤버의 개수에 관계된 것이 아니라, 단지 미완성메소드(추상메소드)를 포함하고 있다는 의미이다.
미완성 설계도로 제품을 만들 수 없듯이 추상클래스로 인스턴스는 생성할 수 없다. 추상클래스는 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.
추상클래스 자체로는 클래스로서의 역할을 못하지만, 새로운 클래스를 작성하는데 바탕이 되는 조상클래스로서 중요한 의미를 가진다.
추상클래스는 키워드 ‘abstract’를 붙이기만 하면 된다. 클래스 선언부의 abstract을 보고 이 클래스에는 추상메서드가 있으니 상속을 통해 구현해야 한다는 것을 쉽게 알 수 있다.
abstract class 클래스이름 {
...
}
추상클래스는 추상메서드를 포함하고 있다는 것을 제외하고 일반 클래스와 동일하므로, 생성자가 있고, 멤버변수와 메서드도 가질 수 있다.
추상클래스를 작성할 때 여러 클래스에 공통적으로 사용될 수 있는 클래스를 바로 작성하기도 하고, 기존 클래스의 공통적인 부분을 뽑아서 추상클래스로 만들어 상속하도록 하는 경우도 있다.
상속계층도를 따라 내려갈수록 클래스는 점점 기능이 추가되어 구체화의 정도가 커지며, 반대로 올라갈수록 추상화의 정도가 커진다고 할 수 있다. 즉, 상속계층도를 따라 내려 갈수록 세분화되며, 올라갈수록 공통요소만 남게 된다.
클래스의 메서드를 추상메서드로 하는 대신, 아무 내용도 없는 메서드로 작성할 수도 있다. 자손클래스에서 오버라이딩하여 자신의 클래스에 맞게 구현하기 때문에 굳이 추상메서드를 사용할 필요가 없다고 생각할 수도 있다.
만일 추상메서드로 정의되지 않고 빈 몸통만 가지고 있다면 상속받는 자손클래스에서는 이 메서드가 온전히 구현된 것으로 인식하고 오버라이딩을 하지 않을수도 있다. 그렇기 때문에 추상메서드로 선언하여 자손클래스에게 내용을 구현해주어야 한다는 것을 알려주는 것이다.
final은 ‘마지막의’ 또는 ‘변경될 수 없는’의 의미를 가지고 있으며 거의 모든 대상에 사용될 수 있다.
변수에 사용되면 값을 변경할 수 없는 상수가 되며, 메서드에 사용되면 오버라이딩을 할 수 없게 되고 클래스에 사용되면 자신을 확장하는 자손클래스를 정의하지 못하게 된다.
선언시
final class FinalTest { // 조상이 될 수 없는 클래스
final int MAX_SIZE = 10; // 값을 변경할 수 없는 멤버변수(상수)
final void getMaxSize() { // 오버라이딩할 수 없는 메서드(변경불가)
final int LV = MAX_SIZE; // 값을 변경할 수 없는 지역변수(상수)
return MAX_SIZE;
}
}
Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다. 다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object클래스로부터 상속받게 함으로써 이것을 가능하게 한다.
아래와 같이 다른 클래스로 부터 상속받지 않는 클래스를 정의하면 코드를 컴파일 할 때 컴파일러에서 자동적으로 extends Object를 추가하여 상속받도록 한다.