자바의 정석 Chapter 07 객체 지향 프로그래밍 Ⅱ - 02. 오버라이딩편

강철의사나이·2021년 12월 19일
0

2. 오버라이딩(overriding)

2.1 오버라이딩이란?

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 한다.
상속받은 메서드를 그대로 사용하기도 하지만,
자손 클래스 자신에 맞게 변경해야 하는 경우가 많다.
이럴 때 조상의 메서드를 오버라이딩한다.
! overried : ~위에 덮어쓰다 (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 클래스는 서로 상속관계에 있으므로
Point 클래스로부터 getLocation()을 상속받았지만,
Point3D 클래스 자신에게 맞게 z축의 좌표값도 포함해서 반환하도록 오버라이딩했다.

Point 클래스를 사용하던 사람들은 getLocation()을 호출하면 점의 좌표를 문자열로 얻을 수 있다고 생각하듯이
자손 클래스인 Point3D에서도 같은 결과를 얻을 수 있다고 충분히 알 수 있을 것이다.

따라서 새로운 메서드를 제공하는 것보다 조상 클래스에 있는 메서드를 오버라이딩 하는 것이 효율적이다.

2.2 오버라이딩의 조건

자손 클래스에서 오버라이딩하는 메서는 조상 클래스의 메서드와
1. 이름이 같아야 한다.
2. 매개변수가 같아야 한다.
3. 반환타입이 같아야 한다.
! JDK1.5부터 '공변 반환타입(covariant return type)'이 추가되어,
반환타입을 자손 클래스의 타입으로 변경하는 것은 가능하도록 조건이 완화됨. 책 457p.

=> 즉 선언부가 서로 일치해야 한다.

단, 접근 제어자 (access modifier)와 예외(exception)은 제한된 조건 하에서만 다르게 변경할 수 있다.

1. 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경 할 수 없다.
만일 조상 클래스에 정의된 메서드의 접근 제어자가 protected라면
이를 오버라이딩 하는 자손 클래스의 메서드는 접근 제어자가 protected나 public이어야 한다.
대부분의 경우 같은 범위의 접근 제어자를 사용한다.
접근 제어자의 접근 범위
public < protected < (default) private

2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
아래의 코드를 보면
Child클래스의 parentMethod()에 선언된 예외의 개수가
조상인 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은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이다.
따라서 예외의 개수는 적거나 같아야 한다는 조건을 만족하지 못하는 잘못된 오버라이딩이다.

조상 클래스의 메서드를 자손 클래스에서 오버라이딩 할 때 주의사항
1. 접근 제어자를 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
2. 예외는 조상 클래스의 메서드보다 많이 선언할 수 없다.
3. 인스턴스 메서드를 static메서드로 또는 그 반대로 변경할 수 없다.

Q. 조상 클래스에 정의된 static 메서드를 자손 클래스에서 똑같은 이름의 static 메서드로 정의할 수 있는가?

A. 가능하다.
하지만 이것은 클래스에 별개의 static 메서드를 정의한 것이고, 오버라이딩이 아니다.
메서드는 클래스 이름으로 구별될 수 있고,
호출할 때는 '참조변수.메서드이름() 대신에
'클래스이름.메서드이름()'으로 하는 것이 바람직하다.
static 멤버들은 자신들이 정의된 클래스에 묶여있다고 생각하자.

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 역시 staitc메서드에서는 사용할 수 없고 인스턴스메서드에서만 사용할 수 있다.

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.outPrintln("x="+x);
        System.outPrintln("this.x="+this.x);
    	System.outPrintln("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()는 조상 클래스의 생성자를 호출하는데 사용된다.

자손 클래스의 인스턴스를 생성하면 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다.
그래서 자손 클래스의 인스턴스가 조상 클래스의 멤버들을 사용할 수 있는 것이다.

이 때 조상 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에
자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.

생성자의 첫 줄에서 조상 클래스의 생성자를 호출하는 이유는
자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으므로
조상의 멤버들이 먼저 초기화되어 있어야 하기 때문이다.

이와 같은 조상 클래스 생성자의 호출은 클래스의 상속관계를 거슬러 올라가면서 계속 반복된다.
결국 마지막으로 모든 클래스의 최고 조상인 Object클래스의 생성자인 Object()까지 가야 끝난다.

그래서 Object 클래스를 제외한 모든 클래스의 생성자는
첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다.

그렇지 않으면 컴파일러는 생성자의 첫 줄에 'super():'를 자동적으로 추가할 것이다.

Object 클래스를 제외한 모든 클래스의 생성자 첫 줄에 생성자로 this() 또는 super()를 호출해야 한다.

인스턴스를 생성할 때는 클래스를 선택하는 것만큼 생성자를 선택하는 것도 중요하다.

  1. 클래스 - 어떤 클래스의 인스턴스를 생성할 것인가?
  2. 생성자 - 선택한 클래스의 어떤 생성자를 이용해서 인스턴스를 생성할 것인가?

0개의 댓글