Language/Java

[Java] 다형성(polymorphism)

Hoplin 2022. 1. 4. 22:44
반응형

다형성

 

다형성은 객체지향의 특징중 하나이다(객체지향의 특징 : 캡슐화, 다형성, 상속성, 동적바인딩, 추상화). 다형성이란, 여러가지 형태를 가질 수 있는 능력을 의미한다. 자바에서 다형성이란, 한 타입의 참조변수로 여러 타입 객체를 참조하는것을 의미한다. 좀 더 자세한 의미로는 조상클래스 타입의 참조변수로 자손타입 인스턴스를 참조할 수 있다. 아래코드를 예시로 들어보자

 

class tv{
    private boolean power;
    private int channel;
    tv(int channel){
        this.channel = channel;
    }
    public boolean getPower(){
        return this.power;
    }
    public int getChannel(){
        return this.channel;
    }
    public void setPower(){
        this.power = !this.power;
    }
}

class CaptionTv extends tv{
    CaptionTv(){
        super(10);
    }
    CaptionTv(int channel){
        super(channel);
    }
    private String text;
    public String getText(){
        return this.text;
    }
    void showCaption(){
        System.out.println(this.getText());
    }
}

tv와 CaptionTv클래스가 서로 상속관계에 있을 경우, 다음과 같이 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하는것이 가능하다.

tv t = new CaptionTv();
CaptionTv a = new CaptionTv();

위에서 t참조변수는 CaptionTv의 모든 멤버에 접근할 수 없다. tv에 존재하는 CaptionTv의 멤버만 접근할 수 있다. 그렇기에 t는 text멤버변수, getText(), showCaption()에 접근이 불가능하다. 반면 CaptionTv형 참조변수 a는 CaptionTv의 모든 멤버에 접근가능하다. 즉, 같은 타입 인스턴스여도, 참조변수 타입에 따라 사용할 수 있는 멤버개수가 달라지는 것이다. 이러한 경우는 어떻게 될까?

CaptionTv c = new tv();

위 같은 경우에는 에러가 난다, 이유는 참조변수 c가 사용할 수 있는 멤버 개수가 tv()인스턴스보다 많기 때문이다. 결론적으로 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스 멤버개수보다 같거나 적어야한다. 또한 조상타입 참조변수로 자손타입의 인스턴스를 참조할 수 있지만, 반대로 자손타입 참조변수로 조상타입의 인스턴스를 참조할 수 없는것이다(상속받은 CaptionTv멤버 수가 tv멤버수보다 많기 때문에 성립하지 않음). 

 

만약에 자손클래스에 조상클래스의 메소드가 오버라이딩 되어있다면, 참조변수는 조상클래스의 메소드가 아닌, 자손클래스의 오버라이딩 된 메소드를 읽어온다. 반면 멤버변수 같은 경우에는 조상클래스의 멤버변수를 읽어온다.

/**
 *
 * 조상타입의 참조변수와 자손타입 참조변수 차이점이 사용할 수 있는 멤버의 개수인것을 알았다.
 *조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의하고, 조상타입으로 자손 인스턴스를 참조하게 되면 어떻게 될까?
 *
 * 메소드 같은 경우에는 오버라이딩 되어 중복된 경우에는 무조건 자손타입의 메소드를 사용하게 된다.
 * 멤버변수가 조상클래스, 자손클래스 중복 정의된 경우 조상타입의 참조변수를 사용했을때 조상 클래스에 선언된 멤버변수가사용되고, 자손타입 참조변수 사용시, 자손클래스 멤버변수가 사용된다.
 */

class Parent{
    int x = 200;
    void method(){
        System.out.println("Parent Method");
    }
    void method2(){}
}

class Child extends Parent{
    int x = 300;
    @Override
    void method(){
        System.out.println("Child Method");
    }
    @Override
    void method2(){
        System.out.println("x = " + x);
        System.out.println("super.x = " + super.x);
        System.out.println("this.x = " + this.x);
    }
}

public class instanceof2 {
    // 멤버변수는 참조변수의 자료형에 따라서 값이 달라진다.
    public static void main(String[] args){
        Parent p = new Child();
        Child c = new Child();
        System.out.println(p.x); // Parent Class 의 멤버변수
        System.out.println(c.x);// Child Class의 멤버변수
        p.method();// child method
        c.method();//child method
        p.method2();
        c.method2();
    }
}

 

참조변수 형변환

