
하나의 객체를 여러 타입의 참조 변수로 다룰 수 있는 성질
즉, 서로 다른 여러 객체를 공통된 타입(상위 클래스/인터페이스) 으로 묶어 동일한 방식으로 처리할 수 있음
상속 관계에서는 부모(조상) 타입 참조 변수로 자식(자손) 객체를 참조할 수 있음
참조 타입이 바뀌어도 실제 객체는 그대로이며, 실행되는 동작(메서드)은 실제 객체 기준으로 결정(동적 바인딩)
참조 타입 = “무엇으로 보느냐”
실제 객체 = “진짜 누구냐”
실행 결과는 진짜 누구냐(실제 객체) 를 따른다.
예시)
Animal a = new Dog(); // Dog 객체를 Animal 타입으로 참조
a.sound(); // 호출은 Dog의 sound()가 실행됨
유연성
상위 클래스 또는 인터페이스 타입 하나로 다양한 구현 객체를 처리할 수 있어, 객체를 쉽게 교체할 수 있다.
재사용성
공통 로직은 상위 클래스(또는 공통 인터페이스 기반의 공통 처리 코드)에 모으고, 각 클래스는 필요한 부분만 구현해 중복을 줄일 수 있다.
확장성
새로운 클래스(새 구현체)를 추가해도, 기존 코드는 상위 타입(인터페이스)에만 의존하므로 클라이언트 코드 변경 없이 확장이 가능하다.
유지보수성
변경이 생겼을 때 영향 범위를 줄일 수 있다.
구현이 바뀌어도 인터페이스(규격)는 유지하면 되고, 필요하면 구현체만 수정/교체하면 되므로 수정 범위가 작아진다.
| 참조 타입 변수 | 인스턴스 생성 | O / X | 이유 |
|---|---|---|---|
Payment p | new CardPay() | O | CardPay는 Payment를 구현 → 상위 타입으로 참조 가능(업캐스팅) |
Payment p | new KakaoPay() | O | KakaoPay는 Payment를 구현 → 상위 타입으로 참조 가능(업캐스팅) |
CardPay c | new CardPay() | O | 같은 타입 객체 생성 |
CardPay c | new KakaoPay() | X | 서로 다른 구현체(형제 관계) → 상속/구현 관계가 없어 참조 불가 |
Object o | new CardPay() | O | 모든 클래스는 Object를 상속 → Object로 참조 가능 |
실제 인터페이스 기반으로 객체를 생성할 때, JVM 메모리(힙 영역)에서 실제로 일아나는 일은 다음과 같다.
1) new KakaoPay()는 힙에 객체 1개를 만듬
new KakaoPay()
실행되면 Heap에 KakaoPay 인스턴스(객체) 1개가 생성된다.
이 표현식의 결과는 “객체 자체”가 아니라, 힙에 있는 객체를 가리키는 참조값(주소) 이다.
즉, 힙에는 KakaoPay 객체가 생기고, 그 주소를 다른 변수가 들고 있게 된다.
2) 아래 세 줄은 힙 객체 3개를 만듬
object obj = new KakaoPay() ;
Payment p = new KakaoPay() ;
Kakaopay kp = new KakaoPay() ;
3) 같은 힙 객체 1개를 여러 타입으로 참조하려면 아래와 같음
KakaoPay real = new KakaoPay(); // Heap: 객체 1개
Object obj = real; // Stack: obj는 그 주소를 들고 있음
Payment p = real; // Stack: p도 같은 주소
KakaoPay kp = real; // Stack: kp도 같은 주소
4) 참조 타입이 다르면 '보이는 범위'가 달라짐
(1) Object obj
obj.toString(); // O (Object에 있는 메서드)
(2) Payment p
p.pay(1000); // O (Payment 규격에 있는 메서드)
(3) KakaoPay kp
kp.pay(1000); // O
kp.kakaoOnly(); // O (KakaoPay 전용 메서드가 있다면)
예시)
KakaoPay implements Payment
CardPay implements Payment
그리고 모든 클래스는 Object를 상속
Object
↳ KakaoPay (그리고 Payment를 구현)
↳ CardPay (그리고 Payment를 구현)
(1) 업캐스팅 : 자손 -> 조상
KakaoPay kp = new KakaoPay();
Payment p = kp; // 업캐스팅 (생략 가능)
Object obj = kp; // 업캐스팅 (생략 가능)
p.pay(10000); // O (Payment에 있음)
p.kakaoOnly(); // X (Payment에는 없음)
(2) 다운캐스팅 : 조상 -> 자손
명시적 형 변환 필요: (KakaoPay) p
항상 가능한 게 아님 (진짜 객체가 KakaoPay일 때만 가능)
잘못된 예시 1)
Payment p = new CardPay(); // 실제 객체는 CardPay
KakaoPay kp = (KakaoPay) p; // X (런타임 ClassCastException)
p의 “타입”은 Payment지만 p가 가리키는 “실제 객체”가 CardPay라서 KakaoPay로 못바꿈
instanceof는 '참조 변수가 가리키는 실제 객체가 어떤 타입인지' 확인할 때 쓴다.
-> 결과를 boolean으로 반환
-> true가 반환이 되면 해당 타입으로 형 변환 가능
Payment p = new KakaoPay();
p instanceof Payment // true
p instanceof KakaoPay // true
p instanceof CardPay // false
p instanceof Object // true
사용 이유 : 다운캐스팅은 위험할 수 있어서
Payment p = new CardPay();
// 아래는 위험: 실제 객체가 CardPay인데 KakaoPay로 바꾸려 하면 런타임 에러
KakaoPay kp = (KakaoPay) p; // ClassCastException
사용 예시
Payment p = new KakaoPay();
if (p instanceof KakaoPay) {
KakaoPay kp = (KakaoPay) p;
kp.kakaoOnly(); // KakaoPay 전용 기능 사용
}
cf) Java 16+ 패턴 매칭
Payment p = new KakaoPay();
if (p instanceof KakaoPay kp) {
kp.kakaoOnly();
}
객체 지향 프로그래밍에서 특성 중 하나인 '다형성'에 대한 설명