[윤성우의 열혈 Java 프로그래밍] Chapter 15 - 클래스의 상속 2: 오버라이딩

Update:     Updated:

카테고리:

태그:

15-1. 상속을 위한 두 클래스의 관계

- 상속의 기본 조건인 'IS-A 관계'

상위 클래스와 하위 클래스간의 다음 관계가 성립한다.

하위 클래스는 상위 클래스의 모든 특성을 지닌다.
하위 클래스는 자신만의 추가적인 특성을 더하게 된다.

예시를 통해서 상속 관계를 이해해보자.

class MobilePhone {
    protected String number;

    public MobilePhone(String number) {
        this.number = number;
    }

    public void answer() {
        System.out.println("Hi~ from " + number);
    }
}

class SmartPhone extends MobilePhone{
    private String androidVer;

    public SmartPhone(String number, String androidVer) {
        super(number);
        this.androidVer = androidVer;
    }

    public void playApp() {
        System.out.println("App is running in " + androidVer);
    }
}

public class MobileSmartPhone {
    public static void main(String[] args) {
        SmartPhone phone = new SmartPhone("010-777-7777", "JellyBean");

        phone.answer();
        phone.playApp();
    }
}

‘모바일폰’과 ‘스마트폰’의 관계를 예시로 들어 상속의 특징을 설명하고 있다.
위의 예시를 객체지향적으로 본다면 다음과 같이 얘기할 수 있다.

모바일폰을 스마트폰이 상속한다.

이를 코드를 통해 나타낸 부분은 다음과 같다.

class SmartPhone extends MobilePhone {...}

그런데, 우리는 ‘스마트폰도 모바일폰의 한 종류’라고 말하기도 한다.
따라서 다음과 같이 말하는 것도 가능하다.

스마트폰도 모바일폰이다.
스마트폰은 일종의 모바일폰이다.

그리고 위의 문장들이 나타내는 관계를 가리켜 ‘IS-A 관계’라고 하고,
이것이 상속의 관계를 맺기 위한 두 클래스의 기본 조건이 된다.

  • IS-A 관계 정리
    • IS-A 관계는 ‘~은 ~이다.’로 표현되는 관계이다.
    • 상속이 갖는 문법적 특성은 IS-A 관계의 표현에 적합하다.
    • 따라서 상속 관계를 형성하기 위한 두 클래스는 IS-A 관계에 있어야한다.

15-2. 메소드 오버라이딩

메소드 오버라이딩: 상위 클래스에 정의된 메소드를 하위 클래스에서 다시 정의하는 것
문법적으로는 단순하지만 이것이 가져다주는 이점은 가볍지 않다.

인스턴스를 생성 후, 무엇으로 참조하느냐에 따라 인스턴스의 활용능력이 달라진다!

- 상위 클래스의 참조변수가 참조할 수 있는 대상의 범위

위에서 만들었던 예시를 다시 보자..

class MobilePhone {
    protected String number;

    public MobilePhone(String number) {
        this.number = number;
    }

    public void answer() {
        System.out.println("Hi~ from " + number);
    }
}

class SmartPhone extends MobilePhone{
    private String androidVer;

    public SmartPhone(String number, String androidVer) {
        super(number);
        this.androidVer = androidVer;
    }

    public void playApp() {
        System.out.println("App is running in " + androidVer);
    }
}

public class MobileSmartPhone {
    public static void main(String[] args) {
        // SmartPhone 인스턴스를 각각 SmartPhone과 MobilePhone 타입의 변수로 참조함
        SmartPhone phone1 = new SmartPhone("010-777-7777", "JellyBean");
        MobilePhone phone2 = new SmartPhone("010-999-9999", "Cookie");

        phone1.answer();
        phone1.playApp();
        System.out.println();

        phone2.answer();
        // phone2.playApp();    // phone2가 부모클래스형 참조변수라서 컴파일 에러가 남
    }
}

이처럼 상위 클래스의 참조변수는 하위 클래스의 인스턴스를 참조할 수 있다. 다음과 같이 정리하자.

  • 모바일폰을 상속하는 스마트폰도 일종의 모바일폰이다.
    • MobilePhone을 상속하는 SmartPhone 인스턴스는 MobilePhone 인스턴스이기도 하다.
  • 따라서 MobilePhone형 참조변수는 SmartPhone 인스턴스를 참조할 수 있다.

