본문 바로가기

Java/Java Language

[Java] 오버라이딩 (Overriding)

오버라이딩의 역할

Override는 말그 대로 메서드 위에 올라타는 것이며, 상위 클래스의 메서드를 하위 클래스의 맞게 변경해야하는 경우에 메서드를 오버라이딩(Overiding)한다. Overloading과 용어는 비슷하지만, Overloading은 없던 메서드를 새로 구현한 것이고, Overriding은 상속받은 메서드의 구현부를 변경하는 것이다. 

 

아래의 Point 클래스를 상속받은 Point3D 클래스에서 getLocation 메서드를 해당 클래스에 맞게 인스턴스 변수 z의 출력이 필요하다. 그리하여 오버라이딩을하여 Point3D 클래스에 맞게 메서드 구현부를 변경하였다. 메서드 선언부(메서드명, 매개변수, 반환 타입)는 오버라이드할 메서드와 동일해야한다.

class Point {  
    int x; // x 좌표  
    int y; // y 좌표  

    void getLocation() {  
        System.out.println(x + ", " + y);  
    }  
}

class Point3D extends Point {  
    int z;  

    @Override
    void getLocation() {  
        System.out.println(x + ", " + y + ", " + z);  
    }  
}

오버라이딩의 조건

접근 제어자의 범위 설정

상위 클래스의 정의된 메서드의 접근 제어자(Access Modifier)가 protected라면, 이를 오버라이딩하는 하위 클래스의 메서드는 접근 제어자가 protected이나 public이어야 한다. 대부분의 경우 같은 범위의 접근 제어자를 사용한다.

 

예외 선언의 개수 설정

상위 클래스의 메서드에 선언된 예외(Exception)의 개수보다, 하위클래스에서 오바리이딩한 메서드에 선언된 예외의 개수가 적어야한다.

아래의 예시에는 Exception의 개수가 서로 동일하여, 오버라이딩의 조건을 만족시킨다.

class Parent {
    void method() throws IOException {

    }
}

class Parent {
    @Override
    void method() throws ArrayStoreException {

    }
}

 

아래의 예시에는 Exception의 개수가 서로 동일하지만, Exception은 모든 예외의 최상위 클래스이므로 가장 많은 개수의 exception을 throw 할 수 있다. 따라서 오버라이딩의 조건에 맞지 않다.

class Parent {
    void method() throws IOException {

    }
}

class Parent {
    @Override
    void method() throws Exception {

    }
}

상위 클래스 참조 변수 super

상위 클래스로 부터 상속받은 멤버를 참조하는데 사용하는 참조 변수이다. 멤버 변수와 지역변수를 구별하기 위해서 this 참조변수로 구별하듯이, 상속 받은 클래스의 멤버와 해당 클래스의 멤버를 구분하기 위해 super를 쓴다.

 

상위 클래스로부터 상속받은 멤버도 하위 클래스의 멤버이므로, super 대신 this로 참조할 수 있다. 그러므로 아래 코드에서 method()의 실행결과, super 또는 this로 참조한 x와 y의 값이 동일하게 출력된다.

class Parent {
    int x = 1, y = 2;
}

class Child {

    void method() {
        System.out.println("x, y " + x + ", " + y);  // x, y 1, 2
        System.out.println("x, y " + this.x + ", " + this.y);  // x, y 1, 2
        System.out.println("x, y " + super.x + ", " + super.y); // x, y 1, 2
    }
}

class SuperTest {
    public static void main(String[] args) {
        Child child = new Child();
        child.method();
    }
}

인스턴스 메서드(instance method) 호출시에, 인스턴스의 주소 값이 지역 변수 배열(local variable array)에 저장된다. 이 값을 참조변수 this와 super에 저장하며, 따라서 super와 this는 근본적으로 동일하다. 다만, this는 현재 클래스의 필드와 메서드를 참조할 때 사용되고, super는 부모 클래스의 필드와 메서드를 참조할 때 사용한다.

 

클래스 메서드(static method)에서는 지역변수에 인스턴스를 가리키는 this 참조변수가 필요없어서 저장하지 않아, this 참조 변수를 사용할 수 없다. 동일하게 인스턴스 참조 값을 저장하는 super 또한 마찬가지이다.

 

