다시 하자 기초! 다형성
다형성이란
여러 가지 형태를 가질 수 있는 능력을 의미하는 다형성은 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.
이를 좀 더 구체적으로 말하자면, 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.
지금까지 우리는 인스턴스를 생성하고 다루기 위해서 해당하는 타입의 참조변수만 사용했다. 예를들어 CaptionTv인스턴스를 사용하기 위해 CaptionTv타입의 참조변수를 사용한다. 이처럼 일치 시키는게 일반적이지만 Tv와 CaptionTv클래스가 서로 상속관계에 있을 경우, Tv t = new CaptionTv(); 처럼 사용하는 것도 가능하다.
그러면 이제 인스턴스를 같은 타입의 참조변수로 참조하는 것과 조상타입의 참조변수로 참조하는 것은 어떤 차이가 있는지에 대해 알아보도록 하자.
CaptionTv c = new CaptionTv();
Tv t = new CapntionTv();
위 코드에서 실제 인스턴스가 CaptionTv타입이라 할지라도, 참조변수 t로는 CaptionTv인스턴스의 모든 멤버를 사용할 수 없다. Tv타입의 참조변수로는 CaptionTv인스턴스 중에서 Tv클래스의 멤버들(상속받은 멤버포함)만 사용할 수 있다. 따라서 생성된 CaptionTv인스턴스 멤버중에서 Tv클래스에 정의 되지않은 멤버, text와 caption()은 참조변수 t로는 사용이 불가능 하다. 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
그렇다면 반대로 CaptionTv c = new Tv(); 는 가능할까?
그렇지 않다. 그 이유는 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기 때문!
CaptionTv클래스에는 text와 caption()이 정의되어 있으므로 참조변수 c로는 c.text, c.caption()과 같은 방식으로 c가 참조하고 있는 인스턴스에서 text와 caption()을 사용하려 할 수 있다. 하지만 c가 참조하는 인스턴스는 Tv타입이고, Tv타입의 인스턴스에는 text와 caption()이 존재하지 않기 때문에 에러가 발생 하는 것이다.
그래서 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.
참조변수의 형변환
참조변수도 형변환이 가능하다. 대신 다음과 같은 조건이 붙는다.
1. 상속관계에 있는 클래스 끼리만 가능하다.
2. 자손타입 → 조상타입(Up-casting) : 형변환 생략가능
3. 자손타입 ← 조상타입(Down-casting) : 형변환 생략 불가
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
car = fe; //car = (Car)fe; 에서 형변환이 생략됨. 업캐스팅
fe2 = (FireEngine)car; //형변환을 생략 불가.
형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것이 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.
class CastingTest1{
public static void main(String args[]){
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
fe.water();
car = fe;
// car.water(); -> 컴파일에러!! Car타입의 참조변수로는 water()를 호출 불가
fe2 = (FireEngine)car; //자손타입 <- 조상타입
fe2.water();
}
}
class Car{
String color;
int door;
void driver(){
System.out.println("driver, Brrr~");
}
}
class FireEngine extends Car{
void water(){
System.out.println("water~~~");
}
}
하지만 주의해야 할 점이 있는데 이전에 말한 것 처럼 자손참조형으로 부모인스턴스를 생성 못하기 때문에 아래와 같은 코드에서는 에러가 뜬다.
Car car = new Car(); //new Car()말고 new FireEngine으로 생성 했더라면 문제 없었을 코드
Car car2 = null;
FireEngine fe = null;
car.driver();
fe = (FireEngine)car; //컴파일은 OK. 실행 시 에러
fe.driver();
car2 = fe;
car2.driver();
참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인 하는 것이 중요하다.
instanceof 연산자
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다. 주로 조건문에 사용되며 왼쪽에는 참조변수를 오른쪽에는 확인할 타입을 적는다. 연산 결과는 false, true로 반환하고 true로 검사된 타입으로는 형변환을 해도 아무런 문제가 없다는 뜻이다.
Car c = new Car();
if(c instanceof FireEngine)
System.out.print("형변환 가능");
참조변수와 인스턴스의 연결
조상 클래스의 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 변수의 호출은 참조변수 타입에 있는 값이 호출된다. 오버라이딩된 메서드는 항상 실제 인스턴스의 타입에 따라 호출됨.
매개변수의 다형성
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 = 0;
void buy(Tv t) {
money = money - t.price;
bonusPoint = bonusPoint + t.bonusPoint;
}
void buy(Audio t) {
money = money - t.price;
bonusPoint = bonusPoint + t.bonusPoint;
}
}
이런식으로 구매자가 Tv나 Audio의 대한 각각에 대한 메소드를 만들어줘야 하지만 다형성으로 이를 한방에 해결이 가능하다. 바로 공통적으로 상속받고 있는 Product를 이용하자.
void buy(Product p){
money = money - p.price;
bonusPoint = bonusPoint + p.bonusPoint;
}
이렇게 하면 위처럼 각각의 상품마다 하지 않아도 되고 상품이 추가 될 경우 새로 메소드를 생성 할 필요도 없기 때문에 엄청난 장점이 될 수 있다!
여러 종류의 객체를 배열로 다루기
조상 타입의 참조변수로 자손타입의 객체를 참조하는 것이 가능하므로, Product클래스가 Tv, Computer, Audio클래스의 조상일 때, 다음과 같이 할 수 있는 것을 이미 배웠다. 이처럼 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다.
class Buyer {
int money = 1000;
int bonusPoint = 0;
Product[] item = new Product[10];
//Vector item = new Vector(); //배열의 크기를 알아서 늘려주는 컬렉션
int i=0;
void buy(Product p){
if(money < p.price){
System.out.print("잔액이 부족해 살 수 없습니다.");
return;
}
money -= p.price;
bonusPoint += p.bonusPoint;
item[i++] = p;
System.out.print(p +"을 구입하셨습니다");
}
}
아까 정의 했던 Buyer를 일부 수정하였는데 Product로 배열을 만들었기 때문에 Audio나 Computer들이 다 들어올 수 있다. 상품들을 하나의 배열로 관리가 용이 해지는 것 이다.