여기서 집중해야할 부분은 이 부분이다.

MobilePhone phone2 = new SmartPhone("010-999-9999", "Cookie");

phone2.playApp();   // compile Error!

참조변수 phone2는 MobilePhone 타입의 참조변수이다.
이런 경우 'phone2가 참조하는 인스턴스와는 상관없이'
MobilePhone 클래스에 정의되었거나, 이 클래스가 상속하는 클래스의 멤버로 제한된다.

이처럼 자바는 메소드 호출 시 참조하는 인스턴스와는 관계없이 ‘참조변수의 형(type)을 참조’하여 그 메소드 호출이 옳은 것인지 판단한다.
이에는 2가지 이유가 있는데,

1) 실행 시간을 늦추는 결과로 이어질 수 있다.

실제 참조하는 인스턴스를 대상으로 메소드의 호출 가능성을 판단하는 것은 간단하지 않다.
참조하는 인스턴스의 종류는 코드의 흐름에 따라 얼마든지 달라질 수 있기 때문이다.

2) 참조변수의 형을 기준으로 접근 가능한 멤버를 제한하는 것은 코드를 단순하게 한다.

단점이 많은 일부 기능을 제한함으로써 단순하고 명료한 코드의 작성을 유도했다.

- 클래스의 상속과 참조변수의 참조 가능성에 대한 정리

위의 내용을 정리해보자!

class Cake {
    public void sweet() {...}
}

class CheeseCake extends Cake {
    public void milky() {...}
}

class StrawberryCheeseCake extends CheeseCake {
    public void sour() {...}
}

image

Cake형의 참조변수 cake1은
-> Cake에 정의된 sweet() 메소드를 호출할 수 있다.

하위 클래스인 CheeseCake형 참조변수 cake2는
-> Cake에 정의된 sweet() 메소드와
-> CheeseCake에 정의된 milky() 메소드를 호출할 수 있다.

- 참조변수 간 대입과 형 변환

Cake 예시를 다시 살펴보자!

class Cake {
    public void sweet() {...}
}

class CheeseCake extends Cake {
    public void milky() {...}
}

컴파일러는 ‘참조변수의 형’만을 가지고 대입의 가능성을 판단한다.

CheeseCake ca1 = new CheeseCake();
Cake ca2 = c1;  // 가능!

‘CheeseCake’ is a ‘Cake’ 이기 때문에 cake형 참조변수로 CheeseCake의 인스턴스를 참조하는 것은 가능하다.

Cake ca3 = new CheeseCake();
CheeseCake ca4 = ca3;   // 불가능!

위의 문장을 자바 컴파일러는 다음과 같은 수준으로 보고 대입의 가능성을 판단한다.
Cake ca3 = …
CheeseCake ca4 = ca3;

이렇게 ca3가 참조하는 인스턴스가 cheeseCake의 인스턴스임을 보장할 수 없다.
(Cake를 상속하는 다른 클래스의 인스턴스일 수도 있다..)

다만, 개발자가 명시적으로 형 변환을 해주면 대입이 가능하다.

Cake ca3 = ...
CheeseCake ca4 = (CheeseCake)ca3;   // 가능함. 주의 요망!

ca3가 참조하는 인스턴스가 CheeseCake 인스턴스임을 개발자가 보장한다는 의미이다.
이렇게하면 컴파일러는 그냥 허용하기 때문에 치명적인 실수가 발생하지 않도록 주의하여 사용해야한다.

- 메소드 오버라이딩(Method Overriding)

상위 클래스에 정의된 메소드를 하위 클래스에서 다시 정의하는 행위를 메소드 오버라이딩이라고 한다.

class Cake {
    public void yummy() {   // 오버라이딩 '된' 메소드
        System.out.println("Yummy Cake");
    }
}

class CheeseCake extends Cake {
    public void yummy() {   // 오버라이딩 '한' 메소드
        System.out.println("Yummy CheeseCake");
    }
}

public class YummyCakeOverriding {
    public static void main(String[] args) {
        Cake cake1 = new CheeseCake();
        CheeseCake cake2 = new CheeseCake();

        cake1.yummy();
        cake2.yummy();
    }
}

image

다음 세 가지가 같아야 메소드 오버라이딩이 성립한다.

  1. 메소드의 이름
  2. 메소드의 반환형
  3. 메소드의 매개변수 선언

