"다형성"은 같은 타입이지만 실행 결과가 다양한 객체를 이용할 수 있는 성질을 말한다. 코드 측면에서 보면 다형성은 하나의 타입에 여러 객체를 대입함으로써 다양한 기능을 이용할 수 있도록 해준다. 다형성을 위해 자바는 부모 클래스로 타입 변환을 허용한다. 즉 부모 타입에 모든 자식 객체가 대입될 수 있다. 이것을 이용하면 객체는 부품화가 가능하다. 예를 들어 자동차를 설계할 때 타이어 클래스 타입을 적용했다면 이 클래스를 상속한 실제 타이어들은 어떤 것이든 상관없이 장착(대입)이 가능하다.
타입 변환이란 데이터 타입을 다른 데이터 타입으로 변환하는 행위를 말한다. 기본 타입의 변환에 대해서는 이미 앞에서 학습했다. 클래스 타입도 마찬가지로 타입 변환이 있다. 클래스 타입의 변환은 상속 관계에 있는 클래스 사이에서 발생한다 .자식 타입은 부모 타입으로 자동 타입 변환이 가능하다.
자동 타입 변환
자동 타입 변환(promotion)은 프로그램 실행 도중에 자동적으로 타입 변환이 일어나는 것을 말한다. 자동 타입 변환은 다음과 같은 조건에서 일어난다.
부모클래스 변수 = 자식클래스타입;
자동 타입 변환의 개념은 자식은 부모의 특징과 기능을 상속받기 때문에 부모와 동일하게 취급될 수 있다는 것이다. 예를 들어 고양이는 동물의 특징과 기능을 상속받았다. 그래서 "고양이는 동물이다." 가 성립한다. 아래 예는 자동 타입 변환의 예시이다.
// promotionExample.java
class A {}
class B extends A {}
class C extends A {}
class D extends B {}
class E extends C {}
public class PromotionExample {
public static void main(String[] args) {
B b = new B();
C c = new C();
D d = new D();
E e = new E();
A al = b;
A a2 = c;
A a3 = d;
A a4 = e;
B b1 = d;
C c1 = e;
B b2 = e; // 컴파일 에러(상속 관계에 있지 않음)
C c2 = d; // 컴파일 에러(상속 관계에 있지 않음)
}
}
부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능하다. 비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로만 한정된다. 그러나 예외가 있는데, 메소드가 자식 클래스에서 오버라이딩 되었다면 자식 클래스의 메소드가 대신 호출된다. 이것은 다형성과 관련이 있기 때문에 매우 중요한 성질이므로 잘 알아두어야 한다.
class Parent {
void method1() { ... }
void method2() { ... }
}
class Child extends Parent {
void method2() { ... } // Overriding
void method3() { ... }
}
class ChildExample {
public static void main(String[] args) {
Child child = new Child();
Parent parent = child;
parent.method1(); // (O)
parent.method2(); // (O)
parent.method3(); // (x) 호출 불가능
}
}
Child 객체는 method3() 메소드를 가지고 있지만, Parent 타입으로 변환된 이후에는 method3()을 호출할 수 없다. 그러나 method2() 메소드는 부모와 자식 모두에게 있다. 이렇게 오버라이딩된 메소드는 타입 변환 이후에도 자식 메소드가 호출된다.
필드의 다형성
그렇다면 왜 자동 타입 변환이 필요할까? 그냥 자식 타입으로 사용하면 될 것을 부모 타입으로 변환해서 사용하는 이유가 뭘까? 그것은 타형성을 구현하는 기술적 방법 때문이다. 다형성이란 동일한 타입을 사용하지만 다양한 결과가 나오는 성질을 말한다. 주로 필드의 값을 다양화함으로써 실행 결과가 다르게 나오도록 구현하는데, 필드의 타입은 변함이 없지만, 실행 도중에 어떤 객체를 필드로 저장하느냐에 따라 실행 결과가 달라질 수 있다. 이것이 필드의 다형성이다.
자동차를 구성하는 부품은 언제든지 교체할 수 있다. 부품은 고장 날 수도 있고, 보다 더 성능이 좋은 부품으로 교체되기도 한다. 객체 지향 프로그램에서도 마찬가지이다. 프로그램은 수많은 객체들이 서로 연결되고 각자의 역할을 하게 되는데, 이 객체들은 다른 객체로 교체될 수 있어야 한다. 예를 들어 자동차 클래스에 포함된 타이어 클래스를 생각해보자. 자동차 클래스를 처음 설계할 때 사용한 타이어 객체는 언제든지 성능이 좋은 다른 타이어 객체로 교체할 수 있어야 한다. 새로 교체되는 타이어 객체는 기존 타이어와 사용방법은 동일하지만 실행 결과는 더 우수하게 나와야 할 것이다. 이것을 프로그램으로 구현하기 위해서는 상속과 오버라이딩, 그리고 타입 변환을 이용하는 것이다. 부모 클래스를 상속하는 자식 클래스는 부모가 가지고 있는 필드와 메소드를 가지고 있으니 사용 방법이 동일할 것이고, 자식 클래스는 부모의 메소드를 오버라이딩해서 메소드의 실행 내용을 변경함으로써 더 우수한 실행 결과가 나오게 할 수 있다. 그리고 자식 타입을 부모 타입으로 변환할 수 있다. 필드의 다형성을 코드로 이해해보자.
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와 KumhoTire로 교체할 필요성이 생겼다고 가정하자. 이러한 경우 다음과 같은 코드를 사용해서 교체할 수 있다.
Car myCar = new Car();
myCar.frontRightTire = new HankookTire();
myCar.backLeftTire = new KumhoTire();
myCar.run();
Tire 클래스 타입인 frontRightTire와 backLeftTire는 원래 Tire 객체가 저장되어야 하지만, Tire의 자식 객체가 저장되어도 문제가 없다. 왜냐하면 자식 타입은 부모 타입으로 자동 타입 변환이 되기 때문이다. frontRightTire와 backLeftTire에 Tire 자식 객체가 저장되어도 Car 객체는 Tire 클래스에 선언된 필드와 메소드만 사용하므로 전혀 문제가 되지 않는다. HankookTire와 KumgoTire는 부모인 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() 메소드가 호출된다. 이와 같이 자동 타입 변환을 통해서 Tire 필드값을 교체함으로써 Car의 run() 메소드 수정 없이도 다양한 roll() 메소드의 실행 결과를 얻게 된다. 이것이 바로 필드의 다형성이다. 조금 길긴 하지만 위의 간단한 코드들을 만들어 보았다.
// 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;
if(accumulatedRotation < maxRotation) { // 정상회전(누적 < 최대)일 경우 실행
System.out.println(location + " Tire 수명 : " + (maxRotation-accumulatedRotation) + "회");
return true;
}
else { // 펑크(누적 = 최대)일 경우 실행
System.out.println("*** " + location + " Tire 펑크 ***");
return false;
}
}
}
// Car.java
public class Car {
// 필드 - 자동차는 4개의 타이어를 가진다.
Tire frontLeftTire = new Tire("앞왼쪽", 6);
Tire frontRightTire = new Tire("앞오른쪽", 2);
Tire backLeftTire = new Tire("뒤왼쪽", 3);
Tire backRightTire = new Tire("뒤오른쪽", 4);
// 생성자
// 메소드 - 모든 타이어를 1회 회전시키기 위해 각 객체의 roll() 메소드를 호출한다.
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(backLeftTire.roll() == false) {stop(); return 4;}
return 0;
}
void stop() { // 펑크 났을 때 실행
System.out.println("[자동차가 멈춥니다.]");
}
}
// HankookTire.java
public class HankookTire extends Tire{
// 필드
// 생성자
public HankookTire(String location, int maxRotation) {
super(location, maxRotation);
}
// 메소드
@Override // roll() 오버라이딩
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 // roll() 오버라이딩
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();
for(int i=1; i<=5; i++) {
int problemLocation = car.run();
switch(problemLocation) {
case 1: // 앞왼쪽 타이어가 펑크 났을 때 HankookTire로 교체
System.out.println("앞왼쪽 HankookTire로 교체");
car.frontLeftTire = new HankookTire("앞왼쪽", 15);
break;
case 2: // 앞오른쪽 타이어가 펑크 났을 때 KumhoTire로 교체
System.out.println("앞오른쪽 HankookTire로 교체");
car.frontRightTire = new KumhoTire("앞오른쪽", 13);
break;
case 3: // 뒤왼쪽 타이어가 펑크 났을 때 HankookTire로 교체
System.out.println("뒤왼쪽 HankookTire로 교체");
car.backLeftTire = new HankookTire("뒤왼쪽", 13);
break;
case 4: // 뒤오른쪽 타이어가 펑크 났을 때 KumhoTire로 교체
System.out.println("뒤오른쪽 HankookTire로 교체");
car.backRightTire = new KumhoTire("뒤오른쪽", 13);
break;
}
System.out.println("-------------------------------"); // 1회전 시 출력되는 내용을 구분
}
}
}
'Java 길찾기 > 이것이 자바다' 카테고리의 다른 글
[Java] 추상 클래스 (0) | 2022.01.25 |
---|---|
[Java] 타입 변환과 다형성 (2) (0) | 2022.01.24 |
[Java] 상속 (2) (0) | 2022.01.20 |
[Java] 상속 (1) (0) | 2022.01.19 |
[Java] 어노테이션 (0) | 2022.01.18 |