다형성
객제지향개념에서 다형성이란 '여러 가지 형태를 가질 수 있는 능력'을 의미하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 함으로써 다형성을 프로그램적으로 구현하였다. 이를 좀 더 구체적으로 말하자면 '부모 클래스 타입의 참조변수로 자식 클래스의 인스턴스를 참조할 수 있도록 하였다'는 것이다.
나는 이게 말로만 하면 무슨 소리인지 모르겠어서 예제로 이해하는 것이 더 쉬웠다.
예제
class Animal {
String name; //이름
int age; //나이
Animal() {
this("덕구", 1);
}
Animal(String n, int a) {
this.name = n;
this.age = a;
}
}
class Tiger extends Animal {
int rank; //서열
Tiger() {
this(1);
}
Tiger(int r) {
this.rank = r;
}
void roar() {
System.out.println("어흥");
}
}
위와 같이 Animal 클래스와 Tiger 클래스가 정의되어 있다고 하자. Animal 클래스와 Tiger 클래스는 서로 상속관계에 있으며, 이 두 클래스의 인스턴스를 생성하고 사용하기 위해서는 다음과 같이 할 수 있다.
Animal a = new Animal();
Tiger t = new Tiger();
이처럼 인스턴스의 타입과 참조변수의 타입이 일치하는 것이 보통이지만, 두 클래스가 서로 상속관계에 있을 경우, 부모 클래스 타입의 참조변수로 자식 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.
Animal a = new Tiger(); //부모 타입의 참조변수로 자식 인스턴스를 참조
그렇다면 인스턴스를 같은 타입의 참조변수로 참조하는 것과 부모타입의 참조변수로 참조하는 것이 어떤 차이가 있는지 알아보자.
Tiger t = new Tiger();
Animal a = new Tiger();
위 코드에서 Tiger 인스턴스 2개를 생성하고, 참조변수 t와 a가 생성된 인스턴스를 하나씩 참조하도록 하였다. 이 경우 실제 인스턴스가 Tiger 타입이라 할지라도, 참조변수 a로는 Tiger인스턴스의 모든 멤버를 사용할 수 없다.
Animal 타입의 참조변수로는 Tiger인스턴스 중에서 Animal클래스의 멤버들(상속받은 멤버 포함)만 사용할 수 있다. 따라서 생성된 Tiger인스턴스의 멤버 중에 Animal클래스에 정의되지 않은 멤버, rank와 roar()은 참조변수 a로 사용이 불가능하다. 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
그렇다면 반대로 자식타입의 참조변수로 부모타입의 인스턴스를 참조하는 것은 가능할까?
Tiger t = new Animal(); //불가능
불가능하다!! 실제 인스턴스인 Animal의 멤버 개수보다 참조변수 t가 사용할 수 있는 멤버 개수가 더 많기 때문이다. 따라서 이를 허용하지 않는다.
참조변수의 형변환
기본형 변수와 같이 참조변수도 형변환이 가능하다. 단, 서로 상속관계에 있는 클래스 사이에서만 가능하기 때문에 자식타입의 참조변수를 부모타입의 참조변수로, 부모타입의 참조변수를 자식타이브이 참조변수로의 형변환만 가능하다.
기본형 변수의 형변환에서 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자식타입의 참조변수를 부모타입으로 형변환하는 경우에는 형변환을 생략할 수 있다.
자식타입 -> 부모타입(Up-casting) : 형변환 생략 가능
자식타입 <- 부모타입(Down-casting) : 형변환 생략 불가
무슨 소린지 예시로 알아보자.
class Animal {
String name;
int age;
Animal() {
this("덕구", 1);
}
Animal(String n, int a) {
this.name = n;
this.age = a;
}
}
class Tiger extends Animal {
int rank; //서열
Tiger() {
this(1);
}
Tiger(int r) {
this.rank = r;
}
void roar() {
System.out.println("어흥");
}
}
class Lion extends Animal {
int rank;
Lion() {
this(1);
}
Lion(int r) {
this.rank = r;
}
void roar() {
System.out.println("으르렁");
}
}
Animal클래스는 Tiger클래스와 Lion클래스의 부모다. 따라서 Animal타입의 참조변수와 Tiger타입의 참조변수 간에는 형변환이 가능하다. Animal타입의 참조변수와 Lion타입의 참조변수 간에도 형변환이 가능하다. 하지만 Tiger타입의 참조변수와 Lion타입의 참조변수 간에는 서로 형변환이 가능하지 않다.
예제를 통해 형변환이 어떻게 작동하는지 알아보자.
Animal ani = null;
Tiger ti = new Tiger();
Tiger ti2 = null;
ani = ti; //ani = (Animal)ti;에서 형변환 생략. 업캐스팅
ti2 = (Tiger)ani; //형변환 생략 불가. 다운 캐스팅
형변환을 할 때는 이 참조변수가 어떤 인스턴스 타입을 참조하는지 중요하기 때문에 형변환을 수행하기 전에 instanceof 연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스의 타입을 확인하는 것이 안전하다.
instanceof 연산자
참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용한다. 주로 조건문에 사용되며, instanceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자로 위치한다. 그리고 연산의 결과로 boolean값인 true와 false 중 하나를 반환한다.
예제
void casting(Animal ani) {
if(ani instanceof Tiger) {
Tiger ti = (Tiger)ani;
}
else if (ani instanceof Lion) {
Lion li = (Lion)ani;
}
}
참조변수와 인스턴스의 연결
부모 클래스에 선언된 멤버변수와 같은 이름의 인스턴스 변수를 자식 클래스에 중복으로 정의했을 때, 부모타입의 참조변수로 자식 인스턴스를 참조하는 경우와 자식타입의 참조변수로 자식 인스턴스를 참조하는 경우는 서로 다른 결과를 얻는다.
메서드의 경우 부모 클래스의 메서드를 자식의 클래스에서 오버라이딩한 경우에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩된 메서드)가 호출된다.
예제
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");
}
}
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();
}
}
실행 결과 p.x = 100 Child Method c.x = 200 Child Method |
타입은 다르지만 참조변수 p와 c 모두 Child 인스턴스를 잠조하고 있다. 그리고 Parent 클래스와 Child 클래스는 서로 같은 멤버들을 정의하고 있다.
이 때 부모타입의 참조변수 p로 Child 인스턴스의 멤버들을 사용하는 것과 자식 타입의 참조변수 c로 Child 인스턴스의 멤버들을 사용하는 것의 차이를 알 수 있다.
메서드인 method()의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴스의 타입인 Child 클래스에 저의된 메서드가 호출되는 것을 볼 수 있다.
매개변수의 다형성
참조변수의 다형적인 특징은 메서드의 매개변수에도 적용된다.
예제
class product {
int price; //제품 가격
int bonusPoint; //제품구매 시 제공하는 보너스 점수
}
class Customer {
int money = 1000;
int bonusPoint = 0;
}
class Tv extends product {};
class Computer extends product {};
class Audio extends product {};
위와 같은 예시가 있다고 하자. Customer 클래스 안에 물건을 구입하는 기능의 메서드를 구현하고자 한다. 구입할 대상이 필요하므로 매개변수로 구입할 제품을 넘겨받아야 한다.
void buy(Tv t) {
money = money - t.price; //고객이 가진 돈에서 티비 가격을 뺀다.
bonusPoint = bonusPoint + t.bonusPoint //고객의 보너스 점수에 티비의 보너스 점수를 더한다.
}
이 메서드는 Tv를 구매할 수 있는 메서드다. 하지만 buy(Tv t)메서드로는 Tv밖에 살 수 없기 때문에 다른 제품들을 구입하기 위해서는 매개변수의 타입에 맞게 메서드를 추가로 선언해주어야 한다.
void buy(Computer c) {
money = money - c.price;
bonusPoint = bonusPoint + c.bonusPoint
}
void buy(Audio a) {
money = money - a.price;
bonusPoint = bonusPoint + a.bonusPoint
}
하지만 제품의 종류가 늘어날 때마다 Customer 클래스 안에 새로운 buy메서드를 추가하는 것은 귀찮은 일이다. 여기서 매개변수에 다형성을 적용하면 하나의 메서드로 간단히 처리할 수 있다.
void buy(Product p) {
money = money - p.price;
bonusPoint = bonusPorint + p.bonusPoint;
}
매개변수가 Product 타입의 참조변수이기 때문에, 메서드의 매개변수로 Product클래스의 자식타입의 참조변수면 어느 것이나 매개변수로 받아들일 수 있다. 그리고 Product 클래스에 price와 bonusPoint가 선언되어 있기 때문에 참조변수 p로 인스턴스의 price와 bonusPoint를 사용할 수 있다.
여러 종류의 객체를 배열로 다루기
부모 타입의 참조변수로 자식 타입의 객체를 잠조하는 것이 가능하므로, Product클래스가 Tv, Computer, Audio클래스의 조상일 때
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();
이처럼 부모타입의 참조변수 배열을 사용하면, 공통의 부모를 가진 서로 다른 종류의 객체를 배열로 묶어서 관리할 수 있다.
총평
그렇다고 한다.
'✏️Java 공부 > Java의 정석' 카테고리의 다른 글
[Java 공부/Java의 정석] Chapter.07 : 객체지향 프로그래밍 2 - 6 (인터페이스) (0) | 2022.07.08 |
---|---|
[Java 공부/Java의 정석] Chapter.07 : 객체지향 프로그래밍 2 - 5 (추상클래스) (0) | 2022.07.07 |
[Java 공부/Java의 정석] Chapter.07 : 객체지향 프로그래밍 2 - 3 (제어자) (0) | 2022.07.06 |
[Java 공부/Java의 정석] Chapter.07 : 객체지향 프로그래밍 2 - 2 (오버라이딩) (0) | 2022.07.06 |
[Java 공부/Java의 정석] Chapter.07 : 객체지향 프로그래밍 2 - 1 (상속) (0) | 2022.07.04 |
댓글