실행결과를 보면 알 수 있듯이, 오버라이딩을 하면 참조변수의 형에 상관없이
오버라이딩 한 메소드(CheeseCake의 yummy)가
오버라이딩 된 메소드(Cake의 yummy)를 대신하게 된다.

- 오버라이딩 된 메소드를 호출하는 방법

클래스 외부에서 인스턴스를 통해 오버라이딩 된 메소드를 호출하는 방법은 없다.
다만, 클래스 외부가 아닌 내부에서 메소드를 호출하는 방법은 있다. 다음을 참고하자.

class Cake {
    public void yummy() {
        System.out.println("Yummy Cake");
    }
}

class CheeseCake extends Cake {
    public void yummy() {
        super.yummy();  // 하위 클래스의 메소드 안에서는 오버라이딩 된 메소드의 호출이 가능하다.
        System.out.println("Yummy CheeseCake");
    }

    public void tasty() {
        super.yummy();  // Cake의 yummy 메소드 호출
        System.out.println("Yummy Tasty Cake");
    }
}

public class YummyCakeSuper {
    public static void main(String[] args) {
        CheeseCake cake = new CheeseCake();

        cake.yummy();
        System.out.println();
        cake.tasty();
    }
}

15-3. instanceof 연산자

연산자 instanceof는 참조변수가 참조하는 인스턴스의 ‘클래스’나 참조하는 인스턴스가 ‘상속하는 클래스’를 묻는 연산자이다.

- instanceof 연산자의 기본

if (ca instanceof Cake) {...}

ca가 참조하는 인스턴스가

  1. Cake의 인스턴스이거나
  2. Cake를 상속하는 클래스의 인스턴스이면
    true를, 그렇지 않으면 false를 반환한다.

- instanceof 연산자의 활용

다음과 같은 메소드를 정의하고 싶다고 가정하자!
image

class Box {
    public void simpleWrap() {
        System.out.println("Simple Wrapping");
    }
}

class PaperBox extends Box {
    public void paperWrap() {
        System.out.println("Paper Wrapping");
    }
}

class GoldPaperBox extends PaperBox {
    public void goldWrap() {
        System.out.println("Gold Wrapping");
    }
}

public class Wrapping {
    public static void main(String[] args) {
        Box box1 = new Box();
        PaperBox box2 = new PaperBox();
        GoldPaperBox box3 = new GoldPaperBox();

        wrapBox(box1);
        wrapBox(box2);
        wrapBox(box3);
    }

    public static void wrapBox(Box box) {
        if (box instanceof Box) {
            box.simpleWrap();
        } else if (box instanceof PaperBox) {
            ((PaperBox)box).paperWrap();    // 형 변환 후 메소드 호출
        } else {
            ((GoldPaperBox)box).goldWrap(); // 형 변환 후 메소드 호출
        }
    }
}

(추가) 상속의 의미를 생각하며 refactor

위의 wrapping 예제는 상속의 장점을 전혀 살리지 못한 예시라고 할 수 있다.
상속은 연관된 일련의 클래스들에 대해 ‘공통적인 규약’을 정의할 수 있다.
이를 상기하며 코드를 리펙토링 해보자!

class Box {
    public void wrap() {    // 오버라이딩
        System.out.println("Simple Wrapping");
    }
}

class PaperBox extends Box {
    public void wrap() {    // 오버라이딩
        System.out.println("Paper Wrapping");
    }
}

class GoldPaperBox extends PaperBox {
    public void wrap() {    // 오버라이딩
        System.out.println("Gold Wrapping");
    }
}

public class Wrapping {
    public static void main(String[] args) {
        Box box1 = new Box();
        PaperBox box2 = new PaperBox();
        GoldPaperBox box3 = new GoldPaperBox();

        wrapBox(box1);  // Simple Wrapping
        wrapBox(box2);  // Paper Wrapping
        wrapBox(box3);  // Gold Wrapping
    }

    public static void wrapBox(Box box) {
        box.wrap();
    }
}

wrapBox 메소드를 보면
기존의 instanceof를 활용하여 실제 참조하고 있는 인스턴스가 무엇인지 구분하는 복잡한 코드를
메소드 오버라이딩을 활용하여 간단하게 줄였다.

Java lang 카테고리 내 다른 글 보러가기

댓글 남기기