다형성1. 다형성이란 무엇인가다형성은 자식 객체를 부모 타입으로도 다룰 수 있는 것이다.
쉽게말해
조상 타입의 참조변수로 자손 타입의 객체를 다룰 수 있다
라고 표현한다.
처음에는 이 문장이 어렵게 느껴질 수 있는데, 뜻은 단순하다.
예를 들어CaptionTv가Tv를 상속했다면,CaptionTv객체는
CaptionTv타입으로도 다룰 수 있고Tv타입으로도 다룰 수 있다즉, 실제로는 자식 객체인데, 부모 타입의 참조변수에 담아서 사용할 수 있다는 뜻이다.
여기서 꼭 같이 기억해야 하는 문장이 있다.
조상 타입의 참조변수로 자손 타입의 객체는 참조할 수 있지만,
반대로 자손 타입의 참조변수로 조상 타입의 객체는 참조할 수 없다.
왜 이런 차이가 생기냐면, 참조변수가 알고 있는 기능 범위와 실제 객체가 가진 기능 범위가 맞아야 하기 때문이다.
자식 객체는 부모 기능을 이미 전부 가지고 있고, 거기에 자기 기능을 더 가지고 있다.
그래서 자식 객체를 부모 타입으로 보면, 부모가 아는 기능만 사용하면 되기 때문에 문제가 없다.
반대로 부모 객체를 자식 타입으로 보게 하면 문제가 생긴다.
자식 타입의 참조변수는 부모 기능뿐 아니라 자식이 추가한 기능도 사용할 수 있다고 생각한다.
그런데 실제 객체가 부모 객체라면, 자식 기능은 아예 존재하지 않는다.
즉, 참조변수는 있다고 믿고 접근하는데 실제 객체에는 없는 기능이 되어 버리므로, 이런 방향은 허용되지 않는다.
2. 가장 먼저 이해해야 하는 그림다형성은 처음 보면 추상적으로 느껴질 수 있다.
그래서 가장 먼저Tv와CaptionTv관계로 보는 것이 이해하기 쉽다.
CaptionTv는Tv를 상속한 자식 클래스다.
그래서CaptionTv객체는 자식 타입으로도 볼 수 있고, 부모 타입으로도 볼 수 있다.
여기서 중요한 것은 실제 객체의 종류와 참조변수의 타입을 구분해서 보는 것이다.
객체가 바뀌는 것이 아니라, 어떤 타입의 참조변수로 바라보느냐에 따라 바로 사용할 수 있는 멤버의 범위가 달라진다.
핵심 특징
CaptionTv는Tv를 상속했기 때문에 부모의 멤버를 물려받는다.- 자식 객체는 부모 기능도 사용할 수 있고, 자신이 추가한 기능도 사용할 수 있다.
- 자식 객체를 부모 타입으로 참조하는 것은 가능하다.
- 하지만 부모 타입으로 참조하면 부모가 가진 멤버까지만 바로 사용할 수 있다.
- 반대로 부모 객체를 자식 타입으로 참조하는 것은 불가능하다.
예제:
PolyBasic1class Tv { boolean power; // 전원 상태 int channel; // 현재 채널 void power() {} // 전원 켜기/끄기 기능 void channelUp() {} // 채널 증가 void channelDown() {}// 채널 감소 } class CaptionTv extends Tv { boolean caption; // 자막 기능 추가 void displayCaption(String text) {} // 자막 출력 기능 추가 } public class PolyBasic1 { public static void main(String[] args) { CaptionTv c = new CaptionTv(); // 자식 타입으로 자식 객체를 참조 Tv t = new CaptionTv(); // 부모 타입으로 자식 객체를 참조 -> 다형성 c.channel = 10; // 가능: 부모에게서 물려받은 멤버 c.caption = true; // 가능: 자식이 추가한 멤버 t.channel = 10; // 가능: Tv에 있는 멤버는 사용 가능 // t.caption = true; // 에러: Tv 타입으로는 자식 멤버가 안 보임 // t.displayCaption("자막"); // 에러: Tv 타입으로는 자식 메서드가 안 보임 } } /* 주석 처리한 두 줄은 컴파일 에러 */이 코드는 같은
CaptionTv객체라도 어떤 타입의 참조변수로 잡느냐에 따라 사용 범위가 달라진다는 점을 보여준다.
CaptionTv c = new CaptionTv();는 자식 타입으로 자식 객체를 참조한 것이다.
그래서 부모에게서 물려받은channel도 사용할 수 있고, 자식이 추가한caption도 사용할 수 있다.
즉,c로는 아래 두 가지가 다 가능하다.
- 부모에게서 물려받은 멤버 사용
- 자식이 추가한 멤버 사용
반면
Tv t = new CaptionTv();는 실제로 만든 객체는CaptionTv지만, 참조변수 타입이Tv다.
그래서Tv에 선언된 멤버까지만 바로 사용할 수 있다.
예를 들면t로는 아래처럼Tv에 있는 멤버만 사용할 수 있다.
powerchannelpower()channelUp()channelDown()하지만 아래처럼
CaptionTv에만 있는 멤버는 바로 사용할 수 없다.
captiondisplayCaption()
즉, 실제 객체 안에는 자식이 추가한 기능까지 모두 들어 있다.
하지만 참조변수의 타입이 부모 타입이면 부모가 가진 범위까지만 바로 보인다.
그래서 다형성은 객체가 바뀌는 개념이 아니라,
하나의 자식 객체를 어떤 타입의 참조변수로 바라보느냐에 따라 바로 사용할 수 있는 멤버가 달라지는 개념이라고 이해하면 된다.
즉, 부모 타입으로 참조하면 부모 기능까지만 바로 보인다.
3. 부모 타입으로 받은 자식 객체는 형변환해서 다시 자식 기능을 사용할 수 있다
Tv t = new CaptionTv();처럼 부모 타입으로 자식 객체를 참조하면,
바로 사용할 수 있는 범위는Tv에 있는 멤버까지만 제한된다.
그래서caption이나displayCaption()같은 자식 멤버는 바로 사용할 수 없다.
하지만 여기서 중요한 점은 실제 객체가CaptionTv라는 사실은 그대로라는 것이다.
즉, 지금은 부모 타입으로 보고 있어서 자식 기능이 안 보일 뿐이지,
객체 자체가 부모 객체로 바뀐 것은 아니다.
그래서 필요하면 형변환해서 다시 자식 타입으로 바라볼 수 있다.예제:
PolyBasic2class Tv { boolean power; int channel; } class CaptionTv extends Tv { boolean caption; void displayCaption(String text) { if (caption) { System.out.println(text); } } } public class PolyBasic2 { public static void main(String[] args) { Tv t = new CaptionTv(); // 부모 타입으로 자식 객체를 참조 t.channel = 10; // 가능: Tv에 있는 멤버 CaptionTv c = (CaptionTv)t; // 자식 타입으로 형변환 c.caption = true; // 가능: 자식 멤버 접근 c.displayCaption("자막 출력"); // 가능: 자식 메서드 호출 } } /* 출력결과 자막 출력 */이 코드는 부모 타입으로 받았던 자식 객체를 다시 자식 타입으로 바꿔서 사용하는 흐름을 보여준다.
코드를 순서대로 보면 이렇다.
1.Tv t = new CaptionTv();
// 실제 객체는CaptionTv
// 하지만Tv타입으로 보고 있으므로 부모 멤버까지만 바로 사용 가능
2.CaptionTv c = (CaptionTv)t;
//t가 가리키는 객체를 다시CaptionTv타입으로 봄
// 객체가 바뀌는 것이 아니라, 보는 타입만 바뀜
3.c.caption = true;
// 이제 자식 타입으로 보고 있으므로 자식 멤버 사용 가능
4.c.displayCaption("자막 출력");
// 자식이 추가한 메서드도 호출 가능즉, 부모 타입으로 받아서 공통으로 처리하다가, 필요할 때 다시 자식 타입으로 형변환해서 자식 기능을 꺼내 쓸 수 있다고 이해하면 된다.
헷갈리기 쉬운 부분형변환은 아무 때나 할 수 있는 것이 아니다.
실제 객체가 정말 그 자식 객체일 때만 가능하다.
예를 들어 이 코드는 가능하다.Tv t = new CaptionTv(); CaptionTv c = (CaptionTv)t;
하지만 이 코드는 안 된다.
Tv t = new Tv(); CaptionTv c = (CaptionTv)t;왜냐하면 실제 객체가
Tv인데,CaptionTv라고 우겨서 보는 것이기 때문이다.
즉, 형변환은 “실제 자식 객체를 부모 타입으로 보고 있던 경우”에만 다시 되돌릴 수 있다.
4. 반대 방향은 왜 안 되는가다형성을 배우면 여기서 꼭 한 번 걸린다.
- 왜
Tv t = new CaptionTv();는 되는데,CaptionTv c = new Tv();는 안 될까?답은 참조변수가 기대하는 기능 범위가 다르기 때문이다.
예제:
PolyBasic3class Tv { boolean power; int channel; } class CaptionTv extends Tv { boolean caption; } public class PolyBasic2 { public static void main(String[] args) { Tv t = new Tv(); // 부모 객체 생성 // CaptionTv c = new Tv(); // 에러 } } /* 주석 처리한 줄은 컴파일 에러 */이 코드에서
CaptionTv c = new Tv();가 허용되면,c는 자식 타입 참조변수이므로caption같은 자식 멤버도 사용할 수 있다고 생각하게 된다.
그런데 실제 객체는 그냥Tv다.
즉,caption이라는 멤버는 아예 없다.
그래서 이런 식의 참조는 안전하지 않다.
즉, 참조변수가 알고 있는 기능 수보다 실제 객체가 가진 기능 수가 적으면 안 된다고 이해하면 된다.
헷갈리기 쉬운 부분많이 헷갈리는 부분은 이거다.
- “자식은 부모 기능을 다 쓰는데, 왜 부모는 자식 기능을 못 쓰는가?”
이건 단순히 부모-자식 관계 때문만이 아니라, 참조변수 타입이 무엇이냐가 더 중요하다.
CaptionTv타입으로 보면 부모 기능 + 자식 기능이 모두 보인다.Tv타입으로 보면Tv에 선언된 기능까지만 보인다.즉, 실제 객체가 자식이라고 해도, 부모 타입으로 보고 있는 순간 자식이 추가한 기능은 바로 보이지 않는다.
정리하면 이렇게 된다.
Tv t = new CaptionTv();→ 가능
→ 부모가 아는 범위로만 사용하면 되기 때문
CaptionTv c = new Tv();→ 불가능
→ 자식 기능까지 있다고 생각하고 접근하게 되기 때문
5. 매개변수의 다형성다형성은 단순히 부모 타입으로 자식 객체를 참조할 수 있다에서 끝나는 개념이 아니다.
진짜 중요한 이유는 서로 다른 객체들을 공통 부모 타입으로 묶어서, 같은 방식으로 처리할 수 있게 해 주기 때문이다.
예를 들어Tv,Computer,Audio는 서로 다른 물건이지만, 모두 상품이라는 공통 성격을 가진다.
그리고 구매할 때 필요한 정보도 비슷하다.
대표적으로 가격과 보너스 점수가 있다.
즉, 물건 종류는 달라도 구매할 때 하는 일은 거의 같다.
- 가격만큼 돈을 차감하고
- 보너스 점수를 누적한다
이 공통 처리를 한 번에 묶기 위해 다형성이 필요하다.
먼저 흐름부터 보기예를 들어 아래처럼 구매 기능을 만든다고 해 보자.
class Buyer { int money = 1000; int bonusPoint = 0; void buy(Tv t) { money -= t.price; bonusPoint += t.bonusPoint; } }여기서
Tv t는 매개변수다.
즉,buy()메서드를 호출할 때 전달받은 Tv 객체를 임시로 담아 두는 변수라고 보면 된다.
예를 들어 아래처럼 호출하면,Buyer b = new Buyer(); b.buy(new Tv());
new Tv()로 만든 객체가buy()안으로 들어오고,
그 객체를 메서드 안에서는t라는 이름으로 부르게 된다.
즉,b.buy(new Tv());이 호출이 일어나면,
메서드 안에서는 전달된Tv객체를t로 받아서 사용하는 것이다.
Tv만 받는 경우의 문제처음에는
Tv만 산다고 생각하면buy(Tv t)로 만들 수 있다.
하지만 나중에Computer도 사고,Audio도 살 수 있게 하려면 문제가 생긴다.
왜냐하면 지금 메서드는Tv객체만 받을 수 있기 때문이다.
그래서 이런 식으로 메서드를 계속 늘려야 한다.void buy(Tv t) { ... } void buy(Computer c) { ... } void buy(Audio a) { ... }그런데 이 메서드들이 하는 일은 거의 똑같다.
받는 객체 이름만 다르고, 실제 처리 방식은 같다.
이렇게 되면 비슷한 코드가 계속 중복된다.
공통 부모로 묶기그래서
Tv,Computer,Audio가 공통으로 가지는 부모 클래스를 만든다.class Product { int price; int bonusPoint; Product(int price) { this.price = price; bonusPoint = price / 10; } } class Tv extends Product { Tv() { super(100); } } class Computer extends Product { Computer() { super(200); } } class Audio extends Product { Audio() { super(50); } }이제
Tv,Computer,Audio는 서로 다른 객체이지만, 전부Product를 상속한 자식 클래스가 된다.
즉, 세 객체 모두 Product의 자식이라는 공통점이 생긴다.
buy(Product p)로 바꾸는 이유이제 구매 메서드를 아래처럼 바꿀 수 있다.
class Buyer { int money = 1000; int bonusPoint = 0; void buy(Product p) { money -= p.price; bonusPoint += p.bonusPoint; } }여기서
Product p도 매개변수다.
차이는 이제Tv만 받는 것이 아니라, Product를 상속한 모든 자식 객체를 받을 수 있다는 점이다.
즉, 아래 호출이 전부 가능해진다.Buyer b = new Buyer(); b.buy(new Tv()); b.buy(new Computer()); b.buy(new Audio());호출할 때 넘기는 객체는 각각 다르지만,
받는 쪽에서는 전부Product타입인p하나로 처리한다.
즉,
new Tv()를 넘기면 그 객체를p가 받는다.new Computer()를 넘기면 그 객체를p가 받는다.new Audio()를 넘기면 그 객체를p가 받는다.받은 객체의 실제 종류는 달라도,
공통 부모인Product가 가진price,bonusPoint를 사용할 수 있기 때문에 같은 방식으로 처리할 수 있다.
예제: 전체 흐름 보기이제 실제로는 어떤 클래스에서 객체를 만들고, 어떤 흐름으로
buy()가 호출되는지 전체 예제를 보면 이해가 더 쉽다.class Product { int price; int bonusPoint; Product(int price) { this.price = price; this.bonusPoint = price / 10; } } class Tv extends Product { Tv() { super(100); } } class Computer extends Product { Computer() { super(200); } } class Audio extends Product { Audio() { super(50); } } class Buyer { int money = 1000; int bonusPoint = 0; void buy(Product p) { money -= p.price; bonusPoint += p.bonusPoint; System.out.println(p.price + "원짜리 상품을 구매했습니다."); } } public class PolyTest { public static void main(String[] args) { Buyer b = new Buyer(); // 1. Buyer 객체를 생성하고 b가 참조함 b.buy(new Tv()); // 2. Tv 객체를 생성해서 buy()에 전달함 // 3. buy(Product p)에서 전달된 Tv 객체를 p가 받음 // 4. p.price, p.bonusPoint를 사용해서 구매 처리함 b.buy(new Computer()); // 5. Computer 객체를 생성해서 buy()에 전달함 // 6. buy(Product p)에서 전달된 Computer 객체를 p가 받음 b.buy(new Audio()); // 7. Audio 객체를 생성해서 buy()에 전달함 // 8. buy(Product p)에서 전달된 Audio 객체를 p가 받음 System.out.println("남은 돈은 " + b.money + "원입니다."); System.out.println("현재 보너스 점수는 " + b.bonusPoint + "점입니다."); // 출력결과 // 100원짜리 상품을 구매했습니다. // 200원짜리 상품을 구매했습니다. // 50원짜리 상품을 구매했습니다. // 남은 돈은 650원입니다. // 현재 보너스 점수는 35점입니다. } }이 예제에서 중요한 점은
Buyer b = new Buyer();,b.buy(new Tv());같은 코드가
따로 튀어나온 것이 아니라main()메서드 안에서 실제로 객체를 만들고 호출하는 실행 흐름이라는 점이다.
즉,
Buyer객체를 먼저 만들고Tv,Computer,Audio객체를 각각 생성해서buy(Product p)메서드에 전달하면- 메서드 안에서는 모두
Product타입 하나로 받아서 같은 방식으로 처리한다이렇게 보면 다형성이 왜 필요한지 더 자연스럽게 연결된다.
왜 다형성이 필요한가결국 다형성이 필요한 이유는
객체 종류마다 메서드를 따로 만들지 않고, 공통된 부분을 기준으로 하나로 묶어 처리하기 위해서다.
즉, 다형성은
- 공통 처리 구조를 만들기 쉽고
- 코드 중복을 줄일 수 있고
- 새로운 상품 종류가 늘어나도 기존 메서드를 크게 바꾸지 않아도 되는
더 유연한 구조를 만들게 해 준다.
그래서 다형성은 단순히 “부모 타입으로 자식을 참조한다”는 문법 개념이 아니라,
여러 자식 객체를 하나의 부모 타입으로 묶어서 같은 방식으로 처리하기 위한 핵심 개념이라고 이해하면 된다.
헷갈리기 쉬운 부분
buy(Product p)라고 해서Product객체만 받을 수 있는 것은 아니다.
Product를 상속한Tv,Computer,Audio객체도 모두 받을 수 있다.
또한p는 새로운 특별한 객체가 아니라,
호출할 때 전달된 자식 객체를 부모 타입으로 받아서 가리키는 변수일 뿐이다.
즉,p가 받는 것은Product 모양으로 보는 자식 객체라고 이해하면 된다.
6. 코드로 먼저 감 잡기이제 실제 코드로 보면 더 쉽게 보인다.
PolyTest는 하나의 매개변수로 여러 종류의 객체를 받을 수 있다는 점을 보여주는 가장 기본적인 예제다.
예제:
PolyTestpackage day7; import java.util.Date; public class PolyTest { public static void main(String[] args) { printInfo("가나다"); // String 객체 전달 printInfo(new Date()); // Date 객체 전달 printInfo("ABC"); // String 객체 전달 printInfo(new int[10]); // int 배열 객체 전달 printInfo(new A()); // 사용자 정의 객체 전달 printInfo(new java.io.File("/education")); // File 객체 전달 printInfo(Integer.valueOf(100)); // Integer 객체 전달 printInfo(new Object()); // Object 객체 전달 } static void printInfo(Object obj) { // 모든 객체를 받을 수 있게 Object로 선언 System.out.println( "obj변수에 전달된 객체의 클래스명: " + obj.getClass().getName() ); } } /* 출력결과 obj변수에 전달된 객체의 클래스명: java.lang.String obj변수에 전달된 객체의 클래스명: java.util.Date obj변수에 전달된 객체의 클래스명: java.lang.String obj변수에 전달된 객체의 클래스명: [I obj변수에 전달된 객체의 클래스명: day7.A obj변수에 전달된 객체의 클래스명: java.io.File obj변수에 전달된 객체의 클래스명: java.lang.Integer obj변수에 전달된 객체의 클래스명: java.lang.Object */이 코드에서 가장 중요한 줄은 이것이다.
static void printInfo(Object obj)매개변수 타입이
Object라서 문자열, 날짜, 배열, 파일, 일반 객체를 전부 받을 수 있다.
이게 가능한 이유는 모든 클래스의 최고 조상이Object이기 때문이다.
즉, 서로 다른 객체들을 부모 하나로 묶어서 받을 수 있는 것이다.
코드 흐름
1) 호출하는 쪽printInfo("가나다"); printInfo(new Date()); printInfo(new Object());여기서는 서로 다른 종류의 객체를 같은 메서드에 전달하고 있다.
즉, 객체 종류는 달라도 메서드 이름은 하나다.
2) 받는 쪽static void printInfo(Object obj)받는 쪽에서는 종류가 다른 객체들을 전부
Object타입 하나로 받는다.
즉, 여러 객체를 부모 하나로 묶어서 받는 구조다.
3) 확인하는 쪽System.out.println(obj.getClass().getName());이 코드는 “지금 실제로 들어온 객체가 무슨 클래스인지”를 출력한다.
그래서 같은printInfo()를 호출해도 결과가 서로 다르게 나온다.
여기서 출력 결과 중[I가 조금 낯설 수 있다.
배열은 클래스명이 조금 특이하게 출력되는데, 여기서[I는int배열이라는 뜻이다.
즉, 받는 변수 타입은 하나지만 실제 객체는 여러 종류일 수 있다는 점을 확인하는 코드다.
7. 검사 후 형변환까지 보기
PolyTest가 “여러 객체를 하나로 받을 수 있다”를 보여주는 코드라면,
PolyTest_1은 그다음 단계인 검사 후 형변환까지 보여준다.
예제:
PolyTest_1package day7; import java.util.Date; public class PolyTest_1 { public static void main(String[] args) { printInfo("가나다"); // String 객체 전달 printInfo("ABC"); // String 객체 전달 printInfo(new Date()); // Date 객체 전달 printInfo(new int[10]); // 배열 객체 전달 printInfo(new A()); // 사용자 정의 객체 전달 printInfo(new java.io.File("/education")); // File 객체 전달 printInfo(Integer.valueOf(100)); // Integer 객체 전달 printInfo(new Object()); // Object 객체 전달 } static void printInfo(Object obj) { // 일단 모두 Object로 받음 if (obj instanceof String) { // 실제 객체가 String인지 먼저 확인 System.out.println( "String 객체가 전달됨 -" + obj.toString() // Object에도 있는 공통 메서드 + "-" + ((String)obj).length() // String으로 형변환 후 length() 사용 ); } else { System.out.println( "obj변수에 전달된 객체의 클래스명: " + obj.getClass().getName() ); } } } /* 출력결과 String 객체가 전달됨 -가나다-3 String 객체가 전달됨 -ABC-3 obj변수에 전달된 객체의 클래스명: java.util.Date obj변수에 전달된 객체의 클래스명: [I obj변수에 전달된 객체의 클래스명: day7.A obj변수에 전달된 객체의 클래스명: java.io.File obj변수에 전달된 객체의 클래스명: java.lang.Integer obj변수에 전달된 객체의 클래스명: java.lang.Object */이 코드에서 중요한 상황은 이것이다.
printInfo("가나다");호출할 때 넘기는
"가나다"는 실제로String객체다.
그런데 메서드 안으로 들어오면 이 객체를 담는 변수obj의 타입은Object다.
즉, 지금 상태는 이렇게 된다.
- 실제 객체:
String- 참조변수 타입:
Object이 상태에서는
obj.length()를 바로 쓸 수 없다.
왜냐하면length()는String의 메서드이고,Object타입 변수로는Object에 있는 기능만 바로 쓸 수 있기 때문이다.
그래서 순서가 이렇게 간다.
1) 먼저 검사if (obj instanceof String)이건 “지금
obj안에 들어 있는 실제 객체가 정말String이냐”를 확인하는 것이다.
2) 맞으면 형변환(String)obj이제
Object로 보고 있던 값을String으로 다시 보겠다는 뜻이다.
객체가 바뀌는 게 아니라, 보는 타입을 바꾸는 것이다.
이 검사가 필요한 이유는, 실제 객체가 그 타입이 아닐 때 무작정 형변환하면 오류가 날 수 있기 때문이다.
그래서 먼저 확인하고, 그다음에 형변환한다.
3) 그다음 자식 기능 사용((String)obj).length()이제야
String전용 메서드인length()를 사용할 수 있다.
즉, 부모 타입으로 받은 뒤 필요하면 검사 후 형변환해서 자식 기능을 쓴다.
반복문 안에서는 이렇게 이어진다방금 본 예제는
obj하나를 검사한 뒤 형변환해서 사용하는 흐름이었다.
이 원리는 객체가 여러 개 들어 있는 배열이나 리스트를for문으로 처리할 때도 그대로 이어진다.
즉, 객체를 하나 다룰 때는
검사 → 형변환 → 그 타입의 기능 사용
순서로 갔다면,
여러 객체를 반복문으로 다룰 때도
각 원소를 하나씩 꺼내면서 같은 순서를 반복하게 된다.
예를 들어 부모 타입 배열 안에 여러 종류의 객체가 함께 들어 있으면,
반복문 안에서 꺼낸 값도 먼저는 부모 타입으로 보게 된다.
그래서 공통 기능은 바로 사용할 수 있지만,
특정 타입에만 있는 기능은 바로 사용할 수 없다.
이럴 때도 먼저instanceof로 현재 꺼낸 객체가 정말 그 타입인지 확인하고, 맞을 때만 형변환해서 그 타입 전용 기능을 사용한다.
즉,
객체 하나에서 하던 검사 후 형변환이 반복문 안으로 그대로 들어온 형태라고 이해하면 된다.예제:
PolyTest_2package day7; public class PolyTest_2 { public static void main(String[] args) { Object[] arr = {"java", 100, "HTML", true}; // 서로 다른 타입의 값을 Object 배열에 함께 저장함 for (int i = 0; i < arr.length; i++) { // 배열 원소를 하나씩 꺼내 확인함 if (arr[i] instanceof String) { // 현재 원소가 String인지 먼저 검사함 String str = (String) arr[i]; // String이 맞을 때만 형변환함 System.out.println(str + " : " + str.length()); // 형변환 후 String 전용 기능인 length()를 사용함 } } } } /* 출력결과 java : 4 HTML : 4 */이 코드에서는
Object[]배열 안에 문자열, 정수, 논리값이 함께 들어 있다.
즉, 여러 종류의 객체를 부모 타입 하나로 묶어서 저장한 상태다.
반복문이 돌면 배열 원소를 하나씩 꺼내는데,
꺼낼 때마다 바로length()를 호출할 수는 없다.
왜냐하면 배열의 타입이Object[]라서, 꺼낸 값도 먼저Object로 보기 때문이다.
그래서 먼저instanceof로 현재 원소가String인지 확인한다.
그리고String일 때만(String)으로 형변환한 뒤
length()를 호출한다.
즉, 반복문 안에서도 흐름은 똑같다.
- 먼저 검사하고
- 맞으면 형변환하고
- 그다음 그 타입의 기능을 사용한다
결국
instanceof는
여러 객체를 반복해서 처리하는 중에, 필요한 타입만 안전하게 골라내기 위해 사용하는 검사라고 이해하면 된다.
8. 여러 종류의 객체를 하나로 묶어서 다루기다형성의 진짜 장점은 여기서 더 잘 보인다.
메서드 하나로 받는 것에서 끝나는 게 아니라, 여러 자식 객체를 한곳에 모아서 저장하고 관리할 수도 있다.
아까 본Product예시를 조금 더 실제처럼 바꿔 보면 이해가 더 쉽다.
예제:
Product[]로 장바구니 만들기class Product { int price; // 제품 가격 int bonusPoint; // 제품 구매 시 적립되는 보너스 점수 Product(int price) { this.price = price; bonusPoint = price / 10; // 가격의 10%를 보너스 점수로 저장 } } class Tv extends Product { Tv() { super(300); // Tv 가격은 300 } public String toString() { return "Tv"; } } class Computer extends Product { Computer() { super(200); // Computer 가격은 200 } public String toString() { return "Computer"; } } class Audio extends Product { Audio() { super(100); // Audio 가격은 100 } public String toString() { return "Audio"; } } class Buyer { int money = 1000; int bonusPoint = 0; Product[] cart = new Product[3]; // Product 타입 배열 int index = 0; // 현재 저장할 위치 void buy(Product p) { // Tv, Computer, Audio를 모두 받을 수 있음 money -= p.price; bonusPoint += p.bonusPoint; cart[index++] = p; // 구입한 제품을 장바구니에 저장 System.out.println(p + "를 구입했습니다."); } } public class PolyProductTest { public static void main(String[] args) { Buyer b = new Buyer(); b.buy(new Tv()); b.buy(new Computer()); b.buy(new Audio()); } } /* 출력결과 Tv를 구입했습니다. Computer를 구입했습니다. Audio를 구입했습니다. */이 코드에서 먼저 봐야 할 것은 배열 선언 부분이다.
Product[] cart = new Product[3];이 뜻은
Product타입의 데이터를 3개 저장할 수 있는 배열을 만들겠다는 뜻이다.
그런데 여기서 중요한 점은Tv,Computer,Audio가 모두Product를 상속한 자식 클래스라는 것이다.
즉,
Tv도ProductComputer도ProductAudio도Product라는 관계가 성립한다.
그래서Product타입 배열에는Product객체만 담는 것이 아니라,
Product의 자식 객체들도 함께 담을 수 있다.
이제buy()메서드를 보자.void buy(Product p)이 메서드는 겉으로 보면
Product하나만 받는 것처럼 보인다.
하지만 실제로는Product를 상속한 자식 객체들도 받을 수 있다.
그래서 아래 코드가 모두 가능하다.b.buy(new Tv()); b.buy(new Computer()); b.buy(new Audio());이렇게 하면 장바구니 배열
cart안에는 순서대로Tv,Computer,Audio객체가 들어가게 된다.
배열 타입은Product[]하나지만, 실제로 담기는 객체의 종류는 서로 달라도 괜찮다.
또 이번 예제에서는price와bonusPoint도 같이 들어 있어서, 왜buy(Product p)가 유용한지 더 잘 보인다.money -= p.price; bonusPoint += p.bonusPoint;이 코드는
Tv,Computer,Audio각각에 대해 따로 작성한 것이 아니다.
공통 부모인Product에price,bonusPoint가 있기 때문에, 부모 타입 하나로 공통 처리할 수 있는 것이다.
즉, 다형성은 단순히
“부모 타입으로 자식 객체를 참조할 수 있다”에서 끝나는 개념이 아니라,
공통된 기준으로 여러 객체를 한 번에 처리하고 저장할 수 있게 해 주는 구조라고 볼 수 있다.
즉, 다형성은 메서드와 배열을 훨씬 깔끔하게 만들어 준다.
9. 헷갈리기 쉬운 부분
1) 객체와 참조변수 타입은 다를 수 있다다형성에서는 이 둘을 꼭 구분해야 한다.
- 실제 객체 =
new로 만든 것- 참조변수 타입 = 그 객체를 어떤 타입으로 보고 있는가
객체는 그대로인데, 참조변수 타입 때문에 보이는 기능 범위가 달라진다.
예를 들어Tv t = new CaptionTv(); Object obj = "가나다";이 두 문장은 같은 원리로 볼 수 있다.
- 첫 번째는
CaptionTv객체를Tv타입으로 받고 있는 것- 두 번째는
String객체를Object타입으로 받고 있는 것즉, 실제 객체는 자식일 수 있지만, 부모 타입으로 보고 있는 동안에는 부모가 가진 기능까지만 바로 사용할 수 있다.
2) 부모 타입으로 받았다고 해서 객체가 부모로 바뀌는 것은 아니다
Tv t = new CaptionTv();라고 해도 실제 객체는 여전히CaptionTv다.
다만Tv타입으로 보고 있기 때문에Tv에 있는 기능까지만 바로 사용할 수 있을 뿐이다.
즉, 조상 타입으로 받았다고 해서 객체가 조상으로 바뀌는 것은 아니다.
객체는 그대로이고, 참조변수 타입 때문에 보이는 범위만 달라지는 것이다.
3) 자식 기능을 쓰고 싶으면 형변환이 필요할 수 있다부모 타입으로 넓게 받은 뒤, 실제 객체가 자식임이 확인되면 형변환해서 자식 기능을 사용할 수 있다.
4) 형변환은 아무 때나 하는 것이 아니다무작정 형변환하면 안 된다.
실제 객체가 그 타입이 맞는지 먼저 확인해야 한다.
그래서instanceof로 검사한 뒤 형변환하는 흐름이 중요하다.
5) 다형성은 메서드와 배열에서 특히 강력하다
- 매개변수를 부모 타입으로 두면 여러 자식 객체를 같은 메서드로 처리할 수 있다.
- 부모 타입 배열을 만들면 여러 자식 객체를 같은 배열에 저장할 수 있다.
핵심만 다시 정리하면
다형성은 자식 객체를 부모 타입으로도 다룰 수 있는 것이다.- 책에서는 이것을 조상 타입의 참조변수로 자손 타입의 객체를 다룬다고 표현한다.
- 반대로 자손 타입의 참조변수로 조상 타입의 객체를 다루는 것은 불가능하다.
- 부모 타입으로 보면 부모가 가진 기능까지만 바로 사용할 수 있다.
- 실제 객체가 자식 객체라면, 필요할 때 검사 후 형변환해서 자식 기능을 사용할 수 있다.
- 다형성의 진짜 장점은 여러 객체를 공통 부모 하나로 묶어서 같은 방식으로 처리할 수 있다는 점이다.
- 그래서 메서드도 줄이고, 배열 저장도 통일하고, 처리 흐름도 훨씬 깔끔하게 만들 수 있다.