Java 다형성(polymorphism)

yesjuhee·2024년 10월 24일

Java 공부

목록 보기
12/17

남궁성님의 Java의 정석 3판 "Chapter 07 객체지향 프로그래밍 2"을 읽고 저의 방식대로 정리 한 글입니다.

객체지향개념에서 다형성이란 ‘여러가지 형태를 가질 수 있는 능력’을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다.

이를 좀 더 구체적으로 말하자면, 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다. 모든 참조변수는 null 또는 4byte의 주소 값이 저장되며, 참조변수의 타입은 참조할 수 있는 객체의 종류와 사용할 수 있는 멤버의 수를 결정한다. 또한 상속 관계에서는 자손 클래스가 조상 클래스보다 항상 멤버 개수가 더 많다. 따라서 조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있지만 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다. 또한 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하여도 자손 클래스의 모든 멤버를 사용할 수는 없다.

class Tv {
	boolean power;
	int channel;
	
	void power() { power = !power }
	void channelUp() { ++chanel; }
	void channelDown() { -- chanle; }
}
class CaptionTv {
	String text;
	void caption() { ... }
}

/**
Tv t1 = new Tv();
Tv t2 = new CaptionTv();
CaptionTv c3 = new CaptionTv();
CaptionTv c4 = new Tv(); (x)
*/

위의 예시에서 t2는 조상 클래스 타입의 참조 변수로 자손 클래스의 인스턴스를 참조하고 있다. 즉 Tv 타입의 참조 변수로 다양한 타입의 객체를 참조함으로써 다형성을 프로그램적으로 구현하고 있다. 이때 t2t3 는 둘 다 CaptionTv 타입의 인스턴스를 참조하고 있지만 사용할 수 있는 멤버의 개수가 다르다. 즉 t2t3와 다르게 t2.text t2.caption()과 같은 사용이 불가능하다. 또한 c4와 같이 자손 클래스 타입의 참조변수가 조상 클래스의 인스턴스를 참조하도록 하는 것은 불가능하다.

참조변수의 형변환

기본형 변수와 같이 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자손타입의 참조변수로 형변환만 가능하다. 또한 공통 조상을 가지고 있는 서로 다른 클래스의 경우 아무런 관계가 없기 때문에 형변환이 불가능하다.

기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자손타입의 참조변수를 조상타입으로 형변환하는 경우에는 형변환을 생략할 수 있다. 조상타입의 참조변수를 자손타입의 참조변수로 변환하는 것을 다운캐스팅(down-casting)이라고 하며, 자손타입의 참조변수를 조상타입의 참조변수로 변환하는 것을 업캐스팅(up-casting)이라고 한다.

형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 참조변수가 참조하고 있는 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것 뿐이다.

컴파일 시에는 참조변수의 타입만 체크하기 때문에 실행 시 생성될 인스턴스의 타입에 대해서는 전혀 알지 못한다. 참조변수와 인스턴스간의 관계는 런타임에 확인된다.

/** 예시 1 */
Tv t = null;
CaptionTv c1 = new CationTv();
CaptionTv c2 = null;

c1.caption();
t = c1;      // 형변환
t.caption(); // CompileError
c2 = (CaptionTv)t;
c2.caption();
  • t = c1; t = (Tv)c1; 에서 형변환이 생략되었다(업캐스팅).
  • t.caption(); 참조변수 t 의 타입이 Tv임은 컴파일타임에 결정된다. 따라서 t.caption()과 같이 사용하는 것이 불가능함을 컴파일타임에 파악할 수 있다.
  • c2 = (CaptionTv)t; 다운캐스팅이므로 형변환을 명시하였다.
  • c2.caption(); 참조변수 c2 의 타입이 CaptionTv임은 컴파일타임에 결정된다. 따라서 c2.caption()과 같이 사용하는 것이 가능함을 컴파일타임에 파악할 수 있다. 또한 c2가 참조하고 있는 인스턴스의 타입은 런타임에 CaptionTv로 결정이 되기 때문에 메서드를 실행시킬 수 있다.
/** 예시 2 */
Tv t1 = new Tv();
Tv t2 = null;
CaptionTv c = null;

t1.power();
c = (CaptionTv)t1;
c.power();
t2 = c;
t2.power();
  • c = (CaptionTv)t1; → 컴파일 타임에 참조변수 ct1간의 타입을 파악할 때, 둘은 상속관계이므로 형변환에 문제가 없다고 판단하여 컴파일은 성공한다. → 런타임에 t1이 참조하고 있는 인스턴스의 타입이 Tv임을 알 수 있다. 이때 자손클래스 타입의 참조변수가 조상클래스 타입의 인스턴스를 참조하는 것은 불가능하므로 런타임 에러가 발생한다. → 예시 1에서도 c2 = (CaptionTv)t;에서 동일한 형변환이 일어나지만 인스턴스의 타입에 따라 런타임 에러가 발생하지 않는다.

instanceof 연산자

참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용한다. 주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수가, 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 그리고 연산의 결과로 boolean 값을 반환한다. instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다. 실제 참조변수가 참조하는 인스턴스와 같은 타입의 instanceof 연산 이외의 조상타입의 instanceof 연산 결과에도 true를 얻는다.

void doWork(Car c) {
	if (c instanceof FireEngine) {
		FireEngine fe = (FireEngine)c;
		fe.water();
		...
	} else if (c instanceof Ambulances) {
		Ambulance a = (Ambulance) c;
		a.siren();
		...
	}
	...
}

class Car {}
class FireEngine extends Car {
	void water() { ... }
}
class Ambulance extends Car {
	void siren() { ... }
}

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

멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입이 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.

메서드의 경우 조상 클래스의 메서드를 자손 클래스에서 오버라이딩한 경우에 참조변수의 타입과 관계없이 항상 오버라아딩된 메서드(실제 인스턴스의 메서드)가 호출된다.

매개변수의 다형성

메서드의 매개변수에 다형성을 적용하면 여러개의 메서드를 하나로 통합하여 작성이 가능하다.

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) {
		if (money < p.price) {
			System.out.println("잔액이 부족하여 물건을 살 수 없습니다.);
			return;
		}
		money -= p.price;
		bonusPoint += b.bonusPoint;
		System.out.println(p + "을/를 구입했습니다.");
	}
}

Buyer 클래스의 buy 함수에서 매개변수가 Product 타입의 참조변수이다. 즉 이 함수는 메서드의 매개변수로 Product 클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다.

매개변수의 다형성의 또다른 예로 PrintStream 클래스에 정의되어있는 print(Object object) 메서드가 있다. Object 는 모든 클래스의 조상이므로 이 메서드의 매개변수로 어떤 타입의 인스턴스든 가능하다. 따라서 이 하나의 메서드로 모든 타입의 인스턴스를 처리할 수 있다.

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

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

Product p[] = new Product[3];
p[0] = new Tv();
p[1] = new Computer();
p[2] = new Audio();
profile
https://yesjuhee.tistory.com/

0개의 댓글