다음 코드를 보면, 하위 클래스(Child)에서 상위 클래스와 동일한 필드명(x, y)으로 선언할 수 있다. this로 현재 클래스의 인스턴스 변수와 메서드에 참조가 가능하며, super로 부모 클래스(Parent)의 인스턴스 변수와 인스턴스 메서드에 참조가 가능하다. 그러므로 method()의 실행결과, super 또는 this로 참조한 x와 y의 값이 다르게 출력된다.

class Parent {
    int x = 1, y = 2;  
}

class Child {
    int x = 5, y = 10;

    void method() {
        System.out.println("x, y " + x + ", " + y);  // x, y 5, 10
        System.out.println("x, y " + this.x + ", " + this.y);  // x, y 5, 10
        System.out.println("x, y " + super.x + ", " + super.y); // x, y 1, 2
    }
}

class SuperTest {
    public static void main(String[] args) {
        Child child = new Child();
        child.method();
    }
}

 

인스턴스 변수는 힙 영역에서 인스턴스 내에, 상위 클래스의 인스턴스 변수와 해당 클래스의 인스턴수 변수가 각각 저장되기 때문에 동일한 인스턴스 변수명으로 저장할 수 있다.

Heap
------------------------------------
|       Child Instance             |
|----------------------------------|
|  Parent:                         |
|    - int x                       |
|    - int y                       |
|----------------------------------|
|  Child:                          |
|    - int x                       |
|    - int y                       |
------------------------------------

 

super 참조 변수를 통하여 상위 클래스의 인스턴스 변수 뿐만 아니라 메서드도 호출이 가능하다.

class Parent {
    final int x = 1, y = 2;

    void method() {
        System.out.println("x, y " + x + ", " + y);  // x, y 1, 2
        System.out.println("x, y " + this.x + ", " + this.y);  // x, y 1, 2
    }
}

class Child {
    final int x = 5;

    @Override
    void method() {
        super.method();
        System.out.println("x, y " + this.x + ", " + this.y); // x, y 5, 10
    }
}

class SuperTest {
    public static void main(String[] args) {
        Child child = new Child();
        child.method();
    }
}

 

상위 클래스의 메서드에 작업을 추가하는 경우라면, 오버라이드하고 상위 클래스의 메서드를 super 참조변수를 통해 호출하고 추가 작업을 구현하는 것이 좋다. 이렇게 구현하면, 상위 클래스의 메서드 구현부가 변경되더라도 하위 클래스에는 자동적으로 반영이된다.

 

인스턴스 메서드는 virtual method으로 vtable에 저장한다. 메서드 영역(Method Area)의 각 클래스 데이터에 vtable이 저장되어 있다. vtable에 클래스 타입에 해당하는 메서드가 저장되어 있어서, 하위 클래스의 메서드를 상위 클래스와 동일한 메서드명으로 정의할 수 있다.

Method Area
------------------------------------
| Parent Class Data                |
------------------------------------
|  vtable:                         |
| +------------------------------+ |
| | method() -> Parent::method   | |
| +------------------------------+ |
------------------------------------

------------------------------------
| Child Class Data                 |
------------------------------------
|  vtable:                         |
| +------------------------------+ |
| | method() -> Parent::method   | |
| +------------------------------+ |
| +------------------------------+ |
| | method() -> Child::method    | |
| +------------------------------+ |
------------------------------------

 

그리고 위의 코드처럼 오버라이딩하였다면, Parent 클래스의 method() 메서드가 오버라이드된 Child::method로 대체되어 저장된다.

Method Area
------------------------------------
| Parent Class Data                |
------------------------------------
|  vtable:                         |
| +------------------------------+ |
| | method() -> Parent::method   | |
| +------------------------------+ |
------------------------------------

------------------------------------
| Child Class Data                 |
------------------------------------
|  vtable:                         |
| +------------------------------+ |
| | method() -> Child::method    | |
| +------------------------------+ |
------------------------------------

 

클래스 변수(static variable)와 클래스 메서드(static method)는 메서드 영역(method area)의 각 클래스 별로 존재하는 클래스 데이터에 저장되는 변수와 메서드이다. 따라서 하위 클래스는 상위 클래스로부터 상속받지못하고 클래스 메서드는 오버라이드가 불가하다. 이로인해, 하위 클래스에서도 동일한 클래스 변수명과 메서드명으로 정의가 가능하다.

class Parent {
    static int s = 100;

    static void staticmethod() {
        System.out.println("s " + s);  // x, y 5, 10
    }
}

