Ch7 - 다형성 TireEx
프로그램 요구사항
1. 자동차의 바퀴는 앞왼쪽, 앞오른쪽, 뒤왼쪽,뒤오른쪽 네 가지이다.
2. 각 바퀴 생명의 초기값은 앞왼쪽부터 6, 2, 3, 4으로 주어진다.
3. 자동차가 총 5회 달린다.
3-1. 자동차가 한번 달릴 때마다 각 바퀴의 수명이 하나씩 줄어든다.
(각 바퀴는 앞왼쪽,앞오른쪽,뒤왼쪽, 뒤오른쪽 순으로 구른다고 가정한다.)
3-1-1. 수명이 다할 경우, 펑크 메시지를 출력하고 자동차가 멈춘다.
3-1-1-1.해당 바퀴를 다른 부품(앞바퀴 - HankookTire, 뒷바퀴 - KumhoTire)으로 교체한다.
(앞왼쪽HT(15) 앞오른쪽HT(14) 뒤왼쪽KT(13) 뒤오른쪽KT(17))
3-1-2. 바퀴의 수명을 출력한다.
*이번 예제의 포인트는 다형성을 이용해 타이어를 부모 클래스에 표준화시킨 뒤,
교체할 때 부품화된 자식 클래스를 사용한다는 것이다.
표준이 되는 frontLeftTire 등이 Tire타입(상위클래스)을 가지기 때문에
부품(하위클래스)만 바꿔 끼워도 문제없이 작동할 수 있다.
표준은 부모타입으로 만들어두었다가 Tire frontRightTire = new Tire();
교체시 자식 타입으로 교체한다 car.frontRightTire = new kumhoTire;
(자식객체의 주소값을 부모타입의 필드에 저장하여 사용 )
클래스 설계
CarRun
// 메소드 main() → new Car() 호출
타이어 교체 → 앞왼쪽HankookTire(15) 앞오른쪽HankookTire(14) 뒤왼쪽KumhoTire(13) 뒤오른쪽KumhoTire(17)
Car
// 메소드 run() : 자동차가 달립니다 → roll(수명,위치) 앞왼쪽6 앞오른쪽2 뒤왼쪽3 뒤오른쪽4
Tire
// 필드 : 수명, 위치
// 생성자 roll(수명,위치) : 생성자가 호출될 때 외부에서 값을 받아 다양하게 초기화한다. 수명이 남았는지 확인 → 펑크메시지 혹은 현재 수명 출력
//메소드 stop() : 펑크시 자동차가 멈춥니다.
HankookTire (KumhoTire 동일)
//생성자 @Override roll()
//메소드 @Override stop()
코드작성
1. Business Class
Car 객체를 생성하고 가독성을 위해 교체 이전까지의 기능은 Car에서 실행하도록 클래스를 단순화한다.
실제로 car.run()를 실행할 때 Tire 클래스와 상호작용하기 때문에 실행클래스는 간단하게 쓸 수 있다.
주목해야할 부분은 앞서 언급했듯 타이어를 Kumho, Hankook으로 교체하기 위해 다형성객체를 만들었다는 점
교체 기능을 좀더 구체적으로 뜯어보자면, frontLeftTire은 Car의 참조변수로, 생성자를 호출하는 명령어를 담고 있다.
Car객체가 생성될 때 필드를 초기화하기 위한 4개의 Tire객체가 생성되고 주소값도 4개가 리턴되는데 그 중 하나가 frontLeftTire에 담긴다.
즉 frontLeftTire은 Car객체생성 시점에 파생되어 생성된 Tire객체를 참조하는 하나의 주소값을 저장하고 있는데, 좀 더 쉬운말로는 기본 타이어의 특정 위치객체를 찾아가는 주소를 저장하고 있는 공간이다. 이 공간에 새로운 객체 HankookTire을 생성해 담는다면, frontLeftTire이란 공간은 그대로 사용하되 내용물을 바꾸게 된다. 물론 HankookTire가 Tire의 자식클래스로 Tire이 가진 속성,동작(roll)을 모두 포함하고 있기에 가능하다.
코드적으로는 Tire 타입의 frontLeftTire 변수에 자식 클래스인 HankookTire를 대입하기 때문에 이는 자식 클래스의 부모타입으로의 변환인 자동형변환에 속한다. 즉 다형성 객체를 생성한 것이다.
이제 frontLeftTire은 새로운 자식 객체를 찾아가 새롭게 생겨난 부모 객체에 super.키워드를 통해 필드를 초기화한다. 즉, 브랜드타이어가 가진 속성과 부모객체를 그대로 이용한다.
package p8;
public class CarExample {
public static void main(String[] args) {
Car car = new Car();
for(int i=1; i<=5; i++) {
int problemLocation = car.run();
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("뒤왼쪽 kumhoTire로 교체");
car.backLeftTire = new KumhoTire("뒤왼쪽", 14);
break;
case 4:
System.out.println("뒤오른쪽 kumhoTire로 교체");
car.backRightTire = new KumhoTire("뒤오른쪽", 17);
break;
}
System.out.println("-------------------------");
}
}
}
2. Car class(부품)
Tire을 타입으로 하는 필드를 가지고 있다는 게 특이한 점.
처음에는 뭐야이거 Tire클래스안으로 가야하는 필드들 아닌가 싶었지만, 사실상 Tire 클래스의 필드를 다양하게 초기화해주고 Tire클래스와 연결하기 위한 참조변수일뿐인다. 다양한 값을 가진 타이어 객체를 4개 생성시킨다.
Tire클래스에 타이어의 위치에 따라 필드를 4개 만들었다면, 타이어의 수명 또한 4가지를 저장해야하기 때문에 코드가 복잡해질 수밖에 없다.
따라서 이를 해결하기 위해 공통되는 속성을 뽑고, 명시적 생성자를 만들어 외부 값에 따라 다양하게 초기화하도록 했다.
책을 안보고 코드하려하니 데이터의 타입이 int, String으로 다른데 String fl[] = new String[2]로 타이어 위치에 따라 필드를 배열로 4개 만들어야 하나 싶었는데, 그럼 각 필드별로 roll()도 반복적으로 작성해야 할 것이다. 매우 비효율적이다. 이점을 기억하며 앞으로 클래스 상세설계에 참고해야지
package p8;
public class Car {
//필드
Tire frontLeftTire = new Tire("앞왼쪽", 6); //Tire 객체 4개를 생성하면서 초기화
Tire frontRightTire = new Tire("앞오른쪽",2);
Tire backLeftTire = new Tire("뒤왼쪽",3);
Tire backRightTire = new Tire("뒤오른쪽",4);
//생성자
//메소드
int run() {
System.out.println("[자동차가 달립니다.]");
if(frontLeftTire.roll() == false) {stop(); return 1;}
if(frontRightTire.roll() == false) {stop(); return 2;}
if(backLeftTire.roll() == false ) {stop(); return 3;}
if(backRightTire.roll() == false) {stop(); return 4;}
return 0;
}
void stop() {
System.out.println("[자동차가 멈춥니다.]");
}
}
3. Tire, KumhoTire, HankookTire(부품)
특별히 볼 건 없고 Car의 필드로 Tire객체가 각각 생성될 때 값을 초기화하기 위한 명시적 생성자가 존재한다.
package p8;
public class Tire {
//필드
public int maxRotation; //최대회전수(타이어수명)
public int accumulatedRotation; //누적 회전수
public String location; //타이어의 위치
//생성자
public Tire(String location, int maxRoation) { //초기화
this.location = location;
this.maxRotation = maxRoation;
}
//메소드
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation<maxRotation) {
System.out.println(location + " Tire 수명: " +
(maxRotation-accumulatedRotation) + "회");
return true;
} else {
System.out.println("*** " + location + " Tire 펑크 ***" );
return false;
}
}
}
package p8;
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;
}
}
}
package p8;
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;
}
}
}
배열로 관리한다면 코드가 더욱 단순화된다. (Car, CarExample클래스 수정)
package BookExampleHomeWork.carRun.array;
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);
} //tires[i].roll()의 리턴값이 true(즉 펑크가 없다면) 수행문을 실행하지않고 다음 반복횟수로
}
return 0; //모든 바퀴를 점검하면 return 0;으로 메소드를 끝냄
}
void stop() {
System.out.println("[자동차가 멈춥니다]");
}
}
단, 반복문 안에 하나의 if문을 사용할 경우 교체부품을 한 가지로만 가능하다. KumhoTire도 하고 싶다면 if문 두개로 작성하자.
package BookExampleHomeWork.carRun.array;
public class CarRun {
public static void main(String[] args) {
Car car = new Car();
for (int i = 1; i <= 5; i++) {
int problemLocation = car.run();
if (problemLocation != 0) {
car.tires[problemLocation - 1] = new HankookTire(15, car.tires[problemLocation - 1].location);
System.out.println(car.tires[problemLocation - 1].location + " HankookTire로 교체");
}
System.out.println("===============");
}
}
}
배열 + 교체타이어의 브랜드를 4가지로 단순화시키고, 사용자에게 선택권을 주었을때(표준입력) 코드 수정
표준입력을 받는 기능만을 choiceTire()로 만들고 하단으로 뺀다.
package p9;
import java.util.Scanner;
public class CarExample {
public static void main(String[] args) {
Car car = new Car();
for (int i = 1; i <= 6; i++) {
int problemLocation = car.run();
if (problemLocation != 0) {
// 키보드로 입력받아 타이어 회사 고르기
int c = choiceTire();
switch (c) {
case 1:
System.out.println(car.tires[problemLocation - 1].location + "HankookTire로 교체");
car.tires[problemLocation - 1] = new HankookTire(car.tires[problemLocation - 1].location, 15);
break;
case 2:
System.out.println(car.tires[problemLocation - 1].location + "KumhoTire로 교체");
car.tires[problemLocation - 1] = new KumhoTire(car.tires[problemLocation - 1].location, 13);
break;
case 3:
System.out.println(car.tires[problemLocation - 1].location + "MichelinTire로 교체");
car.tires[problemLocation - 1] = new Michelin(car.tires[problemLocation - 1].location, 17);
break;
case 4:
System.out.println(car.tires[problemLocation - 1].location + "SnowTire로 교체");
car.tires[problemLocation - 1] = new SnowTire(car.tires[problemLocation - 1].location, 10);
break;
}
}
System.out.println("---------------------");
}
}
private static int choiceTire() {
Scanner sc = new Scanner(System.in);
System.out.println("교체할 타이어 회사를 선택하세요");
System.out.println("1. 한국타이어 2.금호타이어 3.미쉐린 4.스노우타이어");
int choice = sc.nextInt();
return choice;
}
}