객체지향 개념에서 다형성이란, "여러 가지 형태를 가질 수 있는 능력"을 의미함.
Java
에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 허용하여 다형성을 구현함.
class Parent {
int parentMemVar;
void parentMethod() { ... }
}
class Child extends Parent {
int childMemVar;
void childMethod() { ... }
}
Child
클래스는 Parent
클래스를 상속 받았으므로 Parent
의 모든 멤버 변수와 메서드를 가짐.
Parent p = new Parent();
Child c = new Child();
위와 같이 인스턴스 변수와 메서드를 사용하기 위해 그동안은 참조변수의 타입과 인스턴스의 타입을 동일하게 맞추어 생성했었는데,
Parent p = new Child(); // 부모 타입의 참조변수에 자식 타입 인스턴스를 참조시킴
다형성의 원리를 통해 이렇게도 쓸 수 있음.
Child
가 Parent
보다 더 확장된 개념이기 때문에 가능. (반대의 경우는 사용 불가)
Parent p = new Child();
Child c = new Child();
대신, 이 둘은 분명한 차이점이 존재.
참조변수 p
: Parent
클래스 타입이기 때문에, Child
클래스에만 있고 Parent
클래스에는 없는 변수나 메서드는 사용할 수 없음
참조변수 c
: Child
클래스 타입이기 때문에, Child
클래스에 있는 모든 변수와 메서드 사용 가능
→ 인스턴스의 타입은 같아도 참조변수의 타입에 따라 사용 가능한 멤버의 범위가 달라짐
서로 상속 관계에 있는 클래스 사이에서는 참조변수를 형변환 시킬 수 있음 (조상 타입 ↔ 자손 타입)
Up-casting하는 경우에는 형변환 생략 가능 (기본형 변수의 형변환 시, "작은 자료형 → 큰 자료형"은 형변환 생략이 가능한 것과 유사)
Up-casting: 자손 타입 → 조상 타입
Down-casting: 조상 타입 → 자손 타입
캐스트 연산자인 괄호()
를 사용해 변환하고자 하는 타입을 안에 적으면 됨
Parent p = null;
Child c = new Child();
Child c2 = null;
p = c; // (Parent)c -> Up-casting. 생략 가능
c2 = (Child)p; // Down-casting. 생략 불가
개념 파트에서 보았던 예시도 원래는 이렇게 형변환을 해준 것인데 생략된 것.
Parent p = (Parent)new Child();
// 또는
Child c = new Child();
Parent p = (Parent)c;
형변환은 참조변수의 타입을 바꿔줄 뿐, 인스턴스 자체에는 아무런 영향을 끼치지 않음.
→ 형변환을 통해서는 단지 사용가능한 멤버의 범위를 줄였다 늘렸다 할 수 있는 것
참조변수가 가리키는 인스턴스의 실제 타입을 확인하는 용도
varName instanceof ClassName
처럼 사용하고, 반환값은 boolean
(true
또는 false
)인 점을 활용하여 조건문의 조건식에 주로 사용함.
parent instanceof Parent // true
child instanceof Parent // true
parent instanceof Child // false
child instanceof Child // true
결과가
true
이면 검사한 클래스의 타입으로 형변환이 가능하다는 의미
void someMethod(Parent p) {
if (p instanceof Child1) {
Child1 c1 = (Child1) p; // Down-casting 이므로 생략 불가
c1.childOneMethod();
}
if (p instanceof Child2) {
Child2 c2 = (Child2) p;
c2.childTwoMethod();
}
}
참조변수 p
에는 Parent
타입이 오거나, Child1
또는 Child2
타입이 올 수 있음
Parent
타입이 온다면, 아무 조건문도 만족하지 못할 것이고,
Child1
타입이 온다면 첫번째 조건문을, Child2
타입이 온다면 두번째 조건문을 만족하게 될 것임.
Parent p = new Child();
Child c = new Child();
위에서 이 둘의 차이점에 대해서 짚었는데, 한가지 더 차이가 있음
만약 조상 클래스와 자손 클래스에 동일한 이름의 인스턴스 메서드가 있는 경우, 자손의 것으로 오버라이딩되어 항상 현재 참조하고 있는 인스턴스 (예시 기준으로 Child
인스턴스)의 메서드가 나오는데 비해,
→ 멤버 변수는 참조변수의 타입에 따라 다른 결과를 가져옴.
class BindingTest {
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();
System.out.printf("p.x = %d%n", p.x); // 출력: 10
System.out.printf("p.x = %d%n", c.x); // 출력: 20
p.method(); // 출력: Child의 메서드가 실행되었습니다.
c.method(); // 출력: Child의 메서드가 실행되었습니다.
}
}
class Parent {
int x = 10;
void method() {
System.out.println("Parent의 메서드가 실행되었습니다.");
}
}
class Child extends Parent {
int x = 20; // 부모의 멤버변수와 이름이 겹침
void method() {
System.out.println("Child의 메서드가 실행되었습니다.");
}
}
이래서 더더욱 외부 클래스에서 참조변수를 통한 인스턴스 변수 직접 접근을 막아야 함. (참조변수 타입에 따라 상이한 결과가 나옴 = 실수의 여지)
멤버 변수들을
private
으로 지정해서 직접 접근을 막고, 값을 읽어오는 메서드를 정의하여 메서드를 통해서만 접근할 수 있게 해야함.
매개변수로 참조변수(인스턴스)를 받아올 때, 매개 변수의 타입을 조상 클래스의 타입으로 놓으면 자손 클래스 타입의 참조변수 어느 것이든 받아올 수 있음
class Test {
public static void main(String[] args) {
Buyer b = new Buyer();
b.buy(new Tv());
b.buy(new Computer());
b.buy(new Audio());
}
}
class Product {
int price;
int bonusPoint;
Product (int price) {
this.price = price;
this.bonusPoint = (int)(price / 10.0);
}
}
class Tv extends Product {
Tv() {
super(100);
}
}
class Computer extends Product {
Computer() {
super(200);
}
}
class Audio extends Product {
Audio() {
super(300);
}
}
class Buyer {
int money = 1000;
int bonusPoint = 0;
Buyer() {
System.out.printf("money: %d%nbonusPoint: %d%n", money, bonusPoint);
}
void buy(Product p) {
money -= p.price;
bonusPoint += p.bonusPoint;
System.out.printf("money: %d%nbonusPoint: %d%n", money, bonusPoint);
}
}
모든 제품의 조상인 Product
클래스가 있고, 자손으로는 Tv
, Computer
, Audio
클래스가 있음
Buyer
클래스는 구매자로서, buy
메서드를 사용해 제품을 구매함.
이 때 buy
메서드의 매개변수로 특정 제품 인스턴스를 가리키는 참조변수를 넣어야하는데, 만약 매개변수의 타입을 Tv
, Computer
, Audio
타입 중 하나로 설정해두었다면 그 제품밖에 구매할 수 없으므로, buyTv
, buyComputer
, buyAudio
와 같이 개별 메서드를 만들어줘야함.
그러나 매개변수 타입을 조상 클래스인 Product
타입으로 설정해두면, 모든 자손 클래스 타입을 받을 수 있게 됨.
매개변수의 다형성의 또 다른 예시로 print
메서드가 있음 (아래는 실제 System.out.print()
의 일부)
public void print(Object obj) {
write(String.valueOf(obj));
}
위에서 본 원리대로, 매개변수 타입을 모든 클래스의 조상인 Object
클래스 타입으로 설정해서 모든 객체를 다 받을 수 있게됨.
다형성의 원리에 따라 다음과 같이 쓸 수 있음
Product p1 = new Tv();
Product p2 = new Computer();
Product p3 = new Audio();
참조변수들이 모두 같은 타입이 되었으므로 배열로 묶을 수 있음
Product[] p = new Product[]{p1, p2, p3};
// 또는
Product[] p = {p1, p2, p3};
이렇게 다형성을 이용해 공통 조상을 가진 객체들을 배열로 묶어서 보다 효율적인 데이터 관리가 가능해짐.