09_다형성(Polymorphism)
다형성: 많은 모양을 가지고 있는 특징 (== "상속관계 간의" 형 변환)
다형성은 전제 조건으로 상속이 깔려야 함! (중요)
*객체지향프로그래밍(Object Oriented Programming, OOP) 3대 요소
=> 캡슐화, 상속, 다형성
*다형성
같은 자료형에 여러 가지 객체를 대입하여 다양한 결과를 얻어내는 성질
=> "상속" 관계에서만 이루어질 수 있는 "형 변환"의 개념!
명심할 사항: 대입연산자 (=) 기준으로 왼쪽과 오른쪽의 자료형은 같아야 함
Parent 클래스
package com.kh.chap01.poly.part01.basic.model.vo;
public class Parent {
// 필드부
private int x;
private int y;
// 생성자부
public Parent() {
}
public Parent(int x, int y) {
this.x = x;
this.y = y;
}
// 메소드부
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public void printParent() {
System.out.println("나 부모야");
}
}
Child1 클래스
package com.kh.chap01.poly.part01.basic.model.vo;
public class Child1 extends Parent {
// 필드부
private int z;
// 생성자부
public Child1() {
}
public Child1(int x, int y, int z) {
super(x, y);
this.z = z;
}
// 메소드부
public int getZ() {
return z;
}
public void setZ(int z) {
this.z = z;
}
public void printChild1() {
System.out.println("나 첫 번째 자식이야");
}
}
Child2 클래스
package com.kh.chap01.poly.part01.basic.model.vo;
public class Child2 extends Parent {
// 필드부
private int n;
// 생성자부
public Child2() {
}
public Child2(int x, int y, int n) {
super(x, y);
this.n = n;
}
// 메소드부
public int getN() {
return n;
}
public void setN(int n) {
this.n = n;
}
public void printChild() {
System.out.println("나 두 번째 자식이야");
}
}
PolyRun 클래스
package com.kh.chap01.poly.part01.basic.run;
import com.kh.chap01.poly.part01.basic.model.vo.Child1;
import com.kh.chap01.poly.part01.basic.model.vo.Parent;
public class PolyRun {
public static void main(String[] args) {
// 1. 부모 타입의 참조변수(주소값이 들어가는 stack영역의 상자)로 부모 객체(heap 영역의 알맹이)를 다루는 경우
System.out.println("1. 부모 타입의 참조변수로 부모 객체를 다루는 경우");
Parent p1 = new Parent();
// Parent 주소갑 타입 = heap 영역 실물 타입
p1.printParent();
// p1.printChild1();
// => p1 참조 변수로 Parent 객체에만 접근 가능
// 출력물
1. 부모 타입의 참조변수로 부모 객체를 다루는 경우
나 부모야
// 2. 자식 타입의 참조변수로 자식 객체를 다루는 경우
System.out.println("2. 자식 타입의 참조변수로 자식 객체를 다루는 경우");
Child1 c1 = new Child1();
// Child1 주소값 타입 = heap 영역 실물 타입
c1.printChild1();
c1.printParent();
// => c1 참조변수로 Child1, Parent 둘 다 접근 가능
// 출력물
2. 자식 타입의 참조변수로 자식 객체를 다루는 경우
나 첫 번째 자식이야
나 부모야
// 3. 부모 타입 참조변수로 자식 객체를 다루는 경우(다형성이 적용된 개념)
System.out.println("3. 부모 타입 참조변수로 자식 객체를 다루는 경우(다형성이 적용된 개념)");
Parent p2 = /* (Parent) */ new Child1(); // 자동 형변환으로 자식 객체가 부모 타입 참조변수에 담김
// 대입 연산자 기준으로 양쪽의 자료형이 다름에도 불구하고 오류 발생하지 않음
// Child1 타입의 객체가 Parent형으로 "자동 형변환"이 되고 있음
// => "상속" 구조에서는 클래스간의 형 변환이 가능함
p2.printParent(); // p2 참조변수로 Parent에만 접근 가능
// (Child1)p2.printChild1(); 로 진행할 경우 우선 순위 때문에 여전히 오류가 남!
((Child1)p2).printChild1(); // 단, Child1형으로 강제 형변환 한 후 Child1에 접근 가능
// 자식이 부모 되는 건 자동 형변환, 부모가 자식 되려면 강제 형변환
// 출력물
3. 부모 타입 참조변수로 자식 객체를 다루는 경우(다형성이 적용된 개념)
나 부모야
나 첫 번째 자식이야
"상속" 구조에서 클래스 간의 "형 변환"이 가능함
1. UpCasting: 자식타입 => 부모타입(자동 형변환, 형변환연산자 생략 가능) 자부자
2. DownCasting: 부모타입 => 자식타입(강제 형변환, 형변환연산자 명시적으로 작성해야 함) 부자강
다형성을 배워야 하는 이유에 대한 예시
// Child1 객체 2개, Child2 객체 2개가 필요한 상황이라고 가정
Child1[] arr1 = new Child1[2];
arr1[0] = new Child1(1, 2, 4);
arr1[1] = new Child1(2, 1, 5);
Child2[] arr2 = new Child2[2];
arr2[0] = new Child2(5, 7, 2);
arr2[1] = new Child2(2, 3, 5);
// 단, 다형성이 적용되면 부모 타입의 참조변수로
// 다양한 자식객체를 받아줄 수 있음
System.out.println("===== 다형성을 적용한 객체 배열 =====");
Parent[] arr = new Parent[4]; // 자식 객체에 대한 배열 선언 없이 가능해짐!
arr[0] = new Child1(1, 2, 4);
arr[1] = new Child2(5, 7, 2);
arr[2] = new Child1(2, 1, 5);
arr[3] = new Child2(2, 3, 5);
// 부모 타입에 담길 때는 자식 타입이 자동 형변환 된 꼴 => UpCasting
// arr[0].printParent();
((Child1)arr[0]).printChild1();
((Child2)arr[1]).printChild2();
((Child1)arr[2]).printChild1();
((Child2)arr[3]).printChild2();
// 자식타입객체로써 사용하고자 할 경우에는 강제형변환으로 원산 복구 후에 사용 가능 => DownCasting
// ((Child1)arr[3]).printChild1();
// => Child2 타입인 arr[3]을 Child1으로 형 변환 하려고 해서 오류가 발생함!
// ClassCastException: 클래스간 형 변환이 잘못되었을 경우 발생하는 에러
System.out.println("===== 반복문 이용해서 해 보기 =====");
for(int i = 0; i < arr.length; i++) {
// 각 인덱스별로 실제로 참조하고 있는 자식 클래스로 형 변환 후에 메소드 호출
// (강제형변환)arr[i].필요한메소드();
// 1. instancefo 연산자를 쓰는 방법
if(arr[i] instanceof Child1) {
((Child1)arr[i]).printChild1();
}
else {
((Child2)arr[i]).printChild2();
}
}
// 출력물
===== 반복문 이용해서 해 보기 =====
나 첫 번째 자식이야
나 두 번째 자식이야
나 첫 번째 자식이야
나 두 번째 자식이야
*instanceof 연산자
현재 참조변수가 실제로 어떤 자식 클래스를 참고하고 있는지 확인할 때 사용
[ 표현법 ]
참조변수명 instanceof 검사할클래스명
부모객체명 instanceof 부모클래스명 == true
자식객체명 instanceof 부모클래스명 == true (상속을 받았으니 부모님 거도 내 거)
부모객체명 instanceof 자식클래스명 == false (부모객체는 자식객체의 내용물에 접근 불가)
자식객체면 instanceof 자식클래스명 == true
두 번째 방법(instanceof 연산자를 쓰기 싫을 경우)
Parent 클래스
// instanceof 연산자를 사용하기 싫을 경우 (오버라이딩 활용)
public void print() {
System.out.println("나 부모야");
}
Child1 클래스
@Override
public void print() {
System.out.println("나 첫 번째 자식이야");
}
Child2 클래스
@Override
public void print() {
System.out.println("나 두 번째 자식이야");
}
PolyRun 클래스
System.out.println("===== 반복문 이용해서 해 보기 =====");
for(int i = 0; i < arr.length; i++) {
arr[i].print();
// print 메소드는 "오버라이딩" 된 상태
// => 오버라이딩 시 "자식 메소드"가 우선권을 가져 호출됨
}
// 출력물
===== 반복문 이용해서 해 보기 =====
나 첫 번째 자식이야
나 두 번째 자식이야
나 첫 번째 자식이야
나 두 번째 자식이야
단, 오버라이딩을 이용하면 굳이 형변환을 안 해도 됨
=> 알아서 자식객체의 메소드로 찾아가서 호출되는 꼴이기 때문에
ElectronicController1 클래스 (다형성 적용 전)
package com.kh.chap01.poly.part02.electronic.controller;
import com.kh.chap01.poly.part02.electronic.mode.vo.Desktop;
import com.kh.chap01.poly.part02.electronic.mode.vo.NoteBook;
import com.kh.chap01.poly.part02.electronic.mode.vo.Tablet;
// 다형성을 적용시키기 전
public class ElectronicController1 {
// 용산전자상가에 새로 차린 가게
// 필드부: 전자제품을 진열할 수 있는 빈 상자 종류별로 한 개씩 선언
private Desktop desk;
private NoteBook note;
private Tablet tab;
// 메소드부: 해당 상자에 물건을 넣는 용도의 메소드들, 상자에 담긴 물건을 꺼내서 보여 주는 메소드들
// 물건을 넣는(필드에 값을 대입하는) 용도의 메소드들
// setter 메소드의 원리 적용
// Desktop을 넣는 메소드
public void insert(Desktop d) { // insert: 입력하다, 넣다
// Desktop d = new Desktop("삼성", "데스크탑", 1200000, "Geforce 1070");
desk = d;
}
// NoteBook을 넣는 메소드
public void insert(NoteBook n) { // 오버로딩 적용 가능
note = n;
}
// Tablet을 넣는 메소드
public void insert(Tablet t) {
tab = t;
}
// 상자에 담긴 물건을 꺼내서 보여 주는 (필드에 담긴 값을 리턴) 용도의 메소드
// getter 메소드의 원리 적용
// Desktop 객체를 꺼내서 보여 주는 메소드
public Desktop selectDesktop() { // select: 조회하다
return desk;
}
// NoteBook 객체를 꺼내서 보여 주는 메소드: 이번 경우에는 오버로딩 적용 불가
public NoteBook selectNoteBook() { // select: 조회하다
return note;
}
// Tablet 객체를 꺼내서 보여 주는 메소드
public Tablet selectTablet() { // select: 조회하다
return tab;
}
}
ElectronicRun 클래스
package com.kh.chap01.poly.part02.electronic.run;
import com.kh.chap01.poly.part02.electronic.controller.ElectronicController1;
import com.kh.chap01.poly.part02.electronic.mode.vo.Desktop;
import com.kh.chap01.poly.part02.electronic.mode.vo.NoteBook;
import com.kh.chap01.poly.part02.electronic.mode.vo.Tablet;
public class ElectronicRun {
public static void main(String[] args) {
// 1. 다형성을 적용하기 전(ElectronicController1)
ElectronicController1 ec = new ElectronicController1();
// 이 시점 기준에서 ec.desk == null, ec.note == null, ec.tab == null
// => 우리 가게에 빈 상자가 세 개 생긴 꼴
// ec.desk = new Desktop("삼성", "데탑", 1200000, "Geforce 1070");
// => is not visible: private 접근 제한자이므로 직접 접근 불가함
// => desk라는 상자는 private이라 직접 접근 불가하므로 우회해서 간접 접근 해야 함
/* 이렇게 담아도 되고
Desktop d = new Desktop("삼성", "데탑", 1200000, "Geforce 1070");
ec.insert(d);
*/
// 이렇게 담아도 됨!
ec.insert(new Desktop("삼성", "데탑", 1200000, "Geforce 1070"));
ec.insert(new NoteBook("엘지", "그램", 2000000, 4));
ec.insert(new Tablet("애플", "아이패드", 500000, false));
// => 여기까지 물걸을 각각 납품받아서 상자에 담아 준 꼴
// 손님에게 제품등을 상자로부터 꺼내서 보여 주기
Desktop d = ec.selectDesktop(); // 안에서 바깥으로 꺼냄
NoteBook n = ec.selectNoteBook();
Tablet t = ec.selectTablet();
System.out.println(d); // 꺼내 온 상자 보여 주기
System.out.println(n); // 원래는 toString이 주소값을 호출하는 메소드이지만
System.out.println(t); // 우리는 오버라이딩을 통해 toString을 내용 표출로 바꿨으므로 그렇게 나옴
}
}
// 출력물
brand: 삼성, name: 데탑, price: 1200000, graphic: Geforce 1070
brand: 엘지, name: 그램, price: 2000000, usbPort: 4
brand: 애플, name: 아이패드, price: 500000, penFlag: false
// 출력물 잘 나오긴 하지만 만들 상자가 아주 많아요...
ElectronicController2 클래스 (다형성 적용 후)
package com.kh.chap01.poly.part02.electronic.controller;
import com.kh.chap01.poly.part02.electronic.mode.vo.Electronic;
// 다형성을 적용시켰을 때
public class ElectronicController2 {
// 용산전자상가에 다시 새롭게 차린 가게
// 필드부
//어느 물건이든지 넣을 수 있는 3칸짜리 창고
private Electronic[] elec = new Electronic[3];
// 메소드부
// 창고에 물건을 넣는 메소드: 뭐를 어디에 넣을 건지?
public void insert(Electronic any, int index) { // 어느 전자제품이 들어오든지, 창고에 넣을 건데
elec[index] = any; // 그 들어오는 전자제품은 창고에 순서대로 넣을 거야
}
// 창고로부터 물건을 하나 꺼내오는 메소드
public Electronic select(int index) { // 전자제품의 순서를 조회할 건데
return elec[index]; // 내가 원하는 창고 순서에 있는 애를 꺼내 와 줘
}
// 창고에 있는 물건 모두를 다 보여 주는 메소드
public Electronic[] selct() { // 전자제품 조회할 거야
return elec; // 전자제품 다 들고와~
}
}
ElectronicRun 클래스
// 2.다형성을 적용했을 경우 (ElectonicController2)
ElectronicController2 ec2 = new ElectronicController2();
ec2.insert(new Desktop("삼성", "데탑", 1200000, "Geforce 1070"), 0);
ec2.insert(new NoteBook("엘지", "그램", 2000000, 4), 1);
ec2.insert(new Tablet("애플", "아이패드", 500000, false), 2);
/*
// 창고에 담긴 물건들을 하나씩 뽑아 보기
Desktop d = (Desktop)(ec2.select(0)); // 부모 클래스가 자식 클래스로 되기 위해서는 강제 형변환 해야 함
NoteBook n = (NoteBook)(ec2.select(1));
Tablet t = (Tablet)(ec2.select(2));
System.out.println(d);
System.out.println(n);
System.out.println(t);
*/
// 반복문 활용해서 물건 꺼내오기
Electronic[] elec = ec2.select(); // 창고 전체를 꺼내 옴
for(int i = 0; i < elec.length; i++) {
System.out.println(elec[i] /* .toString() */);
// 오버라이딩 했기 때문에 굳이 형 변환 할 필요 없음! 오버라이딩 한 자식 클래스가 우선권 가지니까
}
상속 관계에 있다면 다형성을 통해 자료형이 달라도 같은 배열에 넣을 수 있음!
*다형성을 사용하는 이유
1. 부모 타입의 객체 배열로 다양한 자식 객체들을 모아서 담아둘 수 있음
2. 메소드에 매개 변수나 반환형에 다형성을 적용하게 되면 메소드 개수가 줄어듦