다형성
다형성(polymorphism)이란 여러개를 의미하는 poly와 어떤 형태 또는 실체를 의미하는 morphism의 결합어로 하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 의미한다. 자바 프로그래밍에서 다형성은 한 타입의 참조 변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미한다. 구체적으로 이야기 하면 상위 클래스 타입의 참조 변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것이라 할 수 있다.
// 참조 변수의 다형성 예시
class Friend {
public void friendInfo() {
System.out.println("나는 당신의 친구입니다.");
}
}
class BoyFriend extends Friend {
public void friendInfo() {
System.out.println("나는 당신의 남자친구입니다.");
}
}
class GirlFriend extends Friend {
public void friendInfo() {
System.out.println("나는 당신의 여자친구입니다.");
}
}
public class FriendTest {
public static void main(String[] args) {
Friend friend = new Friend(); // 객체 타입과 참조 변수 타입의 일치
BoyFriend boyfriend = new BoyFriend();
Friend girlfriend = new GirlFriend(); // 객체 타입과 참조 변수 타입의 불일치
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
// 출력값
나는 당신의 친구입니다.
나는 당신의 남자친구입니다.
나는 당신의 여자친구입니다.
위의 예시에서 참조변수 friend와 boyfriend 모두 각각 Friend와 BoyFriend라는 타입과 일치하는 참조 변수 타입을 사용하는 것을 확인할 수 있다. 아래 라인에서 GirlFirend 클래스의 인스턴스를 생성하고 그것을 Friend 타입의 참조변수 girlfriend에 할당하고 있다. 원래라면 타입을 일치시키기 위해 GirlFirend를 참조 변수의 타입으로 지정해주어야 하지만, 그러지 않고 상위클래스 Friend 타입으로 지정해주고 있다. 이 경우 상위 클래스를 참조 변수의 타입으로 지정했기 때문에 자연스럽게 참조 변수가 사용할 수 있는 멤버의 개수는 상위클래스의 멤버의 수가 된다.
public class FriendTest {
public static void main(String[] args) {
Friend friend = new Friend(); // 객체 타입과 참조 변수 타입의 일치 -> 가능
BoyFriend boyfriend = new BoyFriend();
Friend girlfriend = new GirlFriend(); // 객체 타입과 참조 변수 타입의 불일치 -> 가능
// GirlFriend friend1 = new Friend(); -> 하위클래스 타입으로 상위클래스 객체 참조 -> 불가능
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
위의 예시에서는 상위 클래스인 Friend 타입으로 하위 클래스인 GirlFriend를 참조하는 것은 가능하지만 반대로 GirlFirend의 타입으로 상위클래스 객체 Friend를 참조하는 것은 불가능하다는 것을 알 수 있다. 그 이유는 실제 객체인 Friend의 멤버 개수보다 참조변수 friend1이 사용할 수 있는 멤버 개수가 더 많기 때문이다.
좀 더 구체적으로 설명하면 실제 참조하고 있는 인스턴스의 멤버를 기준으로 참조 변수의 타입의 멤버가 실제 인스턴스의 멤버 수보다 작은 것은 실제 사용할 수 있는 기능을 줄이는 것이기에 허용되지만, 그 반대의 경우는 참조하고 있는 인스턴스에 실제로 구현된 기능이 없어 사용이 불가하기 때문이다.
참조 변수의 타입 변환
참조변수의 타입 변환은 다르게 설명하면 사용할 수 있는 멤버의 개수를 조절하는 것을 의미한다. 타입 변환을 위해서는 세가지 조건을 충족해야 한다.
- 서로 상속 관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능하다.
- 하위 클래스 타입에서 상위 클래스 타입으로 타입변환(업캐스팅)은 형 변환 연산자(괄호)를 생략할 수 있다.
- 상위 클래스 타입에서 하위 클래스 타입으로 타입변환(다운캐스팅)은 형 변환 연산자(괄호)를 반드시 명시해야 한다.
또한, 다운 캐스팅은 업 캐스팅이 되어있는 참조변수에 한해서만 가능하다.
public class VehicleTest {
public static void main(String[] args) {
Car car = new Car();
Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
Car car2 = (Car) vehicle; // 하위 클래스 Car타입으로 변환(생략 불가능)
MotorBike motorBike = (MotorBike) car; // 상속 관계가 아니므로 타입 변환 불가 -> 에러발생
}
}
class Vehicle {
String model;
String color;
int wheels;
void startEngine() {
System.out.println("시동 걸기");
}
void accelerate() {
System.out.println("속도 올리기");
}
void brake() {
System.out.println("브레이크!");
}
}
class Car extends Vehicle {
void giveRide() {
System.out.println("다른 사람 태우기");
}
}
class MotorBike extends Vehicle {
void performance() {
System.out.println("묘기 부리기");
}
}
위의 예시를 보면 먼저 Vehicle 클래스가 있고 이로부터 각각 상속을 받아 만들어진 Car와 MotorBike 클래스가 있다. 먼저 Car 클래스의 인스턴스 객체 car를 생성하고 그 객체를 가리키는 참조변수 vehicle의 타입을 Vehicle로 지정하여 참조변수의 타입 변환을 실행하였다. 그 후 반대로 vehicle을 하위 클래스 타입인 Car로 타입변환하여 참조변수 car2에 할당했다. 이처럼 상속 관계에 있는 클래스 간에는 상호 타입 변환이 수행될 수 있다. 다만 하위 클래스를 상위 클래스 타입으로 변환하는 경우 타입 변환 연산자(괄호)를 생략할 수 있는 반면, 그 반대의 경우는, 업 캐스팅이 되어있다는 전제하에 다운캐스팅을 할 수 있으며, 타입 변환 연산자를 생략할 수 없다는 점에서 차이가 있다고 할 수 있다.
Car 클래스와 MotorBike 클래스는 상속관계가 아니므로 타입 변환이 불가하여 에러가 발생한다.
Instanceof 연산자
instanceof 연산자는 앞서 배웠던 참조 변수의 타입 변환, 즉 캐스팅이 가능한지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법 요소이다. 캐스팅 가능 여부를 판단하기 위해서는 두 가지, 객체를 어떤 생성자로 만들었는가 와 클래스 사이에 상속관계가 존재하는가 를 판단해야 한다. 프로젝트의 규모가 커지고 클래스가 많아지면 매번 이러한 정보를 확인하는 것이 어렵기 때문에 이를 해결하기 위해 자바는 instanceof 라는 연산자를 제공한다.
참조_변수 instanceof 타입
만약 참조_변수 instanceof 타입 을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며 반대로 false가 나오는 경우에는 타입 변환이 불가능하다. 만약에 참조 변수가 null 인 경우에는 false를 반환한다.
public class InstanceOfExample {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal instanceof Object); //true
System.out.println(animal instanceof Animal); //true
System.out.println(animal instanceof Bat); //false
Animal cat = new Cat();
System.out.println(cat instanceof Object); //true
System.out.println(cat instanceof Animal); //true
System.out.println(cat instanceof Cat); //true
System.out.println(cat instanceof Bat); //false
}
}
class Animal {};
class Bat extends Animal{};
class Cat extends Animal{};
위의 예시를 보면 Animal 클래스가 있고 Bat과 Cat 클래스가 각각 Animal 클래스를 상속받고 있다. 그리고 각각 객체를 생성하여 Animal 타입의 참조 변수에 넣고 instanceof 키워드를 사용하여 형 변환 여부를 확인하고 있다. Cat 객체를 예로 들어보면 생성된 객체는 Animal 타입으로 선언되어 있지만 다형적 표현방법에 따라 Object와 Animal 타입으로도 선언될 수 있다. 이렇게 소스코드가 길어지는 등 일일이 생성 객체의 타입확인이 어려운 상황에서 instanceof 연산자는 형 변환 여부를 확인하여 에러를 최소화하는 매우 유용한 수단이 될 수 있다.
다형성 활용 예제
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {};
class CaffeLatte extends Coffee {};
class Customer {
int money = 50000;
}
위에는 4개의 클래스가 있다. 커피의 가격 정보를 담고 있는 Coffee 클래스, 이를 상속 받는 Americano와 CaffeLatte 클래스, 5만원의 돈을 가지고 있는 고객을 의미하는 Customer 클래스가 있다. Americano와 CafeLatte 클래스는 아직 구현부가 작성되지 않았다. 위의 코드의 상황에서 고객이 5만원의 돈을 가지고 아메리카노 한 잔과 카페라떼 한 잔을 구입하는 경우를 생각해보자.
void buyCoffee(Americano americano) { // 아메리카노 구입
money = money - americano.price;
}
void buyCoffee(CaffeLatte caffeLatte) { // 카페라테 구입
money = money - caffeLatte.price;
}
위의 기능을 Customer 클래스에 추가할 수 있을 것이다. 다만 구입해야하는 커피의 종류가 다양해진다면, buyCoffee 메서드를 계속 추가해야하여 번거로워질 것이다. 이런 경우 객체의 다형성을 활용하여 문제를 해결할 수 있다.
void buyCoffee(Coffee coffee) { // 매개 변수의 다형성
money = money - coffee.price;
}
각각의 커피 타입이 아닌 상위클래스인 Coffee의 타입을 매개변수로 전달받으면 그 하위 클래스 타입의 참조변수면 어느 것이나 매개변수로 전달될 수 있고 이에 따라 매번 다른 타입의 참조변수를 매개 변수로 전할해야하는 번거로움을 줄일 수 있다. 위의 과정을 합친 전체 코드를 보고 다시 이해해보자.
package package2;
public class PolymorphismEx {
public static void main(String[] args) {
Customer customer = new Customer();
customer.buyCoffee(new Americano());
customer.buyCoffee(new CaffeLatte());
System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
}
}
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {
public Americano() {
super(4000); // 상위 클래스 Coffee의 생성자를 호출
}
public String toString() {return "아메리카노";}; //Object클래스 toString()메서드 오버라이딩
};
class CaffeLatte extends Coffee {
public CaffeLatte() {
super(5000);
}
public String toString() {return "카페라떼";};
};
class Customer {
int money = 50000;
void buyCoffee(Coffee coffee) {
if (money < coffee.price) { // 물건 가격보다 돈이 없는 경우
System.out.println("잔액이 부족합니다.");
return;
}
money = money - coffee.price; // 가진 돈 - 커피 가격
System.out.println(coffee + "를 구입했습니다.");
}
}
// 출력값
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.
앞서 설명한 내용은 제외하고 실제 객체를 생성하여 아메리카노와 카페라떼 한잔을 구입하는 코드만 살펴보자. 객체지향 설계의 다형성을 활용하여 buyCoffee() 메서드의 매개 변수로 Coffee 타입을 전달해주었다. 이제 객체를 생성하고 참조변수를 사용할 때 Coffee 클래스를 상속 받기만 하면 buyCoffee(Coffee coffee)메서드의 매개변수로 전달할 수 있다.
'공부 > 전자컴퓨터공학' 카테고리의 다른 글
Java 열거형(Enum) 이란? 자바 기초 배우기 (0) | 2024.05.28 |
---|---|
Java 추상화(Abstraction) 란? 자바 기초 배우기 (1) | 2024.05.28 |
Java 캡슐화란? 자바 기초 배우기 (0) | 2024.05.27 |
Java 상속(Inheritance) 이란? 자바 기초 배우기 (0) | 2024.05.27 |
Java 내부클래스 (Inner Class) Java 기초 배우기 (0) | 2024.05.27 |