[Java] 객체지향 프로그래밍(oop) - 다형성(polymorphism)

SolChan Kim·2024년 1월 9일

📖다형성이란?

  • 객체지향이론적인 다형성의 정의

    • 여러가지 형태를 가질 수 있는 능력
  • 프로그래밍에서의 다형성

    • 하나의 참조변수로 여러 타입의 객체를 참조할 수 있는 것
    • 조상타입의 참조변수로 자손타입의 객체를 다룰 수 있는 것을 말한다.
public class Car {
	String name;
	String color;
	int speed;
	
	public void speedUp() {speed++;} 
}

public class NewCar extends Car {
	String update;
	
	public void showUpdate() {
		System.out.println("updata : " + update);
	}
}

Car 클래스와 Car클래스를 조상으로 하는 NewCar클래스가 정의되어
있을 때 각 클래스의 인스턴스를 생성하면 다음과 같다.

// Car인스턴스는 Car타입의 참조변수(car01)가 참조함
Car car01 = new Car();
// NewCar인스턴스는 NewCar타입의 참조변수(car02)가 참조함
NewCar car02 = new NewCar();

그런데 NewCar인스턴스를 Car타입의 참조변수로 다루는 것도 가능한데,
이 것이 바로 다형성이다.

Car car03 = new NewCar();

🤔newCar인스턴스를 NewCar타입의 참조변수로 다루는 것과 Car타입의 참조변수로 다루는 것의 차이는 무엇일까?

NewCar인스턴스를 newCar타입의 참조변수 외에도 조상인 Car클래스 타입의 참조변수로도 다룰 수 있다.(다형성)

이 둘의 차이는 사용할 수 있는 멤버의 개수다.
NewCar타입의 참조변수로는 NewCar인스턴스의 모든 멤버를 사용할 수 있지만
Car타입의 참조변수로는 NewCar인스턴스의 모든 멤버를 사용할 수 없다.
-> Car클래스에 정의된 멤버만 사용할 수 있다.

실제 인스턴스가 NewCar인스턴스임에도 불구 하고
Car타입의 참조변수로는 Car클래스에 정의된 4개의 멤버만 사용할 수 있다.

NewCar에 정의된 멤버변수 update와 showUpdate메서드는 Car타입의 참조변수로는 사용할 수 없다.

결론 : 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.


조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있지만 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.

조상타입(Car)의 참조변수로 자손타입(newCar)의 인스턴스를 참조하면
Car클래스가 가지고 있는 멤버만 사용하기 때문에 문제가 발생하지 않는다.
-> 자손타입이 가지고 있는 멤버는 있는지도 모르는 상태

그러나 자손타입의 참조변수로 조상타입의 인스턴스를 참조하게 되면
문제가 발생한다.
-> 참조변수의 타입에 있는 멤버와 기능이 참조변수가 참조하고 있는 인스턴스의 타입 안에 멤버중에 없기 때문이다.


📖참조변수의 형변환

  • 서로 상속관계에 있는 타입간의 형변환만 가능하다.

  • 자손 타입에서 조상타입으로 형변환 하는 경우 : 형변환 생략 가능

public class Car {
	String color;
	int door;
	
	public void drive() {
		System.out.println("drive");
	}
	
	public void stop() {
		System.out.println("stop");
	}
}

public class FireEngine extends Car {
	public void water() {
		System.out.println("water");
	}
}

public class Ambulance extends Car {
	public void siren() {
		System.out.println("siren");
	}
}

기본형 변수뿐만 아니라 참조형 변수도 형변환이 가능하다.
-> 서로 상속관계에 있을 때만 형변환이 가능

Car c = new Car();
Car c2 = new Car();
FireEngine f = new FireEngine();
Ambulance a = new Ambulance();

// 업캐스팅(Up-casting)
// 하나의 자손은 하나의 조상을 가진다.
// 조상이 고유하므로 형변환 생략이 가능하다.
c = a;
// 우변(a)의 타입을 좌변(c)의 타입과 맞춰야 한다.
// a의 타입은 자손이고 c의 타입은 조상이다.
// 따라서 형변환 생략이 가능하다.

// 다운캐스팅(Down-casting)
// 하나의 조상은 다수의 자손을 가질수 있다.
// 어느 자손으로 다운시키는건지 명시해야 하므로 형변환 생략불가
Ambulance a2 = (Ambulance)c;
// 우변(c)의 타입을 좌변(a2)의 타입과 맞춰야 한다.
// c의 타입은 조상이고 a2의 타입은 자손이다.
// 조상타입을 자손타입으로 맞춰야 하므로 형변환 생략 불가

