Java/[도서] 자바의 정석

오버라이딩(overriding)

Lea Hwang 2022. 5. 7. 00:19

자바의 정석

Chapter 소제목
7. 객체지향 프로그래밍 2. 오버라이딩(overriding)

 

 

오버라이딩이란?

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것(재정의)을 오버라이딩이라고 합니다.

 

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좌표를 문자열로 반환하도록 작성되었습니다.

 

이 두 클래스는 서로 상속관계에 있으므로 Point 3D클래스는 Point클래스로부터 getLocation()을 상속받지만, Point3D클래스는 3차원 좌표계의 한 점을 표현하기 위한 것이므로 조상인 Point클래스로 부터 상속받은 getLocation()은 Point3D클래스에 맞지 않습니다. 그래서 이 메세드를 Point3D클래스 자신에 맞게 z축의 좌표값도 포함하여 반환하도록 오버라이딩하였습니다.

 

Point클래스를 사용하던 사람들은 새로 작성된 Point3D클래스가 Point클래스의 자손이므로 Point3D클래스의 인스턴스에 대해서 getLocation()을 호출하면 Point클래스의 getLocation()이 그랬듯이 점의 좌표를 문자열로 얻을 수 있을 것이라고 기대할 것입니다.

 

 

오버라이딩의 조건

오버라이딩은 메서드의 내용만을 새로 작성하는 것이므로 메서드의 선언부는 조상의 것과 완전히 일치해야 합니다.

 

 

오버라이딩 조건

💡 자손 클래스에서 오버라이딩하는 메서드는 조상 클래스의 메서드와
    - 이름이 같아야 한다.
    - 매개변수가 같아야 한다.
    - 반환타입이 같아야 한다.

한마디로 요약하면 선언부가 서로 일치해야 한다는 것입니다.

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

 

1. 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.

접근 제어자의 접근 범위를 넓은 것에서 좁은 것 순으로 나열하면 public, protected, (default), private입니다.

만일 조상 클래스에 정의된 메서드의 접근 제어자가 potected라면 이를 오버라이딩하는 자손 클래스의 메서드는 접근 제어자가 protected나 public 이어야 합니다.

대부분의 경우 같은 범위의 접근 제어자를 사용합니다.

 

2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

아래의 코드를 보면 Child클래스의 parentMethod()에 선언된 예외의 개수가 조상인 Parent클래스 parentMethod()에 선언된 예외의 개수보다 적으므로 바르게 오버라이딩 되어있는 것을 알 수 있습니다.

class Parent {
    void parentMethod() throws IOException, SQLException {
        ...
    }
}

class Child extends Parent {
    void parentMethod() throws IOException {
        ...
    }
}

여기서 주의해야 할 점은 단순히 선언된 예외의 개수의 문제는 아닙니다.

 

class Parent {
    void parentMethod() throws IOException, SQLException {
        ...
    }
}

class Child extends Parent {
    void parentMethod() throws Exception {
        ...
    }
}

만일 위와 같이 오버라이딩을 하였다면, Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것이므로 잘못된 오버라이딩입니다.

 

 

오버로딩 vs. 오버라이딩

둘의 이름은 비슷하지만 차이는 명백합니다.

오버로딩은 기존에 없는 새로운 메서드를 추가하는 것이고,

오버라이딩은 조상으로부터 상속받은 메서드의 내용을 변경하는 것입니다.  - 재정의

 

아래의 코드를 보고 둘을 구별할 수 있어야 합니다.

class Parent {
    void parentMethod() { }
}

class Child extends Parent {
    void parentMethod() { }          // 오버라이딩
    void parentMethod(int i) { }     // 오버로딩

    void childMethod() { }
    void childMethod(int i) { }      // 오버로딩
    void childMethod() { }           // -> 에러, 중복정의 되었음
}

 

super

super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버참조하는 데 사용되는 참조 변수입니다.

 

