Programming Language/Java

[Java] 08. OOP - 다형성(polymorphism)

lumana 2024. 6. 24. 22:44

다형성(polymorphism)

  • 여러가지 형태를 가질 수 있는 능력
  • 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 구현
  • 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다
    • 공통으로 포함된 인스턴스만 접근할 수 있다
  • 같은 타입의 인스턴스라도 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다
  • 반대로 자손 타입의 참조변수로 조상 타입을 참조할 수 없음(컴파일 에러)
  • 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거나 적어야 한다

참조변수의 형변환

  • 기본형 변수와 같이 참조변수도 형변환이 가능함
  • 단, 서로 상속 관게에 있는 클래스 사이에서만 가능함 (부모 <-> 자식)
  • Up-casting : 자손타입을 부모타입에 // 형 변환 생략 가능(별 문제가 없기 때문에)
  • Down-casting : 부모타입을 자손타입에 // 형 변환 생략 불가(인스턴스 개수가 자손이 더 많기 때문)
    • 형변환을 수행하기 전에 instanceof 연산자를 사용하여 참조변수가 참조하고 있는 실제 인스턴스 타입을 확인하는 것이 좋음
  • 형변환은 참조변수의 타입을 변환하는 것이지, 인스턴스를 변환하는 것은 아니 때문에 인스턴스에 아무런 영향을 미치지 않음
  • Tv t = new CaptionTv(); 도 Tv t = (Tv)new CaptionTv();의 생략된 형태임
class CastingTest1 {
    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();
    }
}

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~!");
    }
}
  • 서로 상속관계에 있는 클래스 타입의 참조변수간의 형변환은 양방향으로 자유롭게 수행될 수 있음
  • But 참조변수가 참조하고 있는 인스턴스의 자손타입으로 형변환을 하는 것은 허용되지 않는다
    • 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요함
class CastingTest2 {
    public static void main(String args[]) {
        Car car            = new Car();
        Car car2        = null;
        FireEngine fe     = null;

        car.drive();
        fe = (FireEngine)car;        // 컴파일은 OK. 실행 시 에러 발생
        fe.drive();
        car2 = fe;
        car2.drive();
    }
}

instanceof 연산자

  • 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof연산자를 사용한다.
    • 주로 조건문에 사용된다
    • instanceof의 왼쪽에는 참조변수를, 오른쪽에는 타입(클래스명)이 피연산자로 위치한다
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 연산자를 이용해서 참조 변수가 가리키는 인스턴스 타입을 체크하고, 적절히 형변환하여 사용해야 한다
  • 조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있기 때문에, 참조변수의 타입과 인스턴스의 타입이 항상 일치하지는 않는다는 것을 배웠다
    • 조상타입의 참조변수로는 실제 인스턴스의 멤버들을 모두 사용할 수 없기 때문에, 실제 인스턴스와 같은 타입의 참조 변수로 형변환을 해야지만 인스턴스의 모든 멤버들을 사용할 수 있다. (다운 캐스팅을 해야 한다는 말)
  • ex) instanceof 연산자를 사용하여 어떤 자식 클래스인지 식별하고 다운 캐스팅하는 구조로 사용해봤던 기억이 있네요

Example 7.17

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());
        System.out.println(fe.getClass().getName());    // 클래스의 이름을 출력
    }
}    // class

class Car {}
class FireEngine extends Car {}
  • FireEngine 클래스는 Object 클래스와 Car 클래스를 포함하고 있는 셈이기 때문에, instanceof 연산의 결과가 True가 되는 것이다

instanceof연산의 결과가 true라는 것은 검사한 타입으로의 형변환을 해도 아무런 문제가 없다는 뜻이다.

참고) 참조변수.getClass().getName()은 참조변수가 가리키고 있는 인스턴스의 클래스 이름을 문자열(String)으로 반환한다

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

  • 조상 클래스에 선언된 멤버변수가 같은 이름의 인스턴스변수를 자손 클래스에 중복으로 정의했을 때, 조상타입의 참조변수로 자손 인스턴스를 참조하는 경우와 자손 타입의 참조 변수로 자손 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다
    • cf) 메서드의 경우 조상 클래스의 메서드를 자손의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진다.
    • 조금 더 간단히 구별하자면 메서드우변의 인스턴스 타입에 따라 달라지고, 멤버변수좌변의 참조변수 타입에 따라 달라진다

Example 7.18

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 Method");
    }
}

class Child extends Parent {
    int x = 200;

    void method() {
        System.out.println("Child Method");
    }
}

실행결과
p.x = 100
Child Method
c.x = 200
Child Method

Example 7.19

class BindingTest2 {
    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 Method");
    }
}

class Child extends Parent { }

실행결과
p.x = 100
Parent Method
c.x = 100
Child Method

참조변수의 타입에 따라 결과가 달라지는 경우는 조상 클래스의 멤버변수와 같은 이름의 멤버변수를 자손 클래스에 중복해서 정의한 경우뿐이다.

Example 7.20

class BindingTest3{
    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();
        System.out.println("c.x = " + c.x);
        c.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("x=" + x);  // this.x와 같다
        System.out.println("super.x=" + super.x);
        System.out.println("this.x="  + this.x);
    }
}

인스턴스변수에 직접 접근하면, 참조변수의 타입에 따라 사용되는 인스턴스변수가 달라질 수 있으므로 주의하자

  • 이를 위해 멤버변수들을 주로 private로 접근을 제한하는 것이 좋다

