다형성이란?

  • 객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미합니다.
  • 자바에서는 한 타입참조변수여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였습니다.
  • 자바에서는 조상클래스 타입의 참조변수자손클래스인스턴스참조할 수 있도록 하여 다형성구현했습니다. 
public class Tv {
    boolean power;      // 전원상태 (on/off)
    int channel;        // 채널

    void power(){ power = !power;}
    void channelUp(){ ++channel; }
    void channelDown(){ --channel; }

}

class CaptionTv extends Tv{
    String text;        // 캡션을 보여주기 위한 문자열
    void caption(){/* 내용 생략 */}    
    
}

 

  • TvCaptionTv는 서로 상속관계에 있기 때문에, 조상 클래스 타입의 참조변수( Tv 타입 )로 자손 클래스인스턴스( CaptionTv의 인스턴스 )를 참조하는 것이 가능합니다.
Tv t = new CaptionTv();			// 조상 타입의 참조 변수로 자손 인스턴스를 참조
CaptionTv c = new CaptionTv(); 	// 같은 타입의 참조 변수로 인스턴스 참조

// 아래와 같이 자손 타입의 참조 변수로 조상 인스턴스를 참조할 수 없음 ( 컴파일 에러 )
CaptionTv d = new Tv();
  • 위의 t와 같이 조상타입참조변수자손타입인스턴스를 참조하는 것이 가능합니다. 
  • 이 경우 같은 타입참조변수c와 달리 tTv클래스의 멤버들( 상속받은 멤버 포함 )만 사용할 수 있습니다.
    ( CaptionTv 인스턴스 멤버 중 Tv클래스에 정의되지 않은 text, caption()은 사용 불가합니다. )
  • 즉, tc참조하는 인스턴스는 둘 다 같은 타입인스턴스참조하지만, 참조변수타입에 따라 사용할 수 있는 맴버의 개수가 달라집니다. 
  • 반대로 자손 타입참조변수 조상타입 인스턴스참조하는 경우 존재하지 않는 멤버를 사용하고자 하는 가능성이 있기 때문에 이를 허용하지 않습니다. ( d.text, d.caption()과 같은 방식으로 참조하려고 할 수 있음 )
  • 정리하자면, 참조변수가 사용할 수 있는 맴버의 개수는 인스턴스의 맴버 개수보다 같거나 적어야 합니다.

참조변수의 형변환

  • 서로 상속관계에 있는 클래스 사이에서 참조변수형변환이 가능합니다.
  • 자손타입참조변수조상타입참조변수로, 조상타입참조변수자식타입참조변수형변환 할 수 있습니다.
