Java의 정석_기초편

다형성(polymorphism)

DJDU 2022. 10. 7. 17:34

다형성

  • 조상타입 참조변수로 자손타입 객체를 다루는 것 ⭐️
  • 객체와 참조변수의 타입이 일치할 때와 일치하지 않을 때의 차이
  • 자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 없다.
더보기

조상타입 참조변수로 자손타입 객체를 다루는 것

  • 여러 가지 형태를 가질 수 있는 능력
Tv t = new Tv();
SmartTv c = new SmartTv();

지금까지 우리는 생성된 인스턴스를 다루기 위해서, 인스턴스의 타입과 일치하는 타입의 참조변수만을 사용했다. 즉, Tv 인스턴스를 다루기 위해서는 Tv 타입의 참조변수를 사용하고, SmartTv인스턴스를 다루기 위해서는 SmartTv타입의 참조변수를 사용했다.

Tv t = new SmartTv(); // 타입 불일치 -> OK. 
                      // 일반적으로 참조변수 타입과 객체 타입이 일치해야 함
                      // 하지만, 다형성은 타입이 불일치할 수 있고, 두 가지 '장점' 있음

타입이 불일치 할 경우 장점이 두 가지 있다. 

 

객체와 참조변수의 타입이 일치할 때와 일치하지 않을 때의 차이?

SmartTv s = new SmartTv();  // 참조변수와 인스턴스의 타입이 일치
Tv      t = new SmartTv();  // 조상 타입의 참조변수로 자손 타입의 인스턴스 참조

 

 

자손타입의 참조변수로 조상타입의 객체를 참조할 수 없다.

  • 조상타입의 참조변수로 자손타입의 인스턴스를 참조하는 경우,
    참조변수는 조상타입의 참조변수에 정의된 멤버만 사용할 수 있다.
  • 참조변수가 사용할 수 있는 멤버의 개수는 객체의 멤버 개수보다 적거나 같아야 한다.
  • 자손타입의 참조변수로 조상타입의 객체를 참조할 수 없다.
Tv      t = new SmartTv();   // Ok. 허용
SmartTv c = new Tv();        // 에러. 허용 안 됨

리모컨으로 인스턴스에 없는 기능을 사용하면 에러가 발생한다.

자주 묻는 질문 ❓

  • Q1. 참조변수의 타입은 인스턴스의 타입과 반드시 일치해야 하나요?
  • Q2. 참조변수가 조상타입일 때와 자손타입일 때의 차이는?
  • Q3. 자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 있나요?
더보기

Q1. 참조변수의 타입은 인스턴스의 타입과 반드시 일치해야 하나요?
A. 아닙니다. 일치하는 것이 보통이지만 일치하지 않을 수도 있습니다.


Q2. 참조변수가 조상타입일 때와 자손타입일 때의 차이는?
A. 참조변수로 사용할 수 있는 멤버의 개수가 달라집니다.


Q3. 자손 타입의 참조변수로 조상 타입의 객체를 가리킬 수 있나요?
A. 아니요. 허용되지 않습니다.

참조변수의 형변환

  • 사용할 수 있는 멤버의 갯수를 조절하는 것(리모컨 변경)
  • 조상﹒자손관계의 참조변수만 서로 형변환 가능 ⭐
  • 참조변수가 가리키는 실제 객체가 무엇인지가 중요하고, 그 멤버의 갯수를 넘어서면 안된다.
  • 조상타입(4) ← 자손타입(5), 형변환 생략 불가(증가, 위험)
  • 자손타입(5) ← 조상타입(4), 형변환 생략 가능(감소, 안전)
더보기

사용할 수 있는 멤버의 갯수를 조절하는 것(리모컨 변경)

기본형 형변환이 값을 변환하는 것이라면, 참조변수의 형변환은 멤버의 갯수를 조절하는 것이다.

조상﹒자손관계의 참조변수만 서로 형변환 가능 ⭐

class Car {}
class FireEngine extends Car { }
class Ambulance  extends Car { }
{"originWidth":1280,"originHeight":785,"style":"alignCenter","width":557,"height":342,"caption":"
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!!!");
    }
}
FireEngine fe = new FireEngine();

Car car = (Car)fe;                   // OK. 조상인 Car타입으로 형변환(생략가능)
FireEngine fe2 = (FireEngine)car;    // OK. 자손인 FireEngine타입으로 형변환(생략불가)
Ambulance a = (Ambulance)fe2;        // 에러. 상속관계가 아닌 클래스 간의 형변환 불가
FireEngine fe = new FireEngine();
리모컨 fe가 사용할 수 있는 멤버 : 전체 멤버
Car car = (Car)fe;                   // OK. 조상인 Car타입으로 형변환(생략가능)
리모컨 car가 사용할 수 있는 멤버 : 4개
FireEngine fe2 = (FireEngine)car;    // OK. 자손인 FireEngine타입으로 형변환(생략불가)
리모컨 fe2가 사용할 수 있는 멤버 : 전체 멤버