class Child {
    static int s = 100;

    static void staticmethod() {
        System.out.println("s " + s);  // x, y 5, 10
    }
}

class SuperTest {
    public static void main(String[] args) {
        Parent.staticmethod();  
        Child.staticmethod();
    }
}
Method Area
------------------------------------
|       Parent Class Data          |
|----------------------------------|
|    - static int s                |
|    - static staticmethod()       |
------------------------------------

Method Area
------------------------------------
|       Child Class Data          |
|----------------------------------|
|    - static int s                |
|    - static staticmethod()       |
------------------------------------

상위 클래스의 생성자 super()

생성자에서 다른 생성자를 호출하는 this()와 같이, super() 또한 하위 클래스의 생성자에서 상위 클래스의 생성자를 호출하는데 사용한다. super()는 상위 클래스의 생성자(Constructor)와 마찬가지이다.

 

하위 클래스는 상속 받은 상위 클래스의 인스턴스 변수를 사용하기 위하여 초기화 작업이 수행되어야한다. 그러므로 super()를 호출하여 상위 클래스의 인스턴스 변수를 초기화한다.

class Point {  
    int x; // x 좌표  
    int y; // y 좌표  

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

    void getLocation() {  
        System.out.println(x + ", " + y);  
    }  
}

class Point3D extends Point {  
    int z;  

    Point3D() {  
        this(10, 10, 10);  
    }

    Point3D(int x, int y, int z) {  
        super(x, y);  
        this.z = z;  
    }  

    @Override  
    void getLocation() {  
        System.out.println(x + ", " + y + ", " + z);  
    }  
}


Class Main() {
    public static void main(String[] args) {  
        Point3D1 point3D1 = new Point3D(1, 2, 3);  
        Point3D2 point3D2 = new Point3D();  
    }
}

하위 클래스의 멤버가 상위 클래스의 멤버를 사용할 수 있다. 그러므로 상위 클래스의 멤버를 먼저 초기화하고 후에 하위 클래스가 상위 클래스의 멤버를 사용하여 초기화 또는 작업을 수행할 수 있다. 따라서 하위 클래스 생성자의 첫 줄에 상위 클래스 생성자를 호출해야한다.

 

이렇게 상위 클래스의 생성자를 호출하며 타고 올라가면서, 최상위 계층의 클래스인 Object 클래스의 생성자인 Object() 까지 실행된다. 그리고나서 해당 클래스의 생성자의 실행까지 마치면, 상위 멤버부터 해당 멤버까지의 초기화가 완료된다.

 

만일, 상위 클래스의 기본 생성자(default constcurtor)만 존재하고 하위 클래스의 생성자에서 상위 클래스의 생성자 호출을 하지 않았을 경우에, 컴파일러는 컴파일 타임에 자동적으로 super() 코드를 생성자 첫줄에 삽입한다.

 class Point {  
    int x; // x 좌표  
    int y; // y 좌표  

    // Point() {} 컴파일러 삽입

    void getLocation() {  
        System.out.println(x + ", " + y);  
    }  
}

class Point3D extends Point {  
    int z;  

    Point3D(int x, int y, int z) {  
        // super(); 컴파일러 삽입
        this.x = x;  
        this.y = y;  
        this.z = z;  
    }  

    @Override  
    void getLocation() {  
        System.out.println(x + ", " + y + ", " + z);  
    }  
}

 

위의 코드에서 Point 클래스의 기본 생성자가 아닌 생성자가 정의되어있다면 해당 코드는 에러가 발생한다. 컴파일러는 생성자가 정의되어 있는 클래스에는 기본 생성자를 자동으로 추가하지 않기 때문이다.

 

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

클래스는 어떤 클래스의 인스턴스를 생성할지 결정하며, 생성자는 선택한 클래스의 어떤 생성자를 호출하여 인스턴스를 생성할지 결정해야한다.


챰고 자료

  • 자바의 정석 (남궁성 지음)

'Java > Java Language' 카테고리의 다른 글

[Java] Modifier (Access Modifier와 Encapsulation)  (0) 2024.06.20
[Java] Package와 Import  (0) 2024.06.19
[Java] 상속 (Inheritance)  (0) 2024.06.18
[Java] 변수의 초기화  (0) 2024.06.13
[Java] 생성자 (Constructor)  (0) 2024.06.13