클래스의 상속 2: 오버라이딩

gustjtmd·2022년 1월 29일
0

Java

목록 보기
10/40

상속을 위한 두 클래스의 관계

두 클래스의 상속으의 관계로 맺는 것이 도움이 되는 상황이 있고 도움이 되지 않는 상황이 있다. 그렇다면 언제 두 클래스를 상속의 관계로 맺어야 할까?

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

상속이 갖는 문법적 특성을 통해서 상위 클래스와 하위 클래스를 다음과 같이 이야기 할 수 있다.
'하위 클래스는 상위 클래스의 모든 특성을 지닌다.'
'거기에 더하여 하위 클래스는 자신만의 추가적인 특성을 더하게 된다'

이러한 '상속'의 특성을 현실세계에서도 찾아볼 수 있다.

모바일폰 vs 스마트콘

'모바일폰'이 상위 클래스라면 '스마트폰'은 하위 클래스이다. 이 둘을 객체지향 관점에서 보면
다음과 같이 이야기 할 수 있다

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

스마트폰은 모바일폰이 갖는 특성을 모두 갖는다 게다가 스마트폰은 앱의 설치 및 실행 등 컴퓨터의
특성을 추가적으로 갖고있다 따ㅏ서 클래스를 설계한다면

class 스마트폰 extends 모바일폰{...}

우리는 '스마트폰도 모바일폰의 한 종류'라 말한다 즉 컴퓨터의 기능이 추가된 모바일폰이
스마트폰인 것이다 따라서 다음과 같이 이야기 할 수 있다.

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

그리고 위 문장들이 나타나는 관계를 가리켜 'IS-A 관계'라 하고 이것이 상속의 관계를
맺기 위한 두 클래스의 기본 조건이 된다. is a 는 '~은 ~이다'로 해석된다

정리
1) 노트북은 컴퓨터이다, 전기자동차는 자동차이다.
2) 상속이 갖는 문법적 특성은 IS-A 관계의 표현에 적합하다
3) 따라서 상속 관계를 형성하기 위한 두 클래스는 IS-A 관계에 있어야 한다.
코드로 확인해보자

class MobilePhone{
    protected String number;    //전화번호

    public MobilePhone(String num){
        number = num;
    }
    public void answer(){
        System.out.println("Hi~ from " +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);
    }
}


public class MobileSmartPhone {
    public static void main(String[] args) {
        SmartPhone phone = new SmartPhone("010-555-555", "Nougat");
        phone.answer(); //전화를 받는다
        phone.playApp(); //앱을 선택하고 실행한다.
    }
}

Hi~ from 010-555-555
App is running in Nougat

-------------------------------------------------------------------------

스마트폰은 모바일폰이 갖는 기능을 모두 갖는다. 그리고 실제로 스마트폰은 모바일폰의 일종이다
따라서 모바일폰은 상위 클래스로 스마트폰은 이를 상속하는 하위 클래스로 설계하는 것은 이치에
맞는일.

메소드 오버라이딩

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

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

class SmartPhone extends MobilePhone{...} 상속할시

MobilePhone phone = new SmaryPhone("..");
MobilePhone형 참조변수가 SmartPhone 인스턴스를 참조하게 할 수 있다.

이렇듯 상위 클래스의 참조변수는 하위 클래스의 인스턴스를 참조할 수 있는데 다음과 같이 이해하자

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

class SmartPhone extends MobilePhone{...} 처럼 상속관계가 형성되면

다음 인스턴스는 SmartPhone 인스턴스인 동시에 MobilePhone 인스턴스가 된다.

코드로 확인해보자

class MobilePhone{
    String number;
    
    public MobilePhone(String num){
        number = num;
    }
    public void answer(){
        System.out.println("Hi~from "+number);
    }
}

class SmartPhone extends MobilePhone{
    private String andriodVer;
    
    public SmartPhone(String num, String ver){
        super(num);
        andriodVer = ver;
    }
    public void playApp(){
        System.out.println("App is running in " + andriodVer);
    }
}

public class MobileSmartPhoneRef {
    public static void main(String[] args) {
        SmartPhone ph1 = new SmartPhone("010-555-777","Nougat");
        MobilePhone ph2 = new SmartPhone("010-999-333","Nougat");
        
        ph1.answer();
        ph1.playApp();
        System.out.println();
        
        ph2.answer();
        //ph2.playApp(); 컴파일 에러
    }
}

Hi~from 010-555-777
App is running in Nougat

Hi~from 010-999-333

--------------------------------------------------------------------------

MobilePhone ph2 = new SmartPhone("010-999-333","Nougat");

ph2.answer(); 호출은 MobilePhone 클래스에 정의된 메소드이기 때문에 가능하다

그러나 SmartPhone 클래스에 정의된 메소드 호출은 불가능하다
참조변수 ph2가 실제 참조하는 인스턴스가 SmartPhone 인스턴스이지만 불가능하다