자손타입  -> 조상타입 ( Up - casting ) : 형변환 생략가능
조상타입  -> 자손타입 ( Down - casting ) : 형변환 생략불가
public 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~~~");
    }
}
  • Car 클래스는 FireEngine 클래스와 Ambulance 클래스의 조상이며, FireEngineAmbulance는 서로 아무 관계가 없다. ( 자바상속에서 형제 관계라는 것은 없음, 형변환 불가
  • Car 타입의 참조변수FireEngine 타입, Ambulance 타입의 참조변수 간에는 형변환이 가능합니다.
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;

car = fe;		// car = (Car)fe;에서 형변환 생략됨. 업캐스팅
fe2 = (FireEngine)car;	// 형변환을 생략할 수 없음. 다운캐스팅
  • 업캐스팅의 경우, 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 형변환생략할 수 있습니다.
  • 다운캐스팅의 경우, 참조변수가 다룰 수 있는 멤버의 개수늘리는 것이므로, 실제 인스턴스멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 많아지므로 문제가 발생할 가능성이 있어서 형변환생략할 수 없는 것입니다.
    public static void main(String[] args) {
        Car car = null;
        FireEngine fe = new FireEngine();
        FireEngine fe2 = null;

        fe.water();
        car = fe;       // car = (Car)fe;
//        car.water();  컴파일 에러 발생, Car타입의 참조 변수로는 water()를 호출할 수 없다.
        fe2 = (FireEngine) car;
        fe2.water();
    }
    
    출력 결과
    water!!!
    water!!!
  • 1. Car타입의 참조변수 carnull로 초기화했으며 FireEngine타입의 인스턴스를 fe가 참조하도록 합니다.
  • 2. car = fe; 참조변수 fe참조하고 있는 인스턴스car참조하도록 합니다. fe의 값(fe참조하는 인스턴스 주소)이 car저장됩니다. carCar타입이므로 Car타입의 멤버가 아닌 water()는 사용할 수 없습니다.
  • 3. 참조변수 car참조하고 있는 인스턴스의 주소를 fe2저장합니다. 이 때, 조상클래스에서 자식클래스로의 형변환 ( 다운캐스팅 )이 일어나기 때문에 명시적으로 형변환 하였습니다. fe2FireEngine 타입이므로 FireEngine 인스턴스의 모든 멤버들을 사용할 수 있습니다. ( water() 사용 가능 ) 
public class CastingTest2 {
    public static void main(String[] args) {
        Car car = new Car();
        Car car2 = null;
        FireEngine fe = null;

        car.drive();
        fe = (FireEngine) car	// Car타입의 인스턴스를 자손의 참조변수가 참조할 수 없습니다.
        fe.drive();				
        car2 = fe;
        car2.drive();
    }
}
  • 위 코드는 컴파일성공하지만, 실행 시 에러( ClassCastException )가 발생합니다.
  • 조상 타입의 참조변수자손타입 참조변수형변환한 것이기 때문에 문제가 없어 보이지만, 참조변수 car이 참조하는 인스턴스의 타입이 Car이기 때문에 에러가 발생합니다. ( 조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용되지 않는다.
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.

Instanceof 연산자

  • instanceof참조변수참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 주로 사용하는 연산자입니다.
  • 연산의 결과로 boolean 값인 truefalse 중의 하나를 반환합니다. 
  • instanceof를 이용한 연산결과true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻합니다. 
public class InstanceofTest {
    public static void main(String[] args) {
        FireEngine fe = new FireEngine();

        if (fe instanceof FireEngine) {
            System.out.println("This is a FireEngine instance.");
        }

        if (fe instanceof Car) {
            System.out.println("This is a Car instance.");
        }

        if (fe instanceof Object) {
            System.out.println("This is an Object instance.");
        }

        System.out.println(fe.getClass().getName());    // 클래스의 이름을 출력
    }
}

실행 결과
This is a FireEngine instance.
This is a Car instance.
This is an Object instance.
FireEngine
  • FireEngine 클래스는 Object 클래스와 Car 클래스를 상속받았기 때문에, FireEngine 인스턴스는 Object 인스턴스와 Car 인스턴스를 포함하고 있는 셈입니다. 
  • 따라서 실제 인스턴스같은 타입의 instanceof연산 이외에 조상타입instanceof 연산에도 true를 결과로 얻으며, instanceof연산의 결과true라는 것은 검사한 타입으로 형변환을 해도 아무런 문제가 없다는 뜻입니다.

참조변수와 인스턴스의 연결

  • 조상 클래스메서드자손 클래스에서 오버라이딩한 경우 참조변수타입에 관계없이 항상 실제 인스턴스메서드가 호출됩니다. ( 오버라이딩 된 메서드 ) 
  • 맴버변수조상 클래스자손 클래스중복으로 정의된 경우, 조상타입참조변수를 사용했을 때는 조상 클래스에 선언된 맴버변수가 사용되고, 자손타입참조변수를 사용했을 때는 자손 클래스에 선언된 맴버변수가 사용됩니다.
public class BindingTest {

    public static void main(String[] args) {
        Parent p = new Child();
        Child c = new Child();

        System.out.println("p.x = " + p.x);
        p.method();

        System.out.println("c.x = " + c.x);
        c.method();
    }

}

class Parent {
    int x = 100;

    void method() {
        System.out.println("Parent's Method");
    }
}

class Child extends Parent {
    int x = 200;
    void method() {
        System.out.println("Child's Method");
    }
}

# 실행 결과
p.x = 100
Child's Method
c.x = 200
Child's Method
  • 타입은 다르지만, 참조변수 pc 모두 Child 인스턴스를 참조하고 있다. 
  • 결과를 보면, 참조변수타입에 관계없이 항상 실제 인스턴스타입Child 클래스에 정의된 메서드가 호출되며, 인스턴스변수 x참조변수타입에 따라서 달라진다.

매개변수의 다형성

  • 참조변수다형적인 특징은 메서드매개변수에도 적용할 수 있습니다.
public class Product {
    int price;          // 제품의 가격
    int bonusPoint;     // 제품구매 시 제공하는 보너스점수

    public Product(int price) {
        this.price = price;
        bonusPoint = (int) (price / 10.0);   // 보너스점수는 제품 가격의 10%
    }
}

class Audio extends Product {

    public Audio() {
        super(100);            // Audio의 가격을 100만원으로 지정
    }

    public String toString() {      // Object 클래스의 toString() 메서드 오버라이딩
        return "Audio";
    }
}

class Computer extends Product {
    Computer() {
        super(200); // 컴퓨터의 가격을 200으로 지정
    }

    public String toString() {
        return "Computer";
    }
}

class Buyer {               // 고객
    int money = 1000;       // 소유금액
    int bonusPoint = 0;     // 소유보너스점수

    void buy(Product product) {
        money -= product.price;             // 가진 돈에서 제품의 가격을 뺍니다.
        bonusPoint += product.bonusPoint;   // 가진 보너스점수에 제품구매 시 제공하는 보너스점수를 더해줍니다.
        System.out.println(product + "을/를 구매하셨습니다.");
    }
}

class PolyArgumentTest {
    public static void main(String[] args) {
        Buyer b = new Buyer();

        b.buy(new Audio());
        b.buy(new Computer());

        System.out.println("현재 남은 돈은" + b.money + "만원입니다.");
        System.out.println("현재 보너스점수는" + b.bonusPoint + "점입니다.");
    }
}

# 실행 결과
Audio을/를 구매하셨습니다.
Computer을/를 구매하셨습니다.
현재 남은 돈은700만원입니다.
현재 보너스점수는30점입니다.
  • Buyer 클래스의 buy 메서드를 보시면 각각의 타입이 아닌 Product p 만을 메서드매개변수로 받고 있습니다.
  • 매개변수Product 타입의 참조변수라는 것은, 메서드 매개변수 Product 클래스의 자손타입 참조변수면 어느 것이나 매개변수로 받아들일 수 있기 때문입니다. 

+ Recent posts