본문 바로가기

Java 기본 문법 - 참조 서적 [이것이 자바다 - 한빛미디어]/5. 상속, 다형성

5. Java 자바 - 필드의 다형성

클래스의 자동 타입 변환이 필요한 이유

 

자식 타입으로 사용하면 될텐데, 부모 타입으로 변환해서 사용하는 이유는??

 

다형성을 구현하는 기술적 방법 때문이다.

 

다형성 : 동일한 타입을 사용하지만, 다양한 결과가 나오는 성질

 

주로 필드의 값을 다양화함으로써 실행 결과가 다르게 나오도록 구현한다.

필드 타입은 변함이 없지만,

실행 도중에 어떤 객체를 필드로 저장하느냐에 따라 실행 결과가 달라질 수 있다. (필드의 다형성)

 

프로그램은 수 많은 객체들이 서로 연결되고 각자의 역할을 한다.

 

이 객체들은 다른 객체로 교체될 수 있어야 한다. (부품 처럼!)

 

ex) 자동차 클래스에 포함된 타이어 클래스

 

자동차 클래스를 처음 설계할 때 사용한 타이어 객체는 언제든지 성능 좋은 타이어 객체로 교체할 수 있어야 한다.

새로 교체되는 타이어 객체는 기존 타이어와 사용 방법은 동일하지만, 실행 결과는 더 좋게 나와야 한다.

 

이것을 프로그램으로 구현하기 위해 상속, 오버라이딩, 타입 변환을 이용하는 것이다.

 

- 상속 : 부모 클래스를 상속하는 자식 클래스는 부모가 가지고 있는 필드와 메소드를 가지고 있으므로

            사용 방법이 동일하다.

 

- 오버라이딩 : 자식 클래스는 부모의 메소드를 오버라이딩(재정의) 해서 메소드의 실행 내용을

                      변경함으로써 더 우수한 실행 결과가 나올 수 있게 한다.

 

- 타입 변환 (교체 가능성) : 자식 타입을 부모 타입으로 변환할 수 있다. (다형성)

 

ex)

Car.java

class Car {
    //필드
    Tire frontLeftTire = new Tire();
    Tire frontRightTire = new Tire();
    Tire backLeftTire = new Tire();
    Tire backRightTire = new Tire();
    
    //메소드
    void run() { . . . }
}

 

Car 클래스에는 4개의 Tire 타입 필드를 가지고 있다.

 

Car 클래스로부터 Car 객체를 생성하면, 4 개의 Tire 필드에 각각 하나씩 Tire 객체가 들어가게 된다.

 

만약 frontRightTire, backLeftTire 를 HankookTire, KumboTire 로 교체할 필요성이 생긴 경우,

아래 코드처럼 교체할 수 있다.

 

Car myCar = new Car();   // myCar 객체 생성

myCar.frontRightTire = new HankookTire();   // 새로운 객체 HankookTire 로 교체
myCar.backLeftTire = new KumhoTire();       // 새로운 객체 KumhoTire 로 교체

myCar.run();

 

Tire 클래스 타입인 frontRightTire, backLeftTire 는 원래 Tire 객체가 저장되어야 하지만,

Tire 의 자식 객체가 저장되어도 문제가 없다.

 

왜냐하면, 자식 타입은 부모 타입으로 자동 타입 변환되기 때문이다.

 

frontRightTire 와 backLeftTire 에 Tire 자식 객체가 저장되어도 Car 객체는 Tire 클래스에 선언된 필드와 메소드만 사용하므로 전혀 문제가 되지 않는다.

 

HankookTire 와 KumhoTire 는 부모인 Tire 의 필드와 메소드를 가지고 있기 때문이다. (상속)

 

Car 객체에 run( ) 메소드가 있고, run( ) 메소드는 각 Tire 객체의 roll( ) 메소드를

아래와 같이 호출한다고 가정해보면,,

 

void run() {
    frontLeftTire.roll();
    frontRightTire.roll();
    backLeftTire.roll();
    backRightTire.roll();
}

 