ph2.playApp();	//불가능

참조변수 ph2는 MobilePhone형 참조변수이다 이러한 경우 ph2를 통해서 접근이 가능한 멤버는
MobilePhone 클래스에 정의되어 있거나 이 클래스가 상속하는 클래스의 멤버로 제한된다.

두가지 이유가 있는데
1. 실행 시간을 늦추는 결과로 이어질 수 있기 때문
2. 참조변수의 형을 기준으로 접근 가능한 멤버를 제한하는 것은 코드르 단순하게 하기 때문

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

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

이떄 StrawberryCheeseCake 인스턴스는 다음과 같이 말 할수 있다.
"StrawberryCheeseCake 인스턴스는 CheeseCake 인스턴스이면서 Cake 인스턴스이다"

따라서 다음과 같이 참조 가능하다
Cake cake1 = new StrawberryCheeseCake();
CheeseCake cake2 = new StrawberryCheeseCake();

그러나 Cake형 참조변수 cake1을 통해서 호출할수 있는 메소드는
cake1.sweet(); 한가지이다
-> Cake에 정의된 메소드 호출

그리고 마찬가지로 CheeseCake형 참조변수 cake2를 통해서 호출할수 있는 메소드는 두가지
cake2.sweet();
-> Cake에 정의된 메소드 호출

cake2.milky();
-> CheeseCake에 정의된 메소드 호출

이렇듯 참조변수가 참조하는 인스턴스의 종류에 상관없이 참조변수 형에 해당하는 클래스와
그 클래스가 상속하는 상위 클래스에 정의된 메소드만 호출 가능

참조변수간 대입과 형 변환

상속 관계를 맺은 두 클래스가 존재한다.

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

이상황에서 다음과 같은 형태의 참조변수 사이의 대입은 가능하다.

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

그러나 다음과 같은 형태는 불가하다

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

우리는 위의 대입을 허용해도 된다는 사실을 알지만 컴파일러는 '참조변수의 형'만을 가지고 
대입의 가능성을 판단한다

"자바는 참조변수의 형 정보를 기준으로 대입의 가능성을 판단한다."

컴파일러는 다음과 같은 수준에서 바라보고 대입의 가능성을 판단한다

Cake ca3 = ?
CheeseCake ca4 = ca3;	//불가능

이 경우 ca3이 참조하는 인스턴스가 CheeseCake 인스턴스임을 확신할수 없기 떄문에 허용하지 
않는다. 따라서 명시적으로 형변환을 대입하면 가능하다

Cake ca3 = ?
CheeseCake ca4 = (CheeseCake)ca3;	//가능

이는 ca3이 참조하는 인스턴스다 CheeseCake 인스턴스임을 프로그래머가 보장한다는 의미이다
따라서 컴파일러는 허용하지만 이러한 형 변환을 진행하는 경우 치명적인 실수가 발생하지 않도록
조심해야한다.

클래스의 상속과 참조변수의 참조 가능성 : 배열 관점에서 정리

다음과 같이 상속관계를 맺은 클래스가 존재한다.

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

이때 참조 관계는 배열까지도 이어진다

Cake[] cakes = new CheeseCake[10];

이렇듯 상속 관계에서 두 클래스의 참조 관계가 배열까지 이어진다는 사실을 기억하자.

메소드 오버라이딩

상위 클래스에 정의된 메소드를 하위 클래스에서 다시 정의하는 행위를 가리켜 '메소드 오버라이딩'
이라 한다. 여기서 말하는 오버라이딩은 '무효화 시키다'라는 뜻으로 해석된다 
코드로 확인해보자

class Cake{
    public void yummy(){
        System.out.println("Yummy Cake");
    }
}
class CheeseCake extends Cake{
    public void yummy(){    //Cake의 yummy 메소드를 오버라이딩함
        System.out.println("Yummy Cheese Cake");
    }
}
public class YummyCakeOverriding {
    public static void main(String[] args) {
        Cake c1 = new CheeseCake();
        CheeseCake c2 = new CheeseCake();

        c1.yummy(); //오버라이딩한 CheeseCake의 yummy 메소드 호출됨
        c2.yummy(); //오버라이딩한 CheeseCake의 yummy 메소드 호출됨

        System.out.println();
        Cake c3 = new Cake();
        c3.yummy(); //Cake의 yummy 메소드 호출됨.
    }
}

Yummy Cheese Cake
Yummy Cheese Cake

Yummy Cake

--------------------------------------------------------------------

CheeseCake의 클래스는 Cake를 상속하면서 Cake의 yummy메소드와 다음 세 가지 같은 
메소드를 정의하였다.
1. 메소드의 이름
2. 메소드의 반환형
3. 메소드의 매개변수 선언

이 세가지가 같아야 '메소드 오버라이딩'이 성립한다.Cake의 yummy 메소드를 CheeseCake의 yummy 메소드가 오버라이딩 하였다 그리고
오버라이딩을 하면 참조변수의 형에 상관없이 오버라이딩 한 메소드가(CheeseCake의 yummy메소드)
오버라이딩된 메소드(CakeYummy 메소드를) 대신하게 된다.

