OOP 2.5 다형성

MMM·2023년 7월 21일

다형성

  • 여러 가지 형태를 가질 수 있는 능력

  • 자바는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 하여 프로그램적으로 구현
    = 조상 클래스 타입의 참조 변수로 자손 클래스의 인스턴스를 참조할 수 있도록 하였다는 것

  • 예시)

    • 현재 Tv와 CaptionTv의 형태 : Tv 클래스와 CaptionTv 클래스는 상속관계
        class Tv {
            boolean power;
            int channel;
    
            void power() {
                power = !power;
            }
            void channelUp() {
                ++channel;
            }
            void channelDown() {
                --channel;
            }
        }
    
        class CaptionTv extends Tv {
            String text;
            void caption() {
                // ...
            }
        }
    • 두 클래스가 상속 관계에 있을 경우, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있다.
        CaptionTv c = new CaptionTV();
        Tv t = new CaptionTv();
      • 이 경우, 실제 인스턴스가 CaptionTv 타입이라도 참조변수 t는 CaptionTV 인스턴스의 모든 멤버를 사용할 수 없다. CaptionTv 인스턴스 중 Tv 클래스의 멤버(상속받은 멤버 포함)만 사용 가능.
        -> 참조변수 tt.text 혹은 t.caption()을 사용할 수 없다.
      • 같은 타입의 인스턴스지만 참조 변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라지는 것
      • 조상의 조상으로도 형변환 가능 => 모든 참조변수는 모든 클래스의 조상인 Object 클래스 타입으로 형변환 가능하다.
    • 반면 자손타입의 참조 변수로 조상 타입의 인스턴스는 참조할 수 없다.
        CaptionTV c = new Tv();	//불가능!
      • 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많아지기 때문. -> 존재하지 않는 멤버를 사용하고자 하는 가능성이 있으므로 허용x
      • 참조 변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다.

참조변수의 형변환

  • 자손타입 -> 조상타입(Up-casting) : 형변환 생략 가능
    자손타입 <- 조상타입(Down-casting) : 형변환 생략 불가
  • 자손타입으로 다운캐스팅 : 참조변수가 다룰 수 있는 멤버의 개수를 늘이는 것 -> 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 더 많아짐 => 문제 발생 가능 => 형변환 생략 불가 + 형변환 수행 전 instanceof()로 참조변수가 참조하고 있는 실제 인스턴스 타입 확인하는 것이 안전
  • 형변환은 참조변수의 타입을 변환. 인스턴스를 변환하는 것이 아님 => 인스턴스에 영향 x. 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것 뿐임.

instanceof 연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용
  • 참조변수 instanceof 타입(클래스명)
  • 결과값: true / false
    • 결과값이 true라면 참조변수가 검사한 타입으로 형변환이 가능하다는 것
package main.java.polymorphism;

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());
    }

}
class Car {}
class FireEngine extends Car {}

//    This is a FireEngine instance.
//    This is a Car instance.
//    This is an Object instance.
//    main.java.polymorphism.FireEngine
  • 위 예시와 같이 실제 인스턴스와 같은 타입의 instanceof 연산 외에 조상 타입의 instanceof 연산에도 true를 결과로 얻음 -> 검사한 타입으로의 형변환을 해도 아무런 문제 x!
  • 참고) 참조변수.getClass().getName() : 참조변수가 가리키고 있는 인스턴스의 클래스 이름을 문자열(String)으로 반환

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

  • 조상 클래스에 선언된 멤버변수와 같은 이름의 인스턴스 변수를 자손 클래스에 중복으로 정의할 때,
    조상 타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버 변수가 사용,
    자손 타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버 변수가 사용.
    • (인스턴스)메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩해도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출
      • static메서드는 static변수처럼 참조변수의 타입에 영향을 받음! -> static 변수는 반드시 클래스이름.메서드()로 호출해야 함.
  • 중복으로 정의되지 않은 경우,
    조상타입의 참조변수를 사용했을 때와 자손타입의 참조변수를 사용했을 때 차이 x
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.x = 100
        p.method();		// Child Method
        
        System.out.println("c.x = " + c.x);	// c.x = 200
        c.method();		// Child Method
    }
}

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

class Child extends Parent {
    int x = 200;
    