frontRightTire 와 backLeftTire 를 교체하기 전에는 Tire 객체의 roll( ) 메소드가 호출되지만,

HankookTire 와 KumhoTire 로 교체된 후에는 HankookTire 와 KumhoTire 객체의 roll( ) 메소드가 호출된다.

 

만약, HankookTire, KumhoTire 가 roll( ) 메소드를 재정의(오버라이딩) 했다면,

재정의된 roll( ) 메소드가 호출된다.

 

자동 타입 변환을 이용해서 Tire 필드값을 교체함으로써

Car 의 run( ) 메소드 수정 없이도 다양한 roll( ) 메소드의 실행 결과를 얻게 된다. (필드의 다형성)

 


위의 내용들을 예제로 작성!!

 

1. Tire 클래스

 

- 필드 : maxRotation(최대 회전수), accumulatedRotation(누적 회전수), location(타이어의 위치)

 

최대 회전수만큼 도달하면 타이어가 펑크난다고 가정한다. (타이어의 수명)

 

누적 회전수 : 타이어가 1번 회전할 때 마다 1씩 증가되는 필드로

                    누적 회적수가 최대 회전수값 만큼 도달하면 타이어는 펑크난다.

 

타이어 위치 : 앞오른쪽, 앞왼쪽, 뒤오른쪽, 뒤왼쪽 구분하는 필드

 

- 생성자 : 타이어의 위치와 최대 회전수를 매개값으로 받아서 각 필드에 저장한다. (필드 초기화)

 

- 메소드 : roll( )

               타이어를 1회전 시키는 메소드로 1번 실행할 때 마다 누적 회전수를 1씩 증가시킨다.

               누적 회전수가 최대 회전수보다 작을 경우, 남은 회전수를 출력하고

               최대 회전수가 되면 펑크를 출력시킨다.

               리턴 타입 : boolean ( 정상 회전시 true / 펑크 시 false 반환)

 

Tire.java

public class Tire {
    // 필드
    public int maxRotation;          // 최대 회전수 (타이어 수명)
    public int accumulatedRotation;  // 누적 회전수
    public String location;          // 타이어 위치
    
    // 생성자
    public Tire(String location, int maxRotation) {   // 필드 초기화
        this.location = location;
        this.maxRotation = maxRotation;
    }
    
    // 메소드
    public boolean roll() {
        ++accumulatedRotation;  // 누적 회전수 1 증가
        if(accumulatedRotation < maxRotation) {  // 누적 < 최대 일 경우
            System.out.println(location + " Tire 수명 : "+ (maxRotation - accumulatedRotation) + "회");
            return true;
        } else {  // 누적 <= 최대 일 경우
            System.out.println("*** " + location + " Tire 펑크 ***");
            return false;
        }
    }
}   

 

 

2. Car 클래스

 

- 필드 : 4 개의 타이어 (frontLeftTire, frontRightTire, backLeftTire, backRightTire)

 

Tire 객체 생성 시 타이어의 위치와 최대 회전수를 생성자의 매개값으로 지정

 

frontLeftTire 필드일 경우, 최대 회전수 6 을 주어, 6 회전 시 타이어 펑크!

 

- 생성자 : 기본 생성자

 

- 메소드 : run( ) 

             4 개의 타이어를 1 회전 시키는 메소드

             Tire 객체의 roll( ) 메소드를 호출해서 리턴값이 false (펑크) 이면 stop( ) 메소드를 호출하고,

             해당 타이어 번호(위치) 를 리턴한다.

             

             stop( )

             타이어가 펑크 날 때 자동차를 멈추는 메소드

 

Car.java

public class Car {
    // 필드
    Tire frontLeftTire = new Tire("앞왼쪽", 6);   // 위치 이름과 최대회전수로 객체 생성 및 초기화
    Tire frontRightTire = new Tire("앞오른쪽", 2);
    Tire backLeftTire = new Tire ("뒤왼쪽", 3);
    Tire backRightTire = new Tire ("뒤오른쪽", 4);
    