앞서 배운 바에 의하면 c1은 Cake형 참조변수이니 위 문장의 경우 Cake의 yummy메소드가
호출되어야 하는데 CheeseCake의 메소드 오버라이딩을 통하여 무효화 되었다
떄문에 CheeseCake yummy 대신 메소드가 호출된다.

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

클래스 내부에서 상위 클래스에 정의된 오버라이딩된 메소드의 호출 목적으로 super가 사용된다

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

class CheeseCake extends Cake{
    public void yummy(){
        super.yummy();  //Cake의 yummy 메소드 호출
        System.out.println("Yummy Cheese Cake");
    }
    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();
    }
}

Yummy Cake
Yummy Cheese Cake

Yummy Cake
Yummy Tasty Cake

인스턴스 변수와 클래스 변수도 오버라이딩의 대상이 될까?

상위 클래스에 선언된 변수와 동일한 이름의 변수를 하위 클래스에서 선언하는 일은 피해야한다
이는 코드에 혼란을 가져올 수 있기 때문이다.
코드로 확인해보자

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;    //cheese size

    public CheeseCake(int sz1, int sz2){
        super(sz1);
        size = sz2;
    }
    public void showCakeSize(){
        // super.size는 상위 클래스의 멤버 size를 의미함
        System.out.println("Bread Ounces : "+super.size);

        //size는 이 클래스 CheeseCake의 멤버 size를 의미함
        System.out.println("Cheese Ounces : "+size);
    }
}
public class YummyCakeSize {
    public static void main(String[] args) {
        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("Cheese Ounces: "+ca1.size);
        System.out.println();

        ca1.showCakeSize();
        System.out.println();
        ca2.showCakeSize();
    }
}

Bread Ounces: 5
Cheese Ounces: 7

Bread Ounces : 5
Cheese Ounces : 7

Bread Ounces : 5
Cheese Ounces : 7

----------------------------------------------------------------------
변수는 오버라이딩 되지 않는다 따라서 '참조 변수의 형'에 따라서
접근하는 변수가 결정된다.

CheeseCake c1 = new CheeseCake();	
c1.size=...	//CheeseCake의 size에 접근

Cak2 c2 = new CheeseCake();	
c2.size=...	//Cake의 size에 접근

이러한 특성은 클래스 변수와 클래스 메소드는 마찬가지이다.
이 두가지도 참조변수의 따라서 접근하는 클래스 변수와 메소드가 결정된다.
따라서 오버라이딩 대상이 아니다.

instanceof 연산자

instanceof 연산자의 기본

연산자 instanceof는 참조변수가 참조하는 인스턴스의 '클래스'나 참조하는 인스턴스가
'상속하는 클래스'를 묻는 연산자이다 다음과 같이 문장을 구성할 수 있다.

if(참조변수 instanceof 클래스)

만약 참조하는 상속하는 클래스의 인스턴스이면 true 그렇지 않으면 false 반환한다 
코드로 확인해보자

class Cake{}
class CheeseCake extends Cake{}
class StraberryCheeseCake extends CheeseCake{}


public class YummyCakeOf {
    public static void main(String[] args) {
        Cake cake = new StraberryCheeseCake();

        if(cake instanceof Cake){
            System.out.println("참조변수 cake는 클래스 Cake를 상속합니다.");
        }
        if(cake instanceof CheeseCake){
            System.out.println("참조변수 cake는 클래스 CheeseCake를 상속합니다");
        }
        if(cake instanceof StraberryCheeseCake){
            System.out.println("참조변수 cake는 클래스 StraberryCheeseCake를 상속합니다");
        }
    }
}

참조변수 cake는 클래스 Cake를 상속합니다.
참조변수 cake는 클래스 CheeseCake를 상속합니다
참조변수 cake는 클래스 StraberryCheeseCake를 상속합니다

instanceof 연산자의 활용

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 GoldPaperBox){
            ((GoldPaperBox)box).goldWrap();	//형 변환 후 메소드 호출
        }
        else if(box instanceof PaperBox){
            ((PaperBox)box).paperWrap();	//형 변환 후 메소드 호출
        }
        else{
            box.simpleWrap();
        }
    }
}

Simple Wrapping
Paper Wrapping
Gold Wrapping


----------------------------------------------------------------------

위에서 정의한 세 클래스의 인스턴스 모두 메소드의 인자로 전달될 수 있다.

public static void wraBox(Box box){
	box가 Box 인스턴스를 참조하면 simpleWrap 메소드 호출
    	box가 PaperBox 인스턴스를 참조하면 paperWrap 메소드 호출
        box가 GoldPaperBox 인스턴스를 참조하면 goldWrap 메소드 호출
}
profile
반갑습니다

0개의 댓글