🤔형변환 예제

❗중요

  • 모든 클래스는 자신 또는 자손 클래스의 메모리를 참조할 수 있다.

  • 조상 인스턴스는 조상자신의 멤버만 가지고 있다.(자손에 정의된 멤버와는 상관없다.)

  • 모든 클래스는 부모의 형 변환 없이는 메모리를 참조할 수 없다.

자손클래스 변수명 = 부모클래스의 인스턴스
- 자손 클래스에 있는 멤버가 조상클래스에 없기 때문에 형 변환 없이 참조 불가

- 참조하려면 부모가 자손으로 다운케스팅 되어야만 참조가 가능하다.
  - 다운케스팅을 통해 메모리 또한 자손에 맞게 형이 변환된다.
  - 자손클래스 변수명 = (자손클래스명) 부모클래스의 인스턴스
    - 자손클래스의 참조변수가 참조하는 인스턴스의 타입은 조상클래스다.
  • 다형성에서 모든 메소드들은
    선언된 타입 이 아니라, 참조 되는 타입에 의해서 결정된다.
  • 부모 클래스 참조변수에 자손의 인스턴스가 업케스팅 되어 참도되는 경우에만,
    부모 클래스 참조변수를 다운케스팅하여 자손 클래스 참조변수에 대입할 수 있다.

    • 선 upcasting, 후 downcasting.
  • 조상타입으로 선언된 참조변수는 자손인스턴스를 참조한다 하더라도 조상클래가 가지고있는 멤버만 사용가능

  • 자손타입으로 선언된 참조변수가 다운캐스팅된 조상타입의 참조변수가 참조하는 인스턴스를 참조할 경우 조상,자손이 가지고 있는 멤버를 모두 사용 가능

public class Parent {	
	public void parentMethod() {
		System.out.println("the method is parentMethod");
	}
}

public class Child extends Parent {
	public void childMethod() {
		System.out.println("the method is ChildtMethod");
	}
}

public class Test {
	public static void main(String[] args) {
		Parent parent01 = new Parent();
		Child child01 = new Child();
		Parent parent02 = null;
		
		// ==업케스팅 예시==
		// parent02 = child01; parent02 = (Parent)child01;
		// parent02의 타입 : Parent(조상)
		// parent02의 참조대상 : Child인스턴스
		parent02 = child01;
		
		
		// parent02.childMethod(); -> error
		// -> parent02가 Child인스턴스를 참조한다고 해서 Child클래스의 모든 멤버를 사용할 수 없다.
		//    parent02의 참조타입인 Parent(클래스)에 정의된 메서드만 사용할 수 있다.
		parent02.parentMethod(); // ok
		
		// ==재할당==
		// -> 자손인스턴스를 참조하고 있는 참조변수를 조상인스턴스를 참조하게 한다.
		// parent02의 타입 : Parent(조상)
		// parent02의 참조대상 : Parent인스턴스
		parent02 = parent01;
		
		// ==다운케스팅 예시==
		// parent02의 타입 : Parent(조상)
		// parent02의 참조대상 : Child인스턴스
		// - 조상은 자손이 다수기 때문에 어느 자손타입으로 형변환 할건지 지정해줘야 한다.
		// - 클래스는 자기 자신 or 조상의 타입으로만 형변환 할 수 있다.
		// - 현재 parent02는 Parent인스턴스를 
		//   참조하고 있기 때문에 Child타입으로 형변환 불가
		// - 따라서 업캐스팅을 통해서 parent02가 Child인스턴스를 참조한 다음
		//   다운 캐스팅을 진행해야 한다.
		// ==업캐스팅 진행== 
		parent02 = child01;
		
		// ==다운캐스팅 진행==
		// Child타입의 참조변수 child02를 선언하고
		// 선언된 참조변수는 Child타입으로 강제형변환된 참조변수parent02가
		// 참조하고 있는 인스턴스(Child인스턴스)를 참조하게 된다.
		// child02의 타입 : Child(자손)
		// child02의 참조대상 : Child인스턴스
		Child child02 = (Child)parent02;
		
		// ==child02는 Parent클래스와 Child클래스의 맴버를 사용할 수 있다?
		// -> 있다 : 자손은 조상의 멤버를 상속받고 있기 때문이다.
		child02.childMethod(); // ok
		child02.parentMethod(); // ok
	}
}

📖instanceof연산자

  • 참조변수가 참조하는 인스턴스의 실제 타입을 확인할 수 있다.

  • 이항연산자이며 피연산자는 참조형 변수와 타입. 연산결과는 true, false다.

  • instanceof의 연산결과가 true이면, 해당 타입으로 형변환이 가능하다.