    // 생성자
    
    // 메소드
    int run() {
        System.out.println("[자동차가 달립니다.]");
        
        //모든 타이어 1회전, 각각 roll() 메소드 호출, 펑크 시 stop() 호출하고 위치 리턴
        if(frontLeftTire.roll() == false) { stop(); return 1; }
        if(frontRightTire.roll() == false) { stop(); return 2; }
        if(backLeftTire.roll() == false) { stop(); return 3; }
        if(bakcRightTire.roll() == false) { stop(); return 4; }
        
        return 0;
    }
    
    void stop() {
        System.out.println("[자동차가 멈춥니다.]");
    }
}

 

 

3. HankookTire, KumhoTire 클래스

 

이 두 클래스는 Tire 클래스를 상속받는다.

 

- 필드 : 없음

 

- 생성자 : 타이어의 위치, 최대 회전수를 매개값으로 받아서 부모인 Tire 클래스의 생성자 호출 시

             넘겨준다.

 

- 메소드 : roll( ) 메소드 오버라이딩

             정상 회전과 펑크 났을 때 출력하는 내용이 다르다.

 

HankookTire.java

public class HankookTire extends Tire {
    // 필드
    
    // 생성자
    public HankookTire(String location, int maxRotation) {
        super(location, maxRotation);
    }
    
    // 메소드
    @Override
    public boolean roll() {
        ++accumulatedRotation;
        if(accumulatedRotation < maxRotation) {
            System.out.println(location " HankookTire 수명: "+ (maxRotation - accumulatedRotation) + "회");
            return true;
        } else {
            System.out.println("*** " + location + " HankookTire 펑크 ***");
            return false;
        }
    }
}

 

KumhoTire.java

public class KumhoTire extends Tire {
    // 필드
    
    // 생성자
    public KumhoTire(String location, int maxRotation) {
        super(location, maxRotation);
    }
    
    //메소드
    @Override
    public boolean roll() {
        ++accumulatedRotation;
        if(accumulatedRotation < maxRotation) {
            System.out.println(location " KumhoTire 수명: "+ (maxRotation - accumulatedRotation) + "회");
            return true;
        } else {
            System.out.println("*** " + location + " KumhoTire 펑크 ***");
            return false;
        }
    }
}

 

 

CarExample.java : 실행 클래스

public class CarExample {
    public static void main(String[] args) {
        Car car = new Car();     // Car 객체 생성
        
        for(int i = 1; i < 5; i++) {
            int problemLocation = car.run();  //Car 객체의 run() 메소드 5번 반복 실행
            
            switch(problemLocation) {
                case 1:
                    System.out.println("앞왼쪽 HankookTire로 교체");
                    car.frontLeftTire = new HankookTire("앞왼쪽", 15);
                    break;
                case 2:
                    System.out.println("앞오른쪽 KumhoTire로 교체");
                    car.frontRightTire = new KumhoTire("앞오른쪽", 13);
                    break;
                case 3:
                    System.out.println("뒤왼쪽 HankookTire로 교체");
                    car.backLeftTire = new HankookTire("뒤왼쪽", 14);
                    break;
                case 4:
                    System.out.println("뒤오른쪽 KumhoTire로 교체");
                    car.backRightTire = new KumhoTire("뒤오른쪽", 17);
                    break;
            }
            
            System.out.println("================================================="); // 구분선
        }
    }
}

 

Car 객체의 해당 타이어를 새로운 HankookTire, KumhoTire 객체로 교체한다.

 

car. frontLeftTire = new HankookTire("앞왼쪽", 15);  // 자동 타입 변환!!

 

교체된 이후에는 Car 객체의 run( ) 메소드가 호출될 때

각 객체 HankookTire, KumhoTire 에서 재정의(오버라이딩) 된 roll( ) 메소드가 실행된다.

 


 