매개변수의 다형성

Product, Tv, Computer, Audio, Buyer 클래스가 정의되어 있다고 가정하자.

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;
}
  • 구매 함수를 아래와 같이 작성한다면 제품의 종류가 늘어날 때 마다 Buyer클래스에 새로운 buy 메서드를 추가해야 한다
void buy(Tv t) {
        money = money - t.price;
        bonusPoint += t.bonusPoint;
}

void buy(Computer c) {
        money -= c.price;
        bonusPoint += c.bonusPoint;

// Audio 구매 함수는 생략
}
  • 메서드의 매개변수에 다형성을 적용하면 다음과 같이 간단히 처리할 수 있다
void buy(Product p) {
        money = money - p.price;
        bonusPoint = bonusPoint + p.bonusPoint;
}
  • 매개변수가 Product타입의 참조변수라는 것은, 메서드의 매개변수로 Product 클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다는 뜻이다.
  • 앞으로 다른 제품 클래스를 추가할 때 Product 클래스를 상속받기만하면 buy(Product p) 메서드의 매개변수로 받아들여질 수 있다.

Example 7.21

class Product {
    int price;            // 제품의 가격
    int bonusPoint;        // 제품 구매 시 제공하는 보너스 점수

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

class Tv extends Product {
    Tv() {
        // 조상클래스의 생성자 Product(int price)를 호출
        super(100);        // Tv의 가격 100만원
    }

    // Object 클래스의 toString()을 오버라이딩
    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) {
        if(money < p.price) {
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }

        money -= p.price;                // 가진 돈에서 구입한 제품의 가격 차감
        bonusPoint += p.bonusPoint;        // 가진 보너스 점수에 제품의 보너스 점수 추가
        System.out.println(p + "을/를 구입하셨습니다.");
    }
}

class PolyArgumentTest {
    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.bonusPoint + "점 입니다.");
    }
}

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

Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();
  • 위 코드를 아래와 같이 배열로 처리할 수 있다
Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
  • 조상타입의 참조변수 배열을 사용하면, 공통의 조상을 가진 서로 다른 종류의 객체를 배열로 묶어서 다룰 수 있다
  • 배열의 크기 제한이 거슬린다면, Vector클래스를 이용하면 된다.
    • Vector클래스는 내부적으로 Object타입의 배열을 가지고 있다.

Example 7.23

import java.util.*;        // Vector 클래스를 사용하기 위해서 추가

class Product {
    int price;            // 제품의 가격
    int bonusPoint;        // 제품 구매 시 제공하는 보너스 점수

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

    Product() {
        price = 0;
        bonusPoint = 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 Audio extends Product {
    Audio() { super(50); }
    public String toString() { return "Audio"; }
}

class Buyer {                    // 고객, 물건을 사는 사람
    int money = 1000;            // 소유 금액
    int bonusPoint = 0;            // 보너스 점수
    Vector item = new Vector();    // 구입한 제품을 저장하는 데 사용될 Vector 객체

    void buy(Product p) {        // 제품 구입
        if(money < p.price) {
            System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
            return;
        }
        money -= p.price;                            // 가진 돈에서 구입한 제품의 가격 차감
        bonusPoint += p.bonusPoint;                    // 가진 보너스 점수에 제품의 보너스 점수 추가
        item.add(p);                                // 구입한 제품을 Vector에 저장
        System.out.println(p + "을/를 구입하셨습니다.");
    }

    void refund(Product p) {    // 제품 환불
        if(item.remove(p)) {                        // 제품을 Vector에서 제거
            money += p.price;
            bonusPoint -= p.bonusPoint;
            System.out.println(p + "을/를 반품하셨습니다.");
        } else {                                    // 제거에 실패한 경우
            System.out.println("구입하신 제품중 해당 제품이 없습니다.");
        }
    }

    void summary() {            // 구매한 물품에 대한 정보 요약, 출력
        int sum = 0;            // 구입한 물품의 가격 합계
        String itemList = "";    // 구입한 물품 목록

        if(item.isEmpty()) {    // Vector가 비어있는지 확인
            System.out.println("구입하신 제품이 없습니다.");
            return;
        }

        // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 생성
        for (int i=0; i<item.size(); i++) {
            Product p = (Product)item.get(i);        // Vector의 i번째에 있는 객체를 얻음 (Product로 형변환했음을 관찰)
            sum += p.price;
            itemList += (i==0) ? "" + p : ", " + p;
        }
        System.out.println("구입하신 물품의 총 금액은 " + sum + "만원 입니다.");
        System.out.println("구입하신 제품은 " + itemList + "입니다.");
    }
}

class PolyArgumentTest3 {
    public static void main(String args[]) {
        Buyer b = new Buyer();
        Tv tv = new Tv();
        Computer com = new Computer();
        Audio audio = new Audio();

        b.buy(tv);
        b.buy(com);
        b.buy(audio);
        b.summary();
        System.out.println();
        b.refund(com);
        b.summary();
    }
}

'Programming Language > Java' 카테고리의 다른 글

[Java] 09. OOP - 추상 클래스(abstract class)  (0) 2024.06.24
[Java] 07. OOP - 상속과 접근제어자  (0) 2024.03.12
[Java] 06. OOP - 클래스와 객체  (0) 2024.03.12
[Java] 05. 배열  (0) 2024.03.12
[Java] 04. 변수  (0) 2024.03.12