예제7-7

더보기

참조변수가 가리키는 실제 객체가 무엇이냐가 중요

  • 대부분, 형변환이 되냐 안 되냐만 따지는데
  • 참조변수가 가리키는 실제 객체가 무엇이냐가 더 중요하다.
  • 실제 객체의 멤버 개수가 몇개인지를 파악해야 한다.

멤버 개수가 5개라면, 5개를 넘지 않도록 하는 것이 중요하다. 5개 내에서 줄이고 늘리는 건 다음 문제이다.

코드

class Ex7_7 {
    public static void main(String[] args) {
        Car car = null;
        FireEngine fe = new FireEngine();

        FireEngine fe2 = (FireEngine)car;  // 조상 -> 자손으로 형변환
        Car car2 = (Car)fe2;
        car2.drive();  // NullPointerException발생
        ...

결과

Exception in thread "main" java.lang.NullPointerException
	at ch07.Ex7_7.main(Ex7_7.java:10)

코드2

class Ex7_7 {
    public static void main(String[] args) {
        Car car = new Car();
        FireEngine fe = (FireEngine)car;  // 형변환 런타임 에러. java.lang.ClassCastException
        fe.water();  // 컴파일 OK.
    }
}

결과2

Exception in thread "main" java.lang.ClassCastException: 
class ch07.Car cannot be cast to class ch07.FireEngine
	at ch07.Ex7_7.main(Ex7_7.java:6)

실제 객체의 멤버 개수가 몇개인지를 파악하는 것이 중요하고 했다.

이 예제에서 실제 객체는 Car타입이다. 따라서, Car인스턴스의 멤버는 총 4개이다.

Car인스턴스를 가리키는 참조변수 car를 FireEngine으로 형 변환을 해서 그 주소값을 FireEngine타입 참조변수 fe에 저장했다. 참조변수 fe가 사용할 수 있는 멤버 개수는 5개이다. 리모컨 버튼 개수보다 인스턴스의 멤버 개수가 더 적기 때문에 분명 잘못된 코드이다.

하지만, 컴파일러는 형 변환 연산자만 써줘서 타입만 맞으면 에러를 잡아내지 못 한다. 하지만, 실행을 해보면 ClassCastException이라는 실행 시 에러(Runtime Error)가 발생한다.

instanceof 연산자

  • 참조변수의 형변환 가능 여부에 사용. 가능하면 true 반환.
  • 형변환 전에 반드시 instanceof로 확인해야 함.
  • (참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용)
더보기

조변수의 형변환 가능 여부에 사용. 가능하면 true 반환.

  1. 형변환 확인(instanceof연산자 사용)
  2. 형변환

형변환 전에 반드시 instanceof로 확인해야 함.

  • 메서드가 호출될 때, 매개변수로 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨받겠지만 어떤 인스턴스인지 알 길이 없다.
  • 그래서 instanceof 연산자로 참조변수 c가 가리키고 있는 인스턴스의 타입을 체크한다.
  • 형변환을 하는 이유는 인스턴스의 원래 기능을 모두 사용하려고 하기 때문이다.
  • Car타입의 리모컨인 c로는 water()를 호출할 수 없기 때문에 리모컨을 FireEngine타입으로 바꿔서 water()를 호출한다.

자주 묻는 질문 ❓

  • Q1. 참조변수의 형변환은 왜 하나요?
  • Q2. instanceof연산자는 언제 사용하나요?
더보기

Q. 참조변수의 형변환은 왜 하나요?
A. 참조변수(리모콘)를 변경함으로써 사용할 수 있는 멤버의 갯수를 조절하기 위해서


Q. instanceof연산자는 언제 사용하나요?
A. 참조변수를 형변환하기 전에 형변환 가능여부를 확인할 때

다형성의 장점

  1. 다형적 매개변수 :참조형 매개변수는 메서드 호출 시, 자신과 같은 타입 또는 자손타입의 인스턴스를 넘겨줄 수 있다.
  2. 하나의 배열에 여러 종류의 객체 저장 : 조상타입의 배열에 자손들의 객체를 담을 수 있다.
더보기
Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();
  • 위의 코드를 Product타입의 참조변수 배열로 처리하면 아래와 같다.
Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
Product[] cart = new Product[10];     // 구입한 물건을 담을 배열

void buy(Product p) {
    if(money < p.price) {
        System.out.println("잔액이 부족하여 물건을 살 수 없습니다.");
        return;
    }

    money -= p.price;                
    bonusPoint += p.bonusPoint;      
    cart[i++] = p;                    // 제품을 Product[] cart에 저장한다.
    System.out.println(p + "을/를 구입하셨습니다.");
}

Vector클래스

  • 가변배열 기능(11장)
  • Object[] - 모든 종류의 객체 저장 가능

🍪

📄 - 객체지향개념 암기노트 바로가기