하나의 배열로 객체 관리

 

Car 클래스의 4 개의 타이어 객체를 4 개의 필드로 각각 지정했다.

 

동일한 타입의 값들은 배열로 관리하는 것이 유리하다.

 

따라서 타이어 객체들도 타이어 배열로 관리하는 것이 코드를 더 깔끔하게 할 수 있다.

 

class Car {
    Tire frontLeftTire = new Tire("앞왼쪽", 6);
    Tire frontRightTire = new Tire("앞오른쪽", 2);
    Tire backLeftTire = new Tire("뒤왼쪽", 3);
    Tire backRightTire = new Tire("뒤오른쪽", 4);
}

 

class Car {
    Tire[] tires = {
        new Tire("앞왼쪽", 6),
        new Tire("앞오른쪽", 2),
        new Tire("뒤왼쪽", 3),
        new Tire("뒤오른쪽", 4)
    };
}

 

frontLeftTire = tires[0]

frontRightTire = tires[1]

backLeftTire = tires[2]

backRightTire = tires[3]

 

으로 각각 표현된다.

 

ex) 인덱스 1을 이용해서 앞오른쪽 타이어를 KumhoTire로 교체하려면 아래 처럼 코드를 작성한다.

 

tires[1] = new KumhoTire("앞오른쪽", 13);

// 기존 : Tire frontRightTire = new KumhoTire("앞오른쪽", 13);

 

tires 배열의 각 항목은 Tire 타입이므로 자식 객체인 KumhoTire 를 대입하면 자동 타입 변환이 발생한다.

 

배열의 타입은 Tire 이지만, 실제 저장 항목이 Tire의 자식 객체라면 모두 가능하다.

 

상속 관계에 있는 객체들을 배열로 관리하면 제어문에서 쉽게 사용할 수 있다.

 

ex) 전체 타이어의 roll( ) 메소드를 호출하는 Car 클래스의 run( ) 메소드는 아래와 같이 for 문으로 작성할 수 있다.

 

int run() {
    System.out.println("[자동차가 달립니다.]");
    for(int i = 0; i < tires.length; i++) {
        if(tires[i].roll() == false) {         // if 문을 여러개 쓸 필요가 없다!!
            stop();
            return (i + 1);
        }
    }
    return 0;
}       

 

배열 관리로 수정된

Car.java

public class Car {
    // 필드
    Tire[] tires = {
        new Tire("앞왼쪽", 6),
        new Tire("앞오른쪽", 2),
        new Tire("뒤왼쪽", 3),
        new Tire("뒤오른쪽", 4)
    };
    
    // 메소드
    int run() {
        System.out.println("[자동차가 달립니다.]");
        
        for(int i = 0; i < tires.length; i++) {
            if(tires[i].roll() == false) {
                stop();
                return (i + 1);
            }
        }
        
        return 0;
    }
    
    void stop() {
        System.out.println("[자동차가 멈춥니다.]");
    }
}

 

CarExample.java : 실행 클래스 아래와 같이 간결하게 바꿀 수 있다.

public class CarExample {
    public static void main(String[] args) {
        Car car = new Car();
        
        for(int i = 1; i <= 5; i++) {
            int problemLocation = car.run();
            
            if(problemLocation != 0) {
                System.out.println(car.tires[problemLocation - 1].location + " HankookTire 로 교체");
                car.tires[problemLocation - 1] = new HankookTire(car.tires[problemLocation - 1].location, 15);
            }
            
            System.out.println(" ---------------------------- ");
        }
    }
}

 

problemLocation 은 1 ~ 4 값을 가지므로 (return i + 1)

 

Car 클래스의 해당 타이어 인덱스는problemLocation 에서 1을 뺀 0 ~ 3 까지이다.

 

car.tires[problemLocation - 1] = new HankookTire(car.tires[problemLocation - 1].location, 15);

 

타이어 펑크 위치 정보를 읽어서 Car의 tires 배열 항목으로 HankookTire를 대입한다.