IT 개발노트
다형성이란? 본문
1. 다형성
1.1 다형성이란?
- 객체지향개념에서 다형성이란 '여러가지 형태를 가질 수 있는 능력'을 의미 하며, 자바에서는 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도 록 함으로써 다형성을 프로그램적으로 구현하였다.
* 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하였다는 것이다.
- 참조변수와 인스턴스가 다른 타입을 지닌 경우 참조변수의 타입에 따라 사 용 할 수 있는 멤버의 개수가 달라진다.
- 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스의 멤버 개수보다 같거 나 적어야 한다. (참조변수가 조상이어야 한다.)
- 클래스는 상속을 통해서 확장될 수는 있어도 축소될 수는 없어서, 조상 인스 턴스의 멤버 개수는 자손 인스턴스의 멤버 개수보다 항상 적거나 같다.
- 조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 있다.
- 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다.
1.2 참조변수의 형변환
- 자손타입의 참조변수를 조상타입의 참조변수로, 조상타입의 참조변수를 자 손타입의 참조변수로 형변환만 가능하다.
-> 형변환만 가능할 뿐, 자손 = 조상( ); 과 같은 정의를 할 경우 동일하게 에 러가 발생한다.
- 작은 자료형에서 큰 자료형의 형변환은 생략이 가능하듯이, 참조형 변수의 형변환에서는 자손타입의 참조변수를 조상타입으로 형변환하는 경우에는 형변환을 생략할 수 있다.
-> Child = (Child)Super( );의 경우 형변환 생략 불가능
Super = Child( );의 경우 형변환 생략 가능
- 조상과 자식끼리만 형변환이 가능하다. 자식끼리 같은 조상을 가졌더라도 그 둘은 형변환이 불가능하다.
- Car(Child)타입의 참조변수 c를 Car타입의 조상인 Object(Super)타입의 참조 변수로 형변환 하는 것은 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스 턴스가 갖고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않는 다. 그래서 형변환을 생략할 수 있도록 한 것이다.
- Child = Super( );의 경우, 실제 인스턴스의 멤버 개수보다 참조변수가 사용 할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능성이 있다.
(다른 메모리를 침범할 수 있다.)
- 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아 니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다.
단지, 참조변수의 형변환을 통해서 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것 뿐이다.
- 자식 = (자식)조상( );의 경우 형변환은 가능하지만 실행 시 에러가 나게 된다.
* 서로 상속관계에 있는 타입간의 형변환은 양방향으로 자유롭게 수행될 수 있으나, 참조변수가 가리키는 인스턴스의 자손타입으로 형변환은 허용되지 않는다. 그래서 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.
1.3 instanceof 연산자
- 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 instanceof 연산자를 사용한다.
- instranceof의 왼쪽에는 참조변수를 오른쪽에는 타입(클래스명)이 피연산자 로 위치한다.
- instanceof를 이용한 연산결과로 true를 얻었다는 것은 참조변수가 검사한 타입으로 형변환이 가능하다는 것을 뜻한다.
- 조상타입의 참조변수로는 실제 인스턴스의 멤버들을 모두 사용할 수 없기 때문에, 실제 인스턴스와 같은 타입의 참조변수로 형변환을 해야만 인스턴 스의 모든 멤버들을 사용할 수 있다.
- 실제 인스턴스와 같은 타입의 instanceof 연산 이외에 조상타입의 instanceof연산에도 true를 결과로 얻으며, instanceof 연산의 결과가 true라는 것은 검사 한 타입으로 형변환을 해도 아무 문제가 없다는 뜻이다. 즉, 조상으로 instanceof 연산을 해도 true가 나온다.
- 어떤 타입에 대한 instanceof 연산의 결과가 true라는 것은 검사한 타입으로 의 형변환이 가능하다는 것을 말한다.
1.4 참조변수와 인스턴스의 연결
- 메서드의 경우 조상 클래스의 메서드를 자손 클래스에서 오버라이딩한 경우 에도 참조변수의 타입에 관계없이 항상 실제 인스턴스의 메서드(오버라이딩 된 메서드)가 호출되지만, 멤버변수의 경우 참조변수의 타입에 따라 달라진 다.
- 즉, Super = Child( );의 경우 만일 오버라이딩된 멤버변수가 있다면 Super의 것을 쓰고, 오버라이딩된 메서드가 있다면 그것은 Child의 것을 쓴다.
- 오버라이딩된 변수나 메서드가 없을 경우엔 상관 없다.
- 메서드 : 실제 인스턴스에 따라 사용
- 멤버변수 : 참조변수의 타입에 따라 사용
* static메서드는 static변수처럼 참조변수의 타입에 영향을 받는다. 참조변수 의 타입에 영향을 받지 않는 것은 인스턴스 메서드 뿐이다.
- 멤버벼누가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입 의 참조변수를 사용 했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손 타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다. 하지만 중복 정의가 되지 않은 경우, 조상타입의 참조변수를 사용 했을 때와 자손타입의 참조변수를 사용했을 때의 차이는 없다. 중복된 경우 는 참조변수의 타입에 따라 달라지지만, 중복되지 않은 경우 하나뿐이므로 선택의 여지가 없기 때문이다.
예시 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | package sungju.Java.BindingTest; public class BindingTest { public static void main(String[] args) { Parent p = new Child(); Child c = new Child(); System.out.println("p.x =" + p.x); p.method(); System.out.println("c.x +" + c.x); c.method(); } } class Parent { int x = 100; void method() { System.out.println("Parent Method"); } } class Child extends Parent { int x = 200; void method () { System.out.println("Child Method"); } } |
* 타입은 다르지만, 참조변수 p,c 모두 Child인스턴스를 참조하고 있다.
- Parent클래스와 Child클래스는 서로 같은 멤버들을 정의하고 있다.
-> 조상타입의 참조변수 p로 Child인스턴스의 멤버들을 사용하는 것과 자손 타입의 참조변수 c로 Child인스턴스의 멤버들을 사용하는 것의 차이를 알 수 있다.
- 메서드인 method( )의 경우 참조변수의 타입에 관계없이 항상 실제 인스턴 스의 타입인 Child클래스에 정의된 메서드가 호출되지만, 인스턴스변수인 x 는 참조변수의 타입에 따라서 달라진다.
예시 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | package sungju.Java.BindingTest; public class BindingTest { public static void main(String[] args) { Parent p = new Child(); Child c = new Child(); System.out.println("px =" + p.x); p.method(); System.out.println(); System.out.println("cx =" + c.x); c.method (); } } class Parent { int x = 100; void method() { System.out.println("Parent Method"); } } class Child exteds Parent { int x = 200; void method() { System.out.println("x =" + x); //this.x와 같다. System.out.println("super.x =" + super.x); System.out.println("this.x =" + this.x); } } |
- 자손클래스 Child에 선언된 인스턴스변수 x와 조상 클래스 Parent로부터 상 속받은 인스턴스변수 x를 구분하는데 참조변수 super와 this가 사용된다.
- 자손인 Child클래스에서의 super.x는 조상 클래스인 Parent에 선언된 인스턴 스 변수 x를 뜻하며, this.x 또는 x는 Child클래스의 인스턴스변수 x를 뜻한다.
- 멤버변수들은 주로 private로 접근을 제한하고, 외부에서는 메서드를 통해서 만 멤버변수에 접근할 수 있도록 하지, 이번 예시에서처럼 다른 외부 클래스 에서 참조변수를 통해 직접적으로 인스턴스변수에 접근할 수 있게 하지 않 는다.
- 인스턴스변수에 직접 접근하면, 참조변수의 타입에 따라 사용되는 인스턴스 변수가 달라질 수 있으므로 주의해야 한다.
1.5 매개변수의 다형성
- 참조변수의 다형적인 특징은 매개변수에도 적용된다.
예제 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | package sungju.Java.PolyArgumentTest; public class PolyArgumentTest { public static void main(String[] args) { Buyer b = new Buyer(); b.buy(new Tv()); b.buy(new Computer()); System.out.println("현재 남은 돈은 " + b.money + "만원입니다."); System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다."); } } class Product { int price; // 제품의 가격 int bonusPoint; // 제품구매 시 제공하는 보너스 점수 Product(int price) { this.price = price; bonusPoint = (int)(price/10.0); //보너스점수는 제품가격의 10% } } class Tv extends Product { Tv() { // 조상클래스의 생성자 Product(int price)를 호출한다. super(100); // Tv의 가격을 100만원으로 한다. } // Object클래스의 toString()을 오버라이딩한다. public String toString() { return "Tv"; } } class Computer extends Product { Computer() { super(200); } public String toString() { return "Computer"; } } class Buyer { // 고객, 물건을 사는 사람 int money = 1000; // 소유금액 int bonusPoint = 0; // 보너스점수 void buy(Product p) { if(money < p.price) { System.out.println("잔액이 부족하여 물건을 살 수 없습니다."); return; } money = money - p.price; // 가진 돈에서 구입한 제품의 가격을 뺀다. bonusPoint = bonusPoint + p.bonusPoint; // 제품의 보너스 점수를 추가한다. System.out.println(p + "을/를 구입하셨습니다."); } } |
* 다형성이 적용되어 메서드의 매개변수가 부모 타입인 경우, 상속받은 자식 타입의 참조변수도 받을 수 있다는 것을 확인할 수 있다.
1.6 여러 종류의 객체를 배열로 다루기
- 부모 타입의 참조변수 배열을 사용하여 같은 부모를 가진 서로 다른 객체를 묶어서 배열로 다룰 수 있다.
예제 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | package sungju.Java.PolyArgumentTest; public class PolyArgumentTest { public static void main(String[] args) { Buyer b = new Buyer(); b.buy(new Tv()); b.buy(new Computer()); b.buy(new Audio()); b.summary(); System.out.println("현재 남은 돈은 " + b.money + "만원입니다."); System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다."); } } class Product { int price; // 제품의 가격 int bonusPoint; // 제품구매 시 제공하는 보너스 점수 Product(int price) { this.price = price; bonusPoint = (int)(price/10.0); //보너스점수는 제품가격의 10% } Product() { // 기본 생성자 } } class Tv extends Product { Tv() { // 조상클래스의 생성자 Product(int price)를 호출한다. super(100); // Tv의 가격을 100만원으로 한다. } // Object클래스의 toString()을 오버라이딩한다. public String toString() { return "Tv"; } } class Computer extends Product { Computer() { super(200); } public String toString() { return "Computer"; } } class Audio extends Product { Audio() { super(200); } public String toString() { return "Audio"; } } class Buyer { // 고객, 물건을 사는 사람 int money = 1000; // 소유금액 int bonusPoint = 0; // 보너스점수 Product[] item = new Product[10]; // 구입한 제품을 저장하기 위한 배열 int i = 0; void buy(Product p) { if(money < p.price) { System.out.println("잔액이 부족하여 물건을 살 수 없습니다."); return; } money = money - p.price; // 가진 돈에서 구입한 제품의 가격을 뺀다. bonusPoint = bonusPoint + p.bonusPoint; // 제품의 보너스 점수를 추가한다. item[i++] = p; System.out.println(p + "을/를 구입하셨습니다."); } void summary() { // 구매한 물품에 대한 정보를 요약해서 보여준다. int sum = 0; // 구입한 물품의 가격합계 String itemList =""; // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다. for(int i=0; i<item.length; i++) { if(item[i]==null) break; sum = sum + item[i].price; itemList = itemList + item[i] + ", "; } System.out.println("구입하신 물품의 총금액은 " + sum + "만원입니다."); System.out.println("구입하신 제품은 " + itemList + "입니다."); } } |
위 예제에서 Product배열로 구입한 제품들을 저장할 수 있도록 했지만, 배열의 크기를 10으로 했기 때문에 11개 이상의 제품을 구입할 수 없는 것이 문제다.
이런 경우, Vector클래스를 사용하면 된다.
Vector클래스는 내부적으로 Object타입의 배열을 가지고 있어서, 이 배열에 객체를 추가하거나 제거할 수 있게 작성되어 있다. 그리고 배열의 크기를 알아서 관리해주기 때문에 저장할 인스턴스의 개수에 신경 쓰지 않아도 된다.
* Vertor클래스는 단지 동적으로 크기가 관리되는 객체배열이다.
메서드 / 생성자 |
설명 |
Vector() |
10개의 객체를 저장할 수 있는 Vertor인스턴스를 생성한다. |
boolean add(Object o) |
Vector에 객체를 추가한다. |
boolean remove(Object o) |
Vector에 저장되어 있는 객체를 제거한다. |
boolean isEmpty( ) |
Vector가 비어있는지 검사한다. |
Object get(int index) |
지정된 위치(index)의 객체를 반환한다. |
int size( ) |
Vector에 저장된 객체의 개수를 반환한다. |
예제 3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | package sungju.Java.PolyArgumentTest; import java.util.Vector; public class PolyArgumentTest { public static void main(String[] args) { Buyer b = new Buyer(); Tv tv = new Tv(); Computer com = new Computer(); Audio audio = new Audio(); b.buy(tv); b.buy(com); b.buy(audio); b.summary(); System.out.println(); b.refund(com); b.summary(); System.out.println("현재 남은 돈은 " + b.money + "만원입니다."); System.out.println("현재 보너스점수는 " + b.bonusPoint + "점입니다."); } } class Product { int price; // 제품의 가격 int bonusPoint; // 제품구매 시 제공하는 보너스 점수 Product(int price) { this.price = price; bonusPoint = (int)(price/10.0); //보너스점수는 제품가격의 10% } Product() { // 기본 생성자 price = 0; bonusPoint = 0; } } class Tv extends Product { Tv() { // 조상클래스의 생성자 Product(int price)를 호출한다. super(100); // Tv의 가격을 100만원으로 한다. } // Object클래스의 toString()을 오버라이딩한다. public String toString() { return "Tv"; } } class Computer extends Product { Computer() { super(200); } public String toString() { return "Computer"; } } class Audio extends Product { Audio() { super(50); } public String toString() { return "Audio"; } } class Buyer { // 고객, 물건을 사는 사람 int money = 1000; // 소유금액 int bonusPoint = 0; // 보너스점수 Vector item = new Vector(); // 구입한 제품을 저장하는데 사용될 Vector객체 void buy(Product p) { if(money < p.price) { System.out.println("잔액이 부족하여 물건을 살 수 없습니다."); return; } money = money - p.price; // 가진 돈에서 구입한 제품의 가격을 뺀다. bonusPoint = bonusPoint + p.bonusPoint; // 제품의 보너스 점수를 추가한다. item.add(p); // 구입한 제품을 Vector에 저장한다. System.out.println(p + "을/를 구입하셨습니다."); } void refund(Product p) { // 구입한 제품을 환불한다. if(item.remove(p)) { // 제품을 Vector에서 제거한다. money = money + p.price; bonusPoint = bonusPoint - p.bonusPoint; System.out.println(p + "을/를 반품하셨습니다."); } else { // 제거에 실패한 경우 System.out.println("구입하신 제품 중 해당 제품이 없습니다."); } } void summary() { // 구매한 물품에 대한 정보를 요약해서 보여준다. int sum = 0; // 구입한 물품의 가격합계 String itemList = ""; // 구입한 물품목록 if(item.isEmpty()) { // Vector가 비어있는지 확인한다. System.out.println("구입하신 제품이 없습니다."); return; } // 반복문을 이용해서 구입한 물품의 총 가격과 목록을 만든다. for(int i=0; i<item.size(); i++) { Product p = (Product)item.get(i); // Vector의 i번째에 있는 객체를 얻어온다. sum = sum + p.price; itemList += (i==0) ? "" + p : ", " + p; } System.out.println("구입하신 물품의 총금액은 " + sum + "만원입니다."); System.out.println("구입하신 제품은 " + itemList + "입니다."); } } |
* 문자열과 참조변수의 덧셈(결합연산)은 참조변수에 toString( )을 호출해서 문자열을 얻어 결합한다.
-> 위 예제에서 나오는 ""+ p는 "" + p.toString( )이 된다.
p.toString( )의 결과가 "Audio"라면 ""+"Audio"가 되어 결국 "Audio"가 된다.