다형성이란?
- 객체지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미합니다.
- 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였습니다.
- 자바에서는 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하여 다형성을 구현했습니다.
public class Tv {
boolean power; // 전원상태 (on/off)
int channel; // 채널
void power(){ power = !power;}
void channelUp(){ ++channel; }
void channelDown(){ --channel; }
}
class CaptionTv extends Tv{
String text; // 캡션을 보여주기 위한 문자열
void caption(){/* 내용 생략 */}
}
- Tv와 CaptionTv는 서로 상속관계에 있기 때문에, 조상 클래스 타입의 참조변수( Tv 타입 )로 자손 클래스의 인스턴스( CaptionTv의 인스턴스 )를 참조하는 것이 가능합니다.
Tv t = new CaptionTv(); // 조상 타입의 참조 변수로 자손 인스턴스를 참조
CaptionTv c = new CaptionTv(); // 같은 타입의 참조 변수로 인스턴스 참조
// 아래와 같이 자손 타입의 참조 변수로 조상 인스턴스를 참조할 수 없음 ( 컴파일 에러 )
CaptionTv d = new Tv();
- 위의 t와 같이 조상타입의 참조변수로 자손타입의 인스턴스를 참조하는 것이 가능합니다.
- 이 경우 같은 타입의 참조변수를 c와 달리 t는 Tv클래스의 멤버들( 상속받은 멤버 포함 )만 사용할 수 있습니다.
( CaptionTv 인스턴스 멤버 중 Tv클래스에 정의되지 않은 text, caption()은 사용 불가합니다. ) - 즉, t와 c가 참조하는 인스턴스는 둘 다 같은 타입의 인스턴스를 참조하지만, 참조변수의 타입에 따라 사용할 수 있는 맴버의 개수가 달라집니다.
- 반대로 자손 타입의 참조변수로 조상타입의 인스턴스를 참조하는 경우 존재하지 않는 멤버를 사용하고자 하는 가능성이 있기 때문에 이를 허용하지 않습니다. ( d.text, d.caption()과 같은 방식으로 참조하려고 할 수 있음 )
- 정리하자면, 참조변수가 사용할 수 있는 맴버의 개수는 인스턴스의 맴버 개수보다 같거나 적어야 합니다.
참조변수의 형변환
- 서로 상속관계에 있는 클래스 사이에서 참조변수의 형변환이 가능합니다.
- 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자식타입의 참조변수로 형변환 할 수 있습니다.
자손타입 -> 조상타입 ( Up - casting ) : 형변환 생략가능
조상타입 -> 자손타입 ( Down - casting ) : 형변환 생략불가
public 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!!!");
}
}
class Ambulance extends Car {
void siren() { // 사이렌 울리는 기능
System.out.println("siren~~~");
}
}
- Car 클래스는 FireEngine 클래스와 Ambulance 클래스의 조상이며, FireEngine와 Ambulance는 서로 아무 관계가 없다. ( 자바의 상속에서 형제 관계라는 것은 없음, 형변환 불가)
- Car 타입의 참조변수와 FireEngine 타입, Ambulance 타입의 참조변수 간에는 형변환이 가능합니다.
Car car = null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
car = fe; // car = (Car)fe;에서 형변환 생략됨. 업캐스팅
fe2 = (FireEngine)car; // 형변환을 생략할 수 없음. 다운캐스팅
- 업캐스팅의 경우, 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 형변환을 생략할 수 있습니다.
- 다운캐스팅의 경우, 참조변수가 다룰 수 있는 멤버의 개수를 늘리는 것이므로, 실제 인스턴스의 멤버 개수보다 참조변수가 사용할 수 있는 멤버의 개수가 많아지므로 문제가 발생할 가능성이 있어서 형변환을 생략할 수 없는 것입니다.
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();
}
출력 결과
water!!!
water!!!
- 1. Car타입의 참조변수 car를 null로 초기화했으며 FireEngine타입의 인스턴스를 fe가 참조하도록 합니다.
- 2. car = fe; 참조변수 fe가 참조하고 있는 인스턴스를 car이 참조하도록 합니다. fe의 값(fe가 참조하는 인스턴스 주소)이 car에 저장됩니다. car은 Car타입이므로 Car타입의 멤버가 아닌 water()는 사용할 수 없습니다.
- 3. 참조변수 car가 참조하고 있는 인스턴스의 주소를 fe2에 저장합니다. 이 때, 조상클래스에서 자식클래스로의 형변환 ( 다운캐스팅 )이 일어나기 때문에 명시적으로 형변환 하였습니다. fe2는 FireEngine 타입이므로 FireEngine 인스턴스의 모든 멤버들을 사용할 수 있습니다. ( water() 사용 가능 )
public class CastingTest2 {
public static void main(String[] args) {
Car car = new Car();
Car car2 = null;
FireEngine fe = null;
car.drive();
fe = (FireEngine) car // Car타입의 인스턴스를 자손의 참조변수가 참조할 수 없습니다.
fe.drive();
car2 = fe;
car2.drive();
}
}
- 위 코드는 컴파일은 성공하지만, 실행 시 에러( ClassCastException )가 발생합니다.
- 조상 타입의 참조변수를 자손타입의 참조변수로 형변환한 것이기 때문에 문제가 없어 보이지만, 참조변수 car이 참조하는 인스턴스의 타입이 Car이기 때문에 에러가 발생합니다. ( 조상타입의 인스턴스를 자손타입의 참조변수로 참조하는 것은 허용되지 않는다. )
서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.
Instanceof 연산자
- instanceof는 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 주로 사용하는 연산자입니다.
- 연산의 결과로 boolean 값인 true와 false 중의 하나를 반환합니다.
- instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻합니다.
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()); // 클래스의 이름을 출력
}
}
실행 결과
This is a FireEngine instance.
This is a Car instance.
This is an Object instance.
FireEngine
- FireEngine 클래스는 Object 클래스와 Car 클래스를 상속받았기 때문에, FireEngine 인스턴스는 Object 인스턴스와 Car 인스턴스를 포함하고 있는 셈입니다.
- 따라서 실제 인스턴스와 같은 타입의 instanceof연산 이외에 조상타입의 instanceof 연산에도 true를 결과로 얻으며, instanceof연산의 결과가 true라는 것은 검사한 타입으로 형변환을 해도 아무런 문제가 없다는 뜻입니다.
참조변수와 인스턴스의 연결
- 조상 클래스의 메서드를 자손 클래스에서 오버라이딩한 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드가 호출됩니다. ( 오버라이딩 된 메서드 )
- 맴버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 맴버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 맴버변수가 사용됩니다.
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.method();
System.out.println("c.x = " + c.x);
c.method();
}
}
class Parent {
int x = 100;
void method() {
System.out.println("Parent's Method");
}
}
class Child extends Parent {
int x = 200;
void method() {
System.out.println("Child's Method");
}
}
# 실행 결과
p.x = 100
Child's Method
c.x = 200
Child's Method
- 타입은 다르지만, 참조변수 p와 c 모두 Child 인스턴스를 참조하고 있다.
- 결과를 보면, 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child 클래스에 정의된 메서드가 호출되며, 인스턴스변수인 x는 참조변수의 타입에 따라서 달라진다.
매개변수의 다형성
- 참조변수의 다형적인 특징은 메서드의 매개변수에도 적용할 수 있습니다.
public class Product {
int price; // 제품의 가격
int bonusPoint; // 제품구매 시 제공하는 보너스점수
public Product(int price) {
this.price = price;
bonusPoint = (int) (price / 10.0); // 보너스점수는 제품 가격의 10%
}
}
class Audio extends Product {
public Audio() {
super(100); // Audio의 가격을 100만원으로 지정
}
public String toString() { // Object 클래스의 toString() 메서드 오버라이딩
return "Audio";
}
}
class Computer extends Product {
Computer() {
super(200); // 컴퓨터의 가격을 200으로 지정
}
public String toString() {
return "Computer";
}
}
class Buyer { // 고객
int money = 1000; // 소유금액
int bonusPoint = 0; // 소유보너스점수
void buy(Product product) {
money -= product.price; // 가진 돈에서 제품의 가격을 뺍니다.
bonusPoint += product.bonusPoint; // 가진 보너스점수에 제품구매 시 제공하는 보너스점수를 더해줍니다.
System.out.println(product + "을/를 구매하셨습니다.");
}
}
class PolyArgumentTest {
public static void main(String[] args) {
Buyer b = new Buyer();
b.buy(new Audio());
b.buy(new Computer());
System.out.println("현재 남은 돈은" + b.money + "만원입니다.");
System.out.println("현재 보너스점수는" + b.bonusPoint + "점입니다.");
}
}
# 실행 결과
Audio을/를 구매하셨습니다.
Computer을/를 구매하셨습니다.
현재 남은 돈은700만원입니다.
현재 보너스점수는30점입니다.
- Buyer 클래스의 buy 메서드를 보시면 각각의 타입이 아닌 Product p 만을 메서드의 매개변수로 받고 있습니다.
- 매개변수가 Product 타입의 참조변수라는 것은, 메서드의 매개변수로 Product 클래스의 자손타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있기 때문입니다.
'Java' 카테고리의 다른 글
[ JAVA의 정석 ] 지네릭스 ( Generics ) (0) | 2023.08.13 |
---|---|
[ Java 의 정석 ] 내부 클래스 ( Inner class ) (0) | 2023.08.03 |
[ Java의 정석 ] 인터페이스 ( interface ) (0) | 2023.08.01 |
[ Java의 정석 ] 추상클래스 ( abstract class ) (0) | 2023.07.29 |
[ Java의 정석 ] 제어자 ( modifier ) (0) | 2023.07.27 |