
참고
자바의 정석
객체지향 개념중에서 중요한 개념중에 하나가 다형성이다.
다형성이란? '여러가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현할 수 있다.
즉, 요약하자면 상위클래스 타입의 참조변수로 하위클래스의 인스턴스를 참조 할 수 있다.
class Tv {
boolean power;
int channel;
void power() {
power = !power;
}
void channelUp() {
++channel;
}
void channelDown() {
--channel;
}
}
class CaptionTv extends Tv {
String text;
void caption() {
}
}
위의 코드를 보면 Tv라는 상위클래스가 정의되어 있으며, Tv클래스를 상속받는 CaptionTv 클래스가 정의되어 있다.
두 클래스의 인스턴스 생성은 지금까지 이렇게 해왔다.
Tv t = new Tv();
CaptionTv ct = new CaptionTv();
우리는 지금까지 생성된 인스턴스를 다루기 위해서 인스턴스 타입과 일치하는 타입의 참조변수만을 사용해왔다. 위의 코드처럼 Tv의 인스턴스를 생성하고 싶으면 Tv타입으로 참조변수를 선언하였고 CaptionTv라는 인스턴스를 생성하고 싶으면 CaptionTv타입의 참조변수를 선언하였다. 이렇게 참조변수타입과 인스턴스 타입이 일치하는게 가장 일반적이지만 실무에서는 아래와 같이 서로 상속관계가 있으면 인스턴스타입의 상위 타입으로 참조변수의 타입을 지정할 수 있다.
Tv t = new CaptionTv();
그러면 참조변수와 인스턴스 타입이 같은 경우와 상위타입으로 지정하는 것이 무슨 차이가 있을까?
CaptionTv ct = new CaptionTv();
Tv t = new CaptionTv();
둘다 같은 CaptionTv 인스턴스일지라도 참조변수의 타입에 따라 사용가능한 멤버가 다르다. 같은 인스턴스타입이라도 t는 Tv타입의 참조변수이므로 CaptionTv에서 정의한 멤버들은 사용이 불가능하다. 즉, 같은 타입의 인스턴스지만, 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
그러면 아래와 같이 반대로 선언하면 어떨까?
CaptionTv ct = new Tv();
바로 컴파일 단계에서 에러가 난다. 왜냐하면 실제 인스턴스인 Tv의 멤버의 개수보다 참조변수가 사용할 수 있는 멤버가 많기 때문에 논리적으로 맞지 않기 때문이다.
하위타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 존재하지 않는 멤버를 사용하려고자 할 가능성이 있기에 허용하지 않는것이다. 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.
클래스는 상속을 통해 확장될수는 있어도 축소될 수는 없다. 상위 인스턴스의 멤버 개수는 하위 인스턴스의 멤버 개수보다 항상 적거나 같다.
그러면 여기서 의문점이 들것이다. 참조변수의 타입과 인스턴스 타입이 같으면 모든 멤버들을 사용할 수 있는데 왜 실무에선 참조변수의 타입을 상위타입으로 해서 사용하는 걸까? 점차 학습을 하다보면 알게 될 문제니 지금은 상위타입의 참조변수로 하위 인스턴스를 참조할 수 있다라고만 생각하자.
상위타입의 참조변수로 하위타입의 인스턴스를 참조할 수 있다.
반대로는 참조할 수 없고 에러가 발생한다.
기본형 타입의 변수와 같이 참조형 변수도 형변환이 가능하다. 단, 서로 상속관계여야하만 형변환이 가능하기 때문에 하위타입을 상위타입으로 혹은 상위타입을 하위타입으로 형변환이 가능하다.
모든 클래스는 Object클래스 타입으로 형변환이 가능하다.
기본형 변수의 형변환에서 작은 자료형에서 큰 자료형으로 형변환 시, 형변환을 생략을 할 수 있듯이 하위타입의 참조변수를 상위타입으로 형변환 할 때 생략이 가능하다.
하위타입 -> 상위타입 : 형변환 생략
상위타입 -> 하위타입 : 형변환 생략 불가
상위타입에서 하위타입으로 형변환시, 기본형처럼 형변환하면 가능하다.
class Car {
String color;
int door;
void drive() {
System.out.println("drive, Brrrr~");
}
void stop() {
System.out.println("stop!!");
}
}
class FireEngine extends Car {
void water() {
System.out.println("water!!!");
}
}
class Ambulance extends Car {
void siren() {
System.out.println("siren!")
}
}
위의 클래스들이 정의되어 있다고 하자. 그리고 아래의 코드를 실행하면 에러가 발생한다. 그 이유는 FireEngine 클래스와 Ambulance 클래스는 같은 부모클래스를 상속받고 있다는 점만 있지 형제관계같은 그런 관계는 없기 때문이 일종의 남이다.
FireEngine f = new FireEngine();
Ambulance a = new Ambulance();
a = (Ambulance) f; // ERROR: 상속관계가 아니므로 형변환 불가
f = (FireEngine) a; // ERROR: 상속관계가 아니므로 형변환 불가
그럼 정상적인 형변환의 예를 보자.
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
car = fe; // 업캐스팅 (형변환 생략)
fe2 = (FireEngine) car; // 다운캐스팅 (형변환 생략 불가)
그러면 업캐스팅때 형변환 생략이 왜 가능한 것일까? 예를 들어보면 다음과 같다.
Car타입의 참조변수 c를 Car타입의 조상인 Object타입의 참조변수로 형변환 하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명함으로 문제가 되지 않는다. 그래서 형변환 생략이 가능한 것이다. 반대로 그 역은 멤버의 개수가 늘어나므로 생략을 하면 문제가 발생한다. 그래서 하위타입으로 형변환은 생략이 안되며 형변환을 수행하기 전에 instanceof 연산자를 사용해 참조하고 있는 실제 인스턴스를 확인하고 캐스팅하는게 좋다.
형변환은 참조변수의 타입을 변환하는것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 끼치지 않는다.
단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위를 조절하는 것이다.
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행할 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다.
그래서 참조변수가 가르키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.
참조변수가 참조하고 있는 실제 인스턴스 타입을 알아보기 위해 instanceof 연산자를 이용한다. 주로, 조건문에서 이용을 하며 왼쪽에 참조변수 오른쪽에 타입이 피연산자로 위치하며 반환값은 boolean형이다.
값이 null인 참조변수에 대해 instanceof 연산자를 이용하면 false를 결과로 얻는다.
void doWork(Car c) {
if (c instanceof FireEngine) {
FireEngine fe = (FireEngine) c;
fe.water();
} else if (c instanceof Ambulance) {
Ambulance a = (Ambulance) c;
a.siren();
}
}
어떤 타입에 대한 instanceof 연산의 결과가 true라는 것은 검사한 타입으로 형변환이 가능하다는 것을 보여준다.
상위타입의 선언된 멤버변수와 같은 이름으로 하위타입에 중복으로 적용할 경우 상위타입의 참조변수로 하위 인스턴스로 참조하는 경우와 그 역은 서로 다른 결과를 반환한다.
메서드의 경우 상위타입의 메서드를 하위타입에서 오버라이딩하면 타입에 상관없이 오버라이딩된 메서드가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
static 메서드는 static 변수처럼 참조변수의 타입에 영향을 받는다. 유일하게 참조변수의 타입의 영향을 받지 않는 것은 인스턴스 메서드이다.
결론적으로, 멤버변수가 상위클래스와 하위클래스의 중복으로 정의가 된 경우, 상위타입의 멤버변수를 사용할 경우 상위클래스의 멤버변수가 사용되고, 하위타입의 멤버변수를 사용할 경우 하위클래스의 멤버변수가 사용된다.
만약 상위클래스의 멤버변수를 하위클래스에 중복정의 하지 않았다면 결과는 상위클래스의 멤버변수가 불릴 것이다.
다형성의 특징은 매개변수에서도 활용할 수 있다.
class Product {
int price;
int bonusPoint;
}
class Tv extends Product {
}
class Computer extends Product {
}
class Audio extends Product {
}
class Buyer {
int money = 1000;
int bonusPoint = 10;
}
위의 클래스들이 정의되어 있다고 가정하자. Product클래스는 상위클래스이고 그것을 상속받는 Tv, Computer, Audio 클래스가 존재한다. 여기서, Buyer클래스에 구매하는 메서드를 추가하면 아래와 같다.
void buy(구매할 제품) {}
이럴 경우 Tv를 살려면 오버라이딩으로 매개변수에 Tv 인스턴스를 넘겨줘야하고 Computer를 살려면 Computer 인스턴스를 넘겨줘야 할 것이다. 물론, 오버로딩으로 구현은 할 수 있지만 제품의 종류가 10만개라고 하면 10만개의 메서드를 구현하는 것은 너무 비효율적이다. 그래서 다형성으로 아래의 코드처럼 작성할 수 있다.
void buy(Product p) {}
매개변수가 Product타입의 참조변수라는 것은 메서드의 매개변수로 Product클래스거나 하위타입이면 매개변수로 들어갈 수 있다는 의미고 10만개의 메서드를 1개로 줄였다.
상위타입의 참조변수로 하위타입의 객체를 참조하는 것이 가능하다. 이럴때 아래와 같이 표현할 수 있다는 사실을 알것이다.
Product p1 = new Computer();
Product p2 = new Tv();
product p3 = new Audio();
하지만 제품이 10만개라면 위의 코드처럼 표현하는 것은 비효율적이다. 전에 배웠던 객체 배열로 처리하면 깔끔할 것이다.
Product[] p = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
하지만 여기서도 불현한 사항이 있다. 위의 코드를 보면 제품이 3개가 있다고 가정하고 작성한 코드이다. 하지만 제품이 늘어나면 코드를 수정해야하는 불편함이 있다. 그렇다고 배열을 미리 크게 설정하는 것은 메모리상 안 좋을 것이다. 이런 경우 java에서 지원해주는 Vector 클랙스를 이용하면 된다. Vector클래스는 내부적으로 Object타입의 배열을 가지고 있어 객체를 추가하거나 제거할 수 있다. 간단히 말하면 Vector클래스는 단지 동적으로 크기가 관리되는 클래스이다. 아래의 표를 보면서 Vector클래스에 있는 메서드를 알아보자.
| 메서드 / 생성자 | 설명 |
|---|---|
| Vector() | 10개의 객체를 저장할 수 있는 Vector 인스턴스를 생성한다. 10개이상의 인스턴스가 저장되면 크기가 자동으로 증가한다. |
| boolean add(Object o) | Vector에 객체를 추가한다. 추가에 성공하면 결과 값을 true를 반환 실패하면 false를 반환한다. |
| boolean remove(Object o) | Vector에 객체를 제거한다. 제거에 성공하면 결과 값을 true를 반환 실패하면 false를 반환한다. |
| boolean isEmpty() | Vector에 객체가 비었는지 확인한다. 비어있으면 결과 값을 true를 반환 비어있지 않으면 false를 반환한다. |
| Object get(int index) | 지정된 위치(index)의 객체를 반환한다. 반환타입이 Object타입이므로 적절한 타입으로 형변환이 필요하다. |
| int size() | Vector에 저장된 객체의 수를 반환한다. |