Development/JavaEssential
java - override
linuxism
2012. 11. 3. 11:09
2.1 오버라이딩이란?
조상클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다. 상속받은 메서드를 그대로 사용하기도 하지만, 자손클래스 자신에 맞게 변경해야하는 경우가 많다. 이럴 때 조상의 메서드를 오버라이딩한다.
[참고]override의 사전적 의미는 '~위에 덮어쓰다(overwrite).' 또는 '~에 우선하다.'이다.
2차원 좌표계의 한 점을 표현하기 위한 Point클래스가 있을 때, 이를 조상으로 하는 Point3D클래스, 3차원 좌표계의 한 점을 표현하기 위한 클래스를 다음과 같이 새로 작성하였다고 하자.
class Point { int x; int y; String getLocation() { return "x :" + x + ", y :"+ y; } } class Point3D extends Point { int z; String getLocation() { // 오버라이딩 return "x :" + x + ", y :"+ y + ", z :" + z; } } |
Point클래스의 getLocation메서드는 한 점의 x, y 좌표를 문자열로 반환하도록 작성되었다. 이 두 클래스는 서로 상속관계에 있으므로 Point3D클래스는 Point클래스로부터 getLocation메서드를 상속받지만, Point3D클래스는 3차원 좌표계의 한 점을 표현하기 위한 것이므로 조상인 Point클래스로부터 상속받은 getLocation메서드는 Point3D클래스에 맞지 않는다. 그래서 이 메서드를 Point3D클래스 자신에 맞게 z축의 좌표값도 포함하여 반환하도록 오버라이딩 하였다.
Point클래스를 사용하던 사람들은 새로 작성된 Point3D클래스가 Point클래스의 자손이므로 Point3D클래스의 인스턴스에 대해서 getLocation메서드를 호출하면 Point클래스의 getLocation메서드가 그랬듯이 점의 좌표를 문자열로 얻을 수 있을 것이라고 기대할 것이다. 그렇기 때문에 새로운 메서드를 제공하는 것보다 오버라이딩을 하는 것이 바른 선택이다.
2.2 오버라이딩의 조건
오버라이딩은 메서드의 내용만을 새로 작성하는 것이므로 메서드의 선언부는 조상의 것과 완전히 일치해야한다.
그래서 오버라이딩이 성립하기 위해서는 다음과 같은 조건을 만족해야한다.
자손클래스에서 오버라이딩하는 메서드는 조상클래스의 메서드와 - 이름이 같아야 한다. - 매개변수가 같아야 한다. - 리턴타입이 같아야 한다. |
한마디로 요약하면 선언부가 서로 일치해야한다는 것이다. 다만 접근제어자(Access Modifier)와 예외(Exception)는 제한된 조건 하에서 다르게 변경할 수 있다.
1. 접근제어자는 조상클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
- 만일 조상클래스에 정의된 메서드의 접근제어자가 protected라면, 이를 오버라이딩하는 자손클래스의 메서드는 접근제어자가 protected나 public이어야 한다. [참고]대부분의 경우 같은 범위의 접근제어자를 사용한다. 접근제어자의 접근범위를 넓은 것 순으로 나열하면 public, protected, default, private이다.
2.조상클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
- 아래의 코드를 보면 Child클래스의 parentMethod에 선언된 예외의 개수가 조상인 Parent클래스의 parentMethod에 선언된 예외의 개수보다 적으므로 바르게 오버라이딩 되었다. Class Parent { void parentMethod() throws IOException, SQLException { //.. } } Class Child extends Parent { void parentMethod() throws IOException { //.. } //.. } |
여기서 주의해야할 점은 단순히 선언된 예외의 개수의 문제가 아니라는 것이다.
Class Child extends Parent { void parentMethod() throws Exception { //.. } //.. } |
만일 위와 같이 오버라이딩을 하였다면, 분명히 조상클래스에 정의된 메서드보다 적은 개수의 예외를 선언한 것처럼 보이지만 Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이다.
그래서 예외의 개수는 적거나 같아야 한다는 조건을 만족시키지 못하는 잘못된 오버라이딩인 것이다.
2.3 오버로딩 vs. 오버라이딩
오버로딩과 오버라이딩은 서로 혼동하기 쉽지만 사실 그 차이는 명백하다. 오버로딩은 기존에 없는 새로운 메서드를 추가하는 것이고, 오버라이딩은 조상으로부터 상속받은 메서드의 내용을 변경하는 것이다.
오버로딩(Overloading) - 기존에 없는 새로운 메서드를 정의하는 것(new) 오버라이딩(Overriding) - 상속받은 메서드의 내용을 변경하는 것(change, modify) |
아래의 코드를 보고 오버로딩과 오버라이딩을 구별할 수 있어야 한다.
class Parent { void parentMethod() {} } class Child extends Parent { void parentMethod() {} // 오버라이딩 void parentMethod(int i) {} // 오버로딩 void childMethod() {} void childMethod(int i) {} // 오버로딩 void childMethod() {} // 에러!!! 중복정의 되었음.(already defined in Child) } |
2.4 super
super는 자손클래스에서 조상클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다. 멤버변수와 지역변수의 이름이 같을 때 this를 사용해서 구별했듯이 상속받은 멤버와 자신의 클래스에 정의된 멤버의 이름이 같을 때는 super를 사용해서 구별할 수 있다.
조상클래스로부터 상속받은 멤버도 자손클래스 자신의 멤버이므로 super대신 this를 사용할 수 있다. 그래도 조상클래스의 멤버와 자손클래스의 멤버가 중복 정의되어 서로 구별해야하는 경우에만 super를 사용하는 것이 좋다.
조상의 멤버와 자신의 멤버를 구별하는데 사용된다는 점을 제외하고는 super와 this는 근본적으로 같다. 모든 인스턴스메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조변수인 this와 super의 값이 된다.
static메서드(클래스메서드)는 인스턴스와 관련이 없다. 그래서 this와 마찬가지로 super역시 static메서드에서는 사용할 수 없고 인스턴스메서드에서만 사용할 수 있다.
[예제7-5] SuperTest.java |
class SuperTest { public static void main(String args[]) { Child c = new Child(); c.method(); } } class Parent { int x=10; } class Child extends Parent { void method() { System.out.println("x=" + x); System.out.println("this.x=" + this.x); System.out.println("super.x="+ super.x); } } |
[실행결과] |
x=10 this.x=10 super.x=10 |
이 경우 x, this.x, super.x 모두 같은 변수를 의미하므로 모두 같은 값이 출력되었다.
[예제7-6] SuperTest2.java |
class SuperTest2 { 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); } } |
[실행결과] |
x=20 this.x=20 super.x=10 |
이전 예제와는 달리 같은 이름의 멤버변수가 조상클래스인 Parent에도 있고 자손클래스인 Child클래스에도 있을 때는 super.x와 this.x는 서로 다른 값을 참조하게 된다. super.x는 조상클래스로부터 상속받은 멤버변수 x를 뜻하며, this.x는 자손클래스에 선언된 멤버변수를 뜻한다.
이처럼 조상클래스에 선언된 멤버변수와 같은 이름의 멤버변수를 자손클래스에서 중복해서 정의하는 것이 가능하며 참조변수 super를 이용해서 서로 구별할 수 있다.
변수만이 아니라 메서드 역시 super를 써서 호출할 수 있다. 특히 조상클래스의 메서드를 자손클래스에서 오버라이딩한 경우에 super를 사용한다.
class Point { int x; int y; String getLocation() { return "x :" + x + ", y :"+ y; } } class Point3D extends Point { int z; String getLocation() { // 오버라이딩 // return "x :" + x + ", y :"+ y + ", z :" + z; return super.getLocation() + ", z :" + z; } } |
getLocation메서드를 오버라이딩할 때 조상클래스의 getLocation메서드를 호출해서 포함시켰다. 조상클래스의 메서드의 내용에 추가적으로 작업을 덧붙이는 경우라면 이처럼 super를 사용해서 조상클래스의 메서드를 포함시키는 것이 좋다. 후에 조상클래스의 메서드가 변경되더라도 변경된 내용이 자손클래스의 메서드에 자동적으로 반영될 것이기 때문이다.
2.5 super() - 생성자
this()와 마찬가지로 super() 역시 생성자이다. this()는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super()는 조상클래스의 생성자를 호출하는데 사용된다.
자손클래스의 인스턴스를 생성하면, 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 그래서 자손클래스의 인스턴스가 조상클래스의 멤버들을 사용할 수 있는 것이다.
이 때 조상클래스 멤버의 생성과 초기화 작업이 수행되어야 하기 때문에 자손클래스의 생성자에서 조상클래스의 생성자가 호출되어야 한다.
생성자의 첫 줄에서 조상클래스의 생성자를 호출해야하는 이유는 자손클래스의 멤버가 조상클래스의 멤버를 사용할 수도 있으므로 조상의 멤버들이 먼저 초기화되어 있어야 하기 때문이다.
인스턴스를 생성할 때는 다음의 2가지를 선택해야한다. 1. 클래스 - 어떤 클래스의 인스턴스를 생성할 것인가? 2. 생성자 - 선택한 클래스의 어떤 생성자를 이용해서 인스턴스를 생성할 것인가? 이처럼 인스턴스를 생성할 때는 클래스를 선택하는 것만큼 생성자를 선택하는 것도 중요한 일이다. |
이와 같은 조상클래스 생성자의 호출은 클래스의 상속관계를 거슬러 올라가면서 계속 반복된다. 마지막으로 모든 클래스의 최고조상인 Object클래스의 생성자인 Object()까지 가서야 끝이 난다.
그래서 Object클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야한다. 그렇지 않으면 컴파일러는 생성자의 첫 줄에 super();를 자동적으로 추가할 것이다.
[예제7-7] PointTest.java |
class PointTest { public static void main(String args[]) { Point3D p3 = new Point3D(1,2,3); } } class Point { int x; int y; Point(int x, int y) { this.x = x; this.y = y; } String getLocation() { return "x :" + x + ", y :"+ y; } } class Point3D extends Point { int z; Point3D(int x, int y, int z) { this.x = x; this.y = y; this.z = z; } String getLocation() { // 오버라이딩 return "x :" + x + ", y :"+ y + ", z :" + z; } } |
[컴파일결과] |
C:\j2sdk1.4.1\work>javac PointTest.java PointTest.java:22: cannot resolve symbol symbol : constructor Point () location: class Point Point3D(int x, int y, int z) { ^ 1 error |
이 예제를 컴파일하면 위와 같은 컴파일에러가 발생할 것이다. Point3D클래스의 생성자에서 조상클래스의 생성자인 Point()를 찾을 수 없다는 내용이다.
Point3D클래스의 생성자의 첫 줄이 생성자(조상의 것이든 자신의 것이든)를 호출하는 문장이 아니기 때문에 컴파일러는 다음과 같이 자동적으로 'super();'를 Point3D클래스의 생성자의 첫 줄에 넣어 준다.
Point3D(int x, int y, int z) { super(); this.x = x; this.y = y; this.z = z; } |
그래서 Point3D클래스의 인스턴스를 생성하면, 생성자 Point3D(int x, int y, int x)가 호출되면서 첫 문장인 super();를 수행하게 된다. super()는 Point3D클래스의 조상인 Point클래스의 기본 생성자인Point()를 뜻하므로 Point()가 호출된다.
그러나 Point클래스에 생성자 Point()가 정의되어 있지 않기 때문에 위와 같은 컴파일 에러가 발생한 것이다. 이 에러를 수정하려면, Point클래스에 생성자 Point()를 추가해주던가, 생성자 Point3D(int x, inty, int z)의 첫 줄에서 Point(int x, int y)를 호출하도록 변경하면 된다.
[참고]생성자가 정의되어 있는 클래스에는 컴파일러가 기본 생성자를 자동적으로 추가하지 않는다.
Point3D(int x, int y, int z) { super(x, y); // 조상클래스의 생성자 Point(int x, int y)를 호출한다. this.z = z; } |
위와 같이 변경하면 된다. 문제없이 컴파일 될 것이다. 조상클래스의 멤버변수는 이처럼 조상의 생성자를 이용해서 초기화 하도록 해야 하는 것이다.
[예제7-8] PointTest2.java |
class PointTest2 { public static void main(String argsp[]) { Point3D p3 = new Point3D(); System.out.println("p3.x=" + p3.x); System.out.println("p3.y=" + p3.y); System.out.println("p3.z=" + p3.z); } } class Point { int x=10; int y=20; Point(int x, int y) { this.x = x; this.y = y; } } class Point3D extends Point { int z=30; Point3D() { this(100, 200, 300); // Point3D(int x, int y, int z)를 호출한다. } Point3D(int x, int y, int z) { super(x, y); // Point(int x, int y)를 호출한다. this.z = z; } } |
[실행결과] |
p3.x=100 p3.y=200 p3.z=300 |
Point3D클래스의 인스턴스를 생성하면, 조상인 Point클래스의 인스턴스도 생성되므로 Point클래스의 생성자도 호출되고, Point클래스의 조상인 Object클래스의 생성자까지 호출된다.
이처럼 어떤 클래스의 인스턴스를 생성하면, 클래스의 상속관계를 최고조상인 Object클래스까지 거슬러 올라가면서 조상클래스의 인스턴스를 생성한다.
컴파일러는 Point클래스의 생성자인 Point(int x, int y)의 첫 줄에 super();를 자동적으로 삽입할 것이다. 이 super()는 Object클래스의 생성자인 Object()를 뜻한다.
Point(int x, int y) { super(); this.x = x; this.y = y; } |
생성자의 호출은 계속 이어져서 결국 Object클래스의 생성자인 Object()까지 호출되어야 끝나는 것이다.
출처 - http://cafe.naver.com/javachobostudy/39