    void method() {
        System.out.println("Child Method");
    }
}
  • 참조변수 p, c 모두 Child 인스턴스를 참조 + ParentChild는 서로 같은 멤버들을 정의하고 있음

    • 조상타입의 참조변수 pChild 인스턴스의 멤버들을 사용했을 때,
      메서드method()는 실제 인스턴스의 타입인 Child 클래스에 정의된 메서드가 호출
      인스턴스 변수x는 참조 변수 p의 타입에 따라 Parent 클래스에 정의된 변수가 호출

  • 웬만하면 멤버변수를 접근 제한시켜야 하는 이유

    public class BindingTest3 {
        public static void main(String[] args) {
            Parent p = new Child();
            Child c = new Child();
    
            System.out.println("p.x = " + p.x);		// (1)
            p.method();								// (2)
            System.out.println();
            System.out.println("c.x = " + c.x);		// (3)
            c.method();								// (2)
        }
    }
    
    class Parent {
        int x = 100;
    
        void method() {
            System.out.println("Parent Method");
        }
    }
    
    class Child extends Parent {
        int x= 200;
    
        void method() {
            System.out.println("x = " + x);			// (4)
            System.out.println("super.x = " + super.x);
            System.out.println("this.x = " + this.x);
        }
    }
    • 실행 결과

      p.x = 100		// (1)
      x = 200			// (2)
      super.x = 100
      this.x = 200
      
      c.x = 200		// (3)
      x = 200			// (2)
      super.x = 100
      this.x = 200
      • (1) - 참조변수 p가 조상 타입이므로 조상 클래스 Parent의 멤버변수 x가 호출됐다.
      • (2) - 참조변수의 타입과 관계없이 실제 인스턴스의 타입에 따라 자손 클래스 Childmethod()가 실행되었다.
      • (3) - 참조변수 c가 자손 타입이므로 자손 클래스 Child의 멤버변수 x가 호출됐다.
      • (4) - 여기서 xthis.x와 동일하다.
    • 주로 멤버변수들은 private으로 접근을 제한, 외부에서는 메서드를 통해서만 접근할 수 있도록 함. 위의 예제와 같이 인스턴스변수에 직접 접근하면 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있으니 주의.


매개변수의 다형성

  • 메서드의 매개변수에도 참조변수의 다형적인 특징이 적용된다.
class Product {
    int price;
    int bonusPoint;

    Product (int price) {
        this.price = price;
        bonusPoint = (int) (price / 10.0);
    }
}

class Tv extends Product {
    Tv() { super(100); }

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

class Computer extends Product {
    Computer() { super(200); }

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

class Buyer {
    int money = 1000;
    int bonusPoint = 0;
    
    void buy(Product p) {       // (1)
        if (money < p.price) {
            System.out.println("잔액 부족");
            return;
        }
        
        money -= p.price;
        bonusPoint += p.bonusPoint;
        System.out.println(p + "을/를 구입하셨습니다.");
    }
}


public class PolyArgumentTest1 {
    public static void main(String[] args) {
        Buyer b = new Buyer();
        
        b.buy(new Tv());
        b.buy(new Computer());
        
        System.out.println("현재 남은 돈은 " + b.money + "만원입니다.");
        System.out.println("현재 보너스점수는 " + b.money + "점입니다.");
    }
}
  • (1) - 메서드의 매개변수로 Product 타입의 참조변수 설정 -> Product 클래스의 자손타입이라면 어느 것이나 매개변수로 받아들일 수 있다.
    만일 메서드의 매개변수에 다형성이 적용되지 않았다면 제품별로 buy() 메서드가 필요했을 것.

여러 종류의 객체를 배열로 다루기

  • 조상 타입의 참조변수 배열 사용 -> 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있음.

    Product p[] = new Product[3];
    p[0] = new Tv();
    p[1] = new Computer();
    p[2] = new Audio();

    or 묶어서 다루고 싶은 객체들의 상속관계를 따져서 가장 가까운 공통조상 클래스 타입의 참조변수 배열을 생성, 객체들을 저장


  • Vector 클래스

    • 내부적으로 Object 타입의 배열 가지고 있음. 이 배열에 객체 추가/제거 가능
    • 배열의 크기를 알아서 관리 -> 저장할 인스턴스 개수에 신경쓰지 않아도 됨
    • 동적으로 크기가 관리되는 객체 배열
    • 주요 메서드
profile
과거의 내가 현재의 나보다 똑똑할 때가 있다.

1개의 댓글

comment-user-thumbnail
2023년 7월 21일

소중한 정보 감사드립니다!

답글 달기