멤버 변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속받은 멤버와 자신의 클래스에 정의된 멤버의 이름이 같을 때는 super를 붙여서 구별할 수 있습니다. 조상의 멤버와 자신의 멤버를 구별하는 데 사용된다는 점을 제외하고는 super와 this는 근본적으로 같습니다.

 

모든 인스턴스 메서드에는 자신이 속한 인스턴스의 주소가 지역변수로 저장되는데, 이것이 참조 변수인 this와 super의 값이 됩니다.

 

static메서드는 인스턴스와 관련이 없기 때문에 this와 마찬가지로 super 역시 static메서드에서는 사용할 수 없고 인스턴스 메서드에서만 사용할 수 있습니다.

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);              // x=10
    System.out,println("this.x=" + this.x);    // this.x=10
    System.out,println("super.x=" + super.x);  // super.x=10
  }
}

이 경우, x, this.x, super.x 모두 같은 변수를 의미하므로 모두 같은 값이 출력되었습니다.

 

class SuperTest {
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);              // x=20
    System.out,println("this.x=" + this.x);    // this.x=20
    System.out,println("super.x=" + super.x);  // super.x=10
    }
}

같은 이름의 멤버 변수가 조상 클래스인 Parent에도 있고 자손 클래스인 Child클래스에도 있을 때

⇒ int x

이 경우에는 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를 사용해서 조상 클래스의 메서드를 포함시키는 것이 좋습니다. 후에 조상 클래스의 메서드가 변경되더라도 변경된 내용이 자손 클래스의 메서드에 자동적으로 반영될 것이기 때문이다.

 

super() - 조상 클래스의 생성자

this()와 마찬가지로 super() 역시 생성자입니다.

super와 super()는 다른 개념이므로 헷갈리지 않도록 주의해야 합니다.

this()는 같은 클래스의 다른 생성자를 호출하는 데 사용되지만, super()는 조상 클래스의 생성자를 호출하는 데 사용됩니다.

 

조상 클래스의 생성자를 호출할 때 사용

조상 클래스의 멤버는 조상의 생성자를 호출해서 초기화합니다.

    자식 클래스는 자신이 선언한 것만 초기화 가능합니다.

class PointTest {
    public static void main(String args[]) {
        Point3D p3 = new Point3D(1,2,3);
    }
}

class Point {
    int x, 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;
    }
}

주석 단 부분을 맞게 수정하자면,

 

class PointTest {
    public static void main(String args[]) {
        Point3D p3 = new Point3D(1,2,3);
    }
}

class Point {
    int x, 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) {
        super(x,y);   // 조상클래스의 생성자 Point(int x, int y)를 호출해서 초기화
    	this.z = z;   // 자식 클래스는 자신이 선언한 것만 초기화 가능 
    }

    String getLocation() { // 오버라이딩 
        return "x :" + x + ", y :" + y + ", z :" + z;
    }
}

 

 

추가 조건

💡 [중요]
생성자의 첫 줄에 반드시 생성자를 호출해야 합니다.(예: super(), this())
그렇지 않으면 컴파일러가 생성자의 첫 줄에 super();를 삽입합니다.
class Point {
    int x;
    int y;

    Point() {
        this(0,0);        // 생성자의 첫 줄에 반드시 생성자를 호출해야 한다 -> ok
    }

    Point(int x, int y) {
                      // 조건 불만족, super(); 조상 클래스의 기본 생성자 호출해야함
        this.x = x;
        this.y = y;
    }
}

주석 단 부분을 맞게 수정하자면,

 

class Point extends Object {
    int x;
    int y;

    Point() {
        this(0,0);        // 생성자의 첫 줄에 반드시 생성자를 호출해야 한다 -> ok
    }

    Point(int x, int y) {
    	super();          // Object();             
        this.x = x;
        this.y = y;
    }
}

 

어떤 클래스의 인스턴스를 생성하면, 클래스의 상속관계의 최고조상인 Object클래스까지 거슬러 올라가면서 모든 조상 클래스의 생성자가 순서대로 호출됩니다. 거슬러 올라가는 모습을 그림으로 표현해보겠습니다.