들어가며
메소드 오버라이딩은 상위
클래스에 정의된 메소드를
하위 클래스에서 다시 정의하는
것을 뜻한다.
이렇듯 메소드 오버라이딩이
문법적으로는 단순하지만
이것이 가져다주는 이점은
결코 가볍지 않다.
상위 클래스의 참조변수가 참조할 수 있는 대상의 범위
앞서 다음과 같이 SmartPhone
클래스가
MobilePhone 클래스를 상속하는
형태로 클래스를 디자인한 바 있다.
class SmartPhone extends MobilePhone {....}
따라서 다음과 같이 문장을
구성할 수 있다.
SmartPhone phone = new SmartPhone("010-3333-6666", "Nougat");
그런데 다음과 같이 MobilePhone형
참조변수가
SmartPhone 인스턴스를 참조할
수도 있다.
MobilePhone phone = new SmartPhone("010-3333-6666", "Nougat");
이렇듯 상위 클래스의 참조변수는
하위 클래스의 인스턴스를 참조할
수 있는데,
이 부분을 다음과 같이 이해하자.
- 모바일폰을 상속하는 스마트폰도 일종의 모바일폰이다.
→ MobilePhone을 상속하는 SmartPhone 인스턴스는 MobilePhone 인스턴스이기도 하다. - 따라서 MobilePhone형 참조변수는 SmartPhone 인스턴스를 참조할 수 있다.
부연하면, 다음과 같이 상속
관계가 형성이 되면,
class SmartPhone extends MobilePhone {....}
다음 인스턴스는 SmartPhone
인스턴스인 동시에,
MobilePhone 인스턴스가 된다.
이는 스마트폰을 가리켜 '모바일폰이다.'라고 말할 수 있는것과 같은 이치다.
new SmartPhone("010-3333-6666", "Nougat");
// SmartPhone 인스턴스이면서 동시에 MobilePhone 인스턴스
따라서 다음과 같이 인스턴스를
참조할 수 있을 뿐 아니라,
SmartPhone phone = new SmartPhone("010-3333-6666", "Nougat");
다음과 같이 인스턴스를 참조하는
것도 가능하다.
MobilePhone phone = new SmartPhone("010-3333-6666", "Nougat");
그럼 지금까지 설명한 문법적
특성을
다음 예제를 통해서 정리해
보이겠다.
class MobilePhone {
protected String number;
public MobilePhone(String num) {
number = num;
}
public void answer() {
System.out.println("number: " + number);
}
}
class SmartPhone extends MobilePhone {
private String androidVer;
public SmartPhone(String num, String ver) {
super(num);
androidVer = ver;
}
public void playApp() {
System.out.println("App is running in " + androidVer);
}
}
class MobileSmartPhone {
public static void main(String[] args) {
SmartPhone ph1 = new SmartPhone("010-3333-6666", "Nougat");
MobilePhone ph2 = new SmartPhone("010-4444-8888", "Nougat");
ph1.answer();
ph1.playApp();
System.out.println();
ph2.answer();
//ph2.playApp();
}
}
/* 실행 결과
number: 010-3333-6666
App is running in Nougat
number: 010-4444-8888
*/
위 예제에서는 다음과 같이
인스턴스를 생성하였다.
MobilePhone ph2 = new SmartPhone("010-4444-8888", "Nougat");
그리고 다음과 같이
MobilePhone 클래스에
정의된 메소드를 호출하는데
이는 당연히 가능한 일이다.
ph2.answer();
그러나 다음과 같이
SmartPhone 클래스에
정의된 메소드의 호출은
불가능하다.
참조변수 ph2가 실제 참조하는
인스턴스가 SmartPhone
인스턴스지만 불가능하다.
ph2.playApp();
참조변수 ph2는 MobilePhone 형
참조변수이다.
이러한 경우 ph2를 통해서
접근이 가능한 멤버는
MobilePhone 클래스에
정의되었거나
이 클래스가 상속하는 클래스의
멤버로 제한된다.
ph2가 참조하는 인스턴스가 무엇인지는 상관이 없다.
지금 설명한 이 내용이
비합리적이라고 생각할 수 있다.
참조변수의 형(Type)에
상관없이,
참조하는 인스턴스에 따라서
접근가능한 멤버가 결정되어야
한다고 생각할 수 있다.
그러나 그렇게 설계하지 않은
이유가 2가지가 있는데,
그중 하나는 다음과 같다.
1. 실행 시간을 늦추는 결과로 이어질 수 있다.
자바는 메소드 호출 시
'참조변수의 형(Type)을 참조'하여
그 메소드의 호출이 옳은 것인지
판단한다.
예를 들자면 다음과 같다.
다음과 같이 컴파일러가 판단하고 컴파일을 한다.
ph2.answer();
// ph2가 MobilePhone 형이므로 MobilePhone 클래스의 메소드 answer는 호출 가능
이러한 형태의 판단은
그 속도가 빠르다.
컴파일 단계에서 쉽게 판단이 가능하다.
그러나 실제 참조하는
인스턴스를 대상으로
메소드의 호출 가능성을
판단하는 일은 간단하지 않다.
참조하는 인스턴스의 종류는
코드의 흐름에 따라 얼마든지
달라질 수 있기 때문이다.
그런데 이러한 단점도 감수할
만한 가치가 있다면 감수했을
것이다.
그러나 이어서 언급하는
2번째 이유는
이러한 단점을 감수할 필요가
없다는 결론을 내리게 한다.
다음 Chapter의 내용까지 공부해야 이 내용을 완전히 이해할 수 있다.
2. 참조변수의 형을 기준으로 접근 가능한 멤버를 제한하는 것은 코드를 단순하게 한다.
단점이 많은 일부 기능을
제한함으로써
단순한고 명료한 코드의
작성을 유도하는 언어가
좋은 언어이다.
그런 측면에서 참조변수의
형을 기준으로
접근 가능한 멤버를 제한한
것은 의미가 있는 일이다.
클래스 상속과 참조변수의 참조 가능성에 대한 정리
지금까지 설명한 내용을
정리해보겠다.
이는 지금까지 설명한 내용의
문버벅 결론에 해당한다.
다음과 같이 상속 관계를 맺은
세 클래스가 존재한다고 가정하자.
class Cake {
public void sweet() {....}
}
class CheeseCake extends Cake {
public void milky() {....}
}
class strawberryCheeseCake extends CheeseCake {
public void sour() {....}
}
이때 StrawberryCheeseCake
인스턴스는 다음과 같이
말할 수 있다.
StrawberryCheeseCake 인스턴스는
CheeseCake 이면서
Cake 인스턴스이다.
따라서 다음과 같이 인스턴스를
참조할 수 있다.
Cake cake1 = new StrawberryCheeseCake();
CheeseCake cake2 = new StrawberryCheeseCake();
그러나 Cake형 참조변수
cake1을 통해서
호출할 수 있는 메소드는
다음 한 가지다.
cake1.sweet();
// Cake 클래스에 정의된 메소드 호출
그리고 CheeseCake형 참조변수
cake2를 통해서
호출할 수 있는 메소드는
다음 2가지이다.
cake2.sweet();
// Cake 클래스에 정의된 메소드 호출
cake2.milky();
// CheeseCake 클래스에 정의된 메소드 호출
이렇듯 참조변수가 참조하는
인스턴스의 종류에 상관없이,
참조변수의 형에 해당하는
클래스와
그 클래스가 상속하는 상위
클래스에 정의된 메소드들만
호출이 가능하다.
참조변수 간 대입과 형 변환
다음과 같이 상속 관계를 맺은
두 클래스가 존재한다고 가정하자.
class Cake {
public void sweet() {...}
}
class CheeseCake extends Cake {
public void milky() {...}
}
이 상황에서 다음과 같은
형태의 참조변수 사이의
대입은 가능하다.
CheeseCake 인스턴스는
Cake 인스턴스이기도하니
당연히 가능하다.
CheeseCake cake1 = new CheeseCake();
Cake cake2 = cake1; // 가능한 코드
그러나 다음과 같은 형태의
대입은 허용이 안 된다.
Cake cake3 = new CheeseCake();
CheeseCake cake4 = cake3; // 불가능한 코드
물론 우리는 위의 대입을
허용해도 된다는 사실을 안다.
그러나 컴파일러는
'참조변수의 형'만을 가지고
대입의 가능성을 판단한다.
자바는 참조변수의 형(Type) 정보를 기준으로 대입의 가능성을 판단한다.
Cake cake3 = ...
CheeseCake cake4 = cake3; // 불가능한 코드
이 경우 cake3가 참조하는 인스턴스가
CheeseCake 인스턴스임은 확신할 수 없다.
Cake를 상속하는 다른 클래스의 인스턴스일 수도 있다.
따라서 이를 허용하지 않는다.
그러나 다름과 같이 명시적으로
형 변환을 하면 대입이 가능하다.
Cake cake3 = ...
CheeseCake cake4 = (CheeseCake)cake3; // 가능한 코드
이 경우 cake3가 참조하는
인스턴스가
CheeseCake 인스턴스임을
프로그래머가 보장한다는
의미이다.
따라서 컴파일러는 이를 그냥
허용해버린다.
그러니 프로그래머는 이러한
형 변환을 진행하는 경우,
대입의 가능성을 정확히 판단하여
치명적인 실수가 발생하지 않도록
주의해야 한다.
클래스의 상속과 참조변수의 참조 가능성: 배열 관점에서 정리
다음과 같이 상속 관계를 맺은
클래스가 존재한다고 가정하자.
class Cake {
public void sweet() {...}
}
class CheeseCake extends Cake {
public void milky() {...}
}
이때 다음과 같이 인스턴스를
참조할 수 있음에 대해서 이미
설명하였다.
Cake cake = new CheeseCake();
이러한 참조 관계는 배열까지도
이어진다.
즉 다음과 같이 CheeseCake
배열을 생성하고 참조하는 것이
가능하지만,
CheeseCake[] cakes = new CheeseCake[10];
다음과 같이 CheeseCake 배열을
생성하고 참조하는 것도 가능하다.
CheeseCake 배열은 일종의 Cake 배열이다.
Cake[] cakes = new CheeseCake[10];
이렇듯 상속 관계에 있는
두 클래스의 참조관계가
배열 관계까지 이어진다는
사실을 기억하자.
지금 이 내용을 활용하지는
않지만,
본서를 공부하는 과정에서
이러한 유형의 코드를 볼
일이 있으니 말이다.
메소드 오버라이딩(Method Overriding)
상위 클래스에 정의된 메소드를
하위 클래스에서 다시 정의하는
행위를 카리켜
'메소드 오버라이딩'이라 하는데,
여기서 말하는 오버라이딩은
'무효화 시키다.'의 뜻으로
해석이 된다.
그럼 다음 예제를 통해서 메소드
오버라이딩의 결과를 확인하자.
class Cake {
public void eat() {
System.out.println("Eat Cake");
}
}
class CheeseCake extends Cake {
public void eat() {
System.out.println("Eat Cheese Cake");
}
}
class EatCakeOverriding {
public static void main(String[] arg) {
Cake c1 = new CheeseCake();
CheeseCake c2 = new CheeseCake();
c1.eat(); // 오버라이딩한 CheeseCake의 eat 메소드 호출
c2.eat(); // 오버라이딩한 CheeseCake의 eat 메소드 호출
}
}
/* 실행 결과
Eat Cheese Cake
Eat Cheese Cake
*/
다음은 위 예제에서 보인 CheeseCake
클래스이다.
class CheeseCake extends Cake {
public void eat() {
System.out.println("Eat Cheese Cake");
}
}
이 클래스는 Cake를 상속하면서,
Cake에 정의된 eat 메소드와
다음 3가지가 같은 메소드를
정의하였다.
- 메소드의 이름, 메소드의 반환형, 메소드의 매개변수 선언
이 3가지가 같아야 '메소드 오버라이딩'이 성립한다.
즉 Cake의 eat 메소드를
CheeseCake의 eat 메소드가
오버라이딩 하였다.
그리고 오버라이딩을 하면,
참조변수의 형에 상관없이
오버라이딩 한 메소드가
CheeseCake의 eat 메소드가
오버라이딩 된 메소드를
대신하게 된다.
Cake의 eat 메소드를 대신하게 된다.
예제의 main 메소드에서
다음과 같이 Cake형 참조변수로
CheeseCake 인스턴스를 참조하였다.
Cake c1 = new CheeseCake();
그리고 다음과 같이 eat 메소드를
호출하였다.
c1.eat();
앞서 배운 바에 의하면 c1은 Cake형
참조변수이니,
위 문장의 경우 Cake의 eat 메소드가
호출되어야 한다.
CheeseCake 인스턴스를 참조하고
있는 상황이라도 말이다.
그러나 Cake의 eat 메소드는
오버라이딩 되었다.
즉, 무효화 되었다.
따라서 이 경우에는 CheeseCake의
eat 메소드가 대신 호출이 된다.
메소드 오버라이딩의 일반화
메소드 오버라이딩을 유용하게
사용한 사례는 다음 Chapter에서
소개하고,
본 Chapte에서는 메소드
오버라이딩을
문법적으로 이해하는데 초점을
맞추고자 한다.
앞서 설명한 메소드 오버라이팅을
문법적으로 정리하기 위해서
클래스를 다음과 같이 정의하였다.
class Cake {
public void eat() {....}
}
class CheeseCake extends Cake {
public void eat() {....}
} // Cake의 eat 메소드를 오버라이딩 함
class StrawberryCheeseCake extends CheeseCake {
public void eat() {....}
} // CheeseCake의 eat 메소드를 오버라이딩 함
위와 같이 클래스를 정의한 경우
CheeseCake의 참조변수와
인스턴스의 생성문을 다음과
같이 구성할 수 있다.
Cake c1 = new StrawberryCheeseCake();
CheeseCake c2 = new StrawberryCheeseCake();
StrawberryCheeseCake c3 = new StrawberryCheeseCake();
그리고 다음 세 문장이 실행되었을
때 호출되는 메소드는
StrawberryCheeseCake의
eat 메소드이다.
c1.eat(); // StrawberryCheeseCake의 eat 메소드 호출
c2.eat(); // StrawberryCheeseCake의 eat 메소드 호출
c3.eat(); // StrawberryCheeseCake의 eat 메소드 호출
이렇듯 StrawberryCheeseCake
인스턴스를대상으로 오버라이딩 된
eat 메소드를 호출하면, 가장 마지막으로
오버라이딩한 eat 메소드가 호출된다.
오버라이딩 된 메소드를 호출하는 방법
다음 Cake 클래스의 eat 메소드는
하위 클래스 CheeseCake에 의해서
오버라이딩 되었다.
class Cake {
public void eat() {....}
}
class CheeseCake extends Cake {
public void eat() {....}
} // Cake의 eat 메소드를 오버라이딩 함
따라서 다음과 같이 CheeseCake
인스턴스를 생성하여
Cake 클래스에 정의된 eat 메소드를
호출하는 것은 불가능하다.
Cake c1 = new CheeseCake();
// 이 인스턴스를 대상으로는 Cake의 eat 메소드 호출 불가
CheeseCake c2 = new CheeseCake();
// 이 인스턴스를 대상으로는 Cake의 eat 메소드 호출 불가
하지만 클래스 외부가 하닌 내부에서
Cake의 eat 메소드를 호출하는 방법은 있다.
이와 관련하여 다음 예제를 보자.
class Cake {
public void eat() {
System.out.println("Eat Cake");
}
}
class CheeseCake extends Cake {
public void eat() {
super.eat();
System.out.println("Eat Cheese Cake");
}
}
class EatCakeSuper {
public static void main(String[] arg) {
CheeseCake cake = new CheeseCake();
cake.eat();
}
}
/* 실행 결과
Eat Cake
Eat Cheese Cake
*/
지금까지는 상위 클래스의 생성자를
호출할 목적으로 키워드 super를 사용하였다.
그런데 위의 예제에서 보이듯이
상위 클래스에 정의된,
오버라이딩된 메소드의 호출을
목적으로도 super가 사용된다.
인스턴스 변수와 클래스 변수도 오버라이딩의 대상이 되는가?
상위 클래스에 선언된 변수와
동일한 이름의 변수를
하위 클래스에서 선언하는 일은
가급적 피해야 한다.
이는 코드에 혼란을 가져올 수
있기 때문이다.
그러나 문법적인 측면에서 이렇게
변수를 선언했을 때
어떻게 동작하는지 정도는 확인할
필요가 있다.
그럼 이와 관련하여 다음 예제를 보자.
class Cake {
public int size; // cake size
public Cake(int sz) {
size = sz;
}
public void showCakeSize() {
System.out.println("Bread Ounces : " + size);
}
}
class CheeseCake extends Cake {
public int size; // CheeseCake size
public CheeseCake(int sz1, int sz2) {
super(sz1);
size = sz2;
}
public void showCakeSize() {
// super.size는 상위 클래스인 Cake의 멤버 size를 의미함
System.out.println("Bread Ounces : " + super.size);
// size는 현재 클래스인 CheeseCake의 멤버 size를 의미함
System.out.println("Bread Ounces : " + size);
}
}
class EatCakeSize {
public static void main(String[] arg) {
CheeseCake ca1 = new CheeseCake(5, 7);
Cake ca2 = ca1;
// ca2는 Cake형이므로 ca2.size는 Cake의 멤버 size를 의미함
System.out.println("Bread Ounces : " + ca2.size);
// ca1는 CheeseCake형이므로 ca1.size는 CheeseCake의 멤버 size를 의미함
System.out.println("Bread Ounces : " + ca1.size);
System.out.println();
ca1.showCakeSize();
System.out.println();
ca2.showCakeSize();
}
}
/* 실행 결과
Bread Ounces : 5
Bread Ounces : 7
Bread Ounces : 5
Bread Ounces : 7
Bread Ounces : 5
Bread Ounces : 7
*/
위 예제에서 정의한 클래스의 핵심은
다음과 같다.
이렇듯 변수 size가 상위 클래스와
하위 클래스에 모두 선언된 것이 핵심이다.
class Cake {
public int size; // cake size
....
}
class CheeseCake extends Cake {
public int size; // CheeseCake size
....
}
그런데 변수는 오버라이딩이 되지 않는다.
따라서 '참조변수의 형'에 따라서
접근하는 변수가 결정된다.
예를 들어서 다음과 같이 접근하면
CheeseCake의 size에 접근하게 된다.
CheeseCake c1 = new CheeseCake();
c1.size = .... // CheeseCake의 size에 접근
반면에, 다음과 같이 접근하면 Cake의
size에 접근하게 된다.
Cake c2 = new CheeseCake();
c2.size = .... // Cake의 size에 접근
이러한 특성은 클래스 변수와
클래스 메소드의 경우에도
마찬가지이다.
이 두 가지도 참조변수의 형에
따라서
접근하는 클래스 변수와 클래스
메소드가 결정된다.
다시 말해서 클래스 변수와 클래스
메소드도 오버라이딩 대상이 아니다.
참고 및 출처
|