참조변수 또한 형변환이 가능하다. 단, 이는 서로 상속관계에 있는 경우에만 가능하다. 가능한 형변환은 아래와같다.

  • Up-Casting : 자손타입참조변수 -> 조상타입참조변수 : 형변환 생략가능
  • Down- Casting : 조상타입참조변수 -> 자손타입참조변수 : 형변환 생략 불가능

이 두개에서 주목해야할 것은 그냥 '타입'이 아닌 '타입의 참조변수'라는 것이다. 이해하는데에 있어서 잘못볼 경우 혼란을 줄 수 있다는것을 조심하자. 또 다른 주의할 점은, 서로 상속관계에 있는 클래스간의 형변환은 성립하지만, 일반적인 클래스간의 관계에서는 참조변수 형변환이 불가능하다는것을 기억하자.

 

업캐스팅의 형변환 생략 가능의 원리는, 기본 자료형에서 작은 자료형에서 큰 자료형은 생략이 가능한것과 동일하게 생각하면 된다.(int -> float/double 생략가능, float/double -> int 생략불가능). 이 형변환을 통해, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 조절하는것이다.

 

업캐스팅은 아래와 같은 형태이다.

tv t = new CaptionTv();

다운캐스팅은 아래와 같은 형태이다.

CaptionTV a = new tv();

하지만 일반적으로 다운 캐스팅은 컴파일은 되지만 대부분의 경우에서 오류가 난다. 이유는 위에서 보았듯이, 자식클래스 참조변수로 조상클래스 인스턴스를 참조하면, 참조변수가 사용할 수 있는 멤버가 더 많기때문에, 성립되지 않는다. 그렇다면 다운 캐스팅은 어느경우에 성립될까? 아래와 같은 경우에는 다운캐스팅이 성립된다.

tv e = new CaptionTv();
CaptionTv a = (CaptionTv)e;

 

다운캐스팅은 쉽게 생각해서, 객체의 잃어버린 성격을 다시 되찾아주는것이라고 생각하면 된다. 위 예시같은 경우에는 CationTv()인스턴스지만, tv타입의 참조변수에 의해 원 성격을 잃어버리게 된다. 이걸 CaptionTv참조변수로 재참조를 함으로서 잃어버린 CaptionTv성질을 모두 복구해 주는 것이다.

 

참조변수 형변환을 활용한 간단한 예시 코드를 작성해 보았다.

package Interface2;

class Car{
    private String color;
    private final int door = 2;
    Car(String color){
        this.color = color;
    }

    void Drive(){
        System.out.println("Driving now");
    }
    void Stop(){
        System.out.println("Stop Driving");
    }
}

class FireEngine extends Car{
    FireEngine(){
        super("Black");
    }
    @Override
    public String toString(){
        return "Fire Engine";
    }

    @Override
    void Drive(){
        System.out.println("Driving with " + this.toString());
    }

    @Override
    void Stop(){
        System.out.println("Stop driving " + this.toString());
    }

    void SpreadWater(){
        System.out.println("Spreading Water");
    }
}

class Ambulance extends Car{
    Ambulance(){
        super("White");
    }
    @Override
    public String toString(){
        return this.getClass().getName();
    }

    @Override
    void Drive(){
        System.out.println("Drive with " + this.toString());
    }
    @Override
    void Stop(){
        System.out.println("Stop driving " + this.toString());
    }
    void Siren(){
        System.out.println("Start Siren");
    }
}

public class polymorphismex {
    public static void main(String[] args){
        Car car = null;
        Car car2 = new Car("White");
        FireEngine fe1 = new FireEngine();
        FireEngine fe2 = null;
        FireEngine fe3 = null;

        fe1.SpreadWater(); // 오버라이딩된 메소드 호출
        car = fe1; // 조상클래스 참조변수로 자식클래스 인스턴스 참조, 형변환 명시 생략가능
        //car.SpreadWater(); // 조상클래스 참조변수로 인스턴스 참조를 해주었으므로, 조상클래스의 멤버에 해당하는 메소드들만 사용이 가능하게 된다.
        fe2 = (FireEngine)car; // 현재 조상클래스 참조변수 car은 자식클래스 인스턴스 fe1을 참조하고있다. 그렇기 때문에 결론적으로 이 구문은 FireEngine fe2 = fe1과 같은 형태이므로,
        //잃어버린 fe1의 성질을 되찾아주는 형태의 다운캐스팅이다. 그렇기때문에, 이는 성립하는 다운캐스팅이다.
        //fe3 = (FireEngine) car2; // Class Cast Exception : 조상클래스 인스턴스

        Ambulance a = new Ambulance();
        a.Siren();
        a.Drive();
        a.Stop();
    }
}
반응형