public class InstanveOfTest {
	public static void method(Object obj) {
    	// 매개변수 obj가 참조하고 있는 인스턴스의 타입이 Car라면
		if(obj instanceof Car) {
			// Car타입의 참조변수 c를 선언한다.
            // c의 타입 : Car(자손) / obj의 타입(조상)
            // obj의 타입을 c와 맞춰야 하므로 다운캐스팅
            // c가 참조하는 인스턴스 = obj가 참조하는 인스턴스
			Car c = (Car)obj;
            // c가 참조하는 인스턴스에 drive()메서드 호출
			c.drive();
		}else if(obj instanceof FireEngine ) {
			FireEngine f = (FireEngine)obj;
			f.water();
		}
	}
	
	public static void main(String[] args) {
		Car c = new Car();
		FireEngine f = new FireEngine();
		
        // 아래 if문 모두 true인 이유
        // f의 인스턴스 타입은 FireEngine이다.
        // FireEngine클래스는 Car클래스를 상속받고 있기 때문에 
        // f의 인스턴스 타입이 Car타입이기도 하다.
		if(f instanceof FireEngine) {
			System.out.println("참조변수 f의 인스턴스 타입은 FireEngine");
		}
		if(f instanceof Car) {
			System.out.println("참조변수 f의 인스턴스 타입은 Car");
		}
        
        method(c);
	}
}

// result
참조변수 f의 인스턴스 타입은 FireEngine
참조변수 f의 인스턴스 타입은 Car
drive

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

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

public class Child extends Parent {
	// 조상의 멤버변수 x와 중복정의되어있다.
	int x = 200;
	
    // 조상의 메서드가 오버라이딩되어있다.
    @Override
	public void method() {
		System.out.println("Child method");
	}
}

public class Main {
	public static void main(String[] args) {
    	// 조상(Parent)타입의 참조변수 p가 자손(Child)의 인스턴스를 참조한다.
		Parent p = new Child();
        // 자손타입의 참조변수 c가 자손의 인스턴스를 참조한다.
		Child c = new Child();
		
        // Parent타입의 참조변수 p로 멤버변수 x를 접근하면
        // Parent클래스에 정의된 멤버변수 x와 연결된다.(100)
		System.out.println("p.x = " + p.x);
        
        // Child클래스에 정의된 메서드가 호출("Child method")
		p.method();
		
        // Child타입의 참조변수 c로 멤버변수 x를 접근하면
        // Child클래스에 정의된 멤버변수 x와 연결된다.(200)
		System.out.println("c.x = " + c.x); // 200
        
        // Child클래스에 정의된 메서드가 호출("Child method")
		c.method();
	}
}

// result
p.x = 100
Child method
c.x = 200
Child method

📖매개변수의 다형성

public class Product {
	int price; // 제품가격
	int bonusPoint; // 보너스 점수
}

public class Tv extends Product{}
public class Computer extends Product{}
public class Audio extends Product{}

public class Buyer{
	int money = 1000; // 소유금액
    int bonusPoint = 0; // 보너스 점수
    
    public void buy(Tv t){
    	money -= t.price;
        bonusPoint += t.bonusPoint;
    }
}

public class Main {
	public static void main(String[] args) {
		Buyer b = new Buyer();
		Tv tv = new Tv();
		Computer com = new Computer();
		
		Product p1 = new Tv();
		Product p2 = new Computer();
		Product p3 = new Audio();
		
		b.buy(tv);
		b.buy(com);
	}
}

Buyer클래스에 정의된 buy()메서드로는 Tv밖에 구매할 수 없다.
다른 제품을 사기 위해서는 제품을 매개변수로 받는 메서드를 새로 작성해야 한다.
제품이 추가될 때마다 메서드를 새로 작성하는 것은 비효율적이다.

그래서 buy()메서드의 매개변수를 Tv타입이 아닌 제품들의 공통조상인
Product타입으로 정의해야 한다.

public void buy(Product p){
    	money -= t.price;
        bonusPoint += t.bonusPoint;
    }

Product클래스는 모든 제품 클래스의 조상이라서
Tv인스턴스, Computer인스턴스, Audio인스턴스를 참조할 수 있다.

이렇게 매개변수의 타입을 공통조상으로 정의하면 여러 종류의 자손인스턴스를
넘겨받을 수 있는데 이것을 매개변수의 다형성이라 한다.


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

  • 업로드 예정...

0개의 댓글