다형성이란?
o 객체지향개념에서 다형성이란 ‘여러 가지 형태를 가질 수 있는 능력’을 의미하며, 한 타입의 참조변수로 여러 타입의 객체를 참조할 수 있도록 한다. 구체적으로는 조상클래스 타입의 참조변수로 자손클래스의 인스턴스를 참조할 수 있도록 하는 것이다.
o 지금까지 생성된 인스턴스를 다루기 위해서는 인스턴스 타입과 일치하는 참조변수만을 사용했다. 그러나 서로 상속 관계에 있는 클래스의 경우 조상 클래스 타입의 참조변수로 자손 클래스의 인스턴스를 참조하도록 하는 것도 가능하다.
객체 배열의 다형성 |
매개 변수의 다형성 |
||
|
|
- 이처럼 다형성을 이용하여 조상클래스 타입의 참조변수로 자손클래스 타입의 인스턴스를 참조하거나 조상클래스 타입의 매개변수로 자손클래스 타입의 매개변수를 받을 수도 있다.
package JavaProject;
class Tv{
boolean power;
int channel;
void power() {power=!power; }
void channnelUp() {++channel;}
void channnelDown() {--channel;}
}
class CaptionTv extends Tv{
String text; //자막을 보여주기 위한 문자열
void caption() {/*생략*/}
}
public class Test {
public static void main(String[] args) {
CaptionTv c = new CaptionTv();
Tv t = new CaptionTv(); //조상타입의 참조변수로 자손 인스턴스를 참조
c.power();
c.channnelUp();
c.caption();
c.text = "자막";
t.power();
t.channnelUp();
// CaptionTv인스턴스를 생성했지만
// 참조 변수형이 Tv타입이기에 Tv클래스에 정의된 것만 사용 가능
// t.caption(); //에러
// t.text = "자막"; //에러
}
}
|
- 참조변수 t의 인스턴스가 CaptionTv일지만 참조변수 t로는 CaptionTv 인스턴스의 모든 멤버를 사용할 수 없다.
- Tv타입의 참조변수(t)로는 CaptionTv인스턴스 중 Tv클래스 멤버만 사용할 수 있다. (상속받은 멤버가 있다면 포함)
- 둘 다 같은 타입의 인스턴스지만 참조변수의 타입에 따라 사용할 수 있는 멤버의 개수가 달라진다.
o 조상 타입의 참조 변수를 사용하여 자손 클래스의 인스턴스를 참조할 경우 자손 클래스의 모든 멤버를 사용할 수는 없고 조상 클래스에 정의된 멤버만 사용이 가능하다.
o 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조하는 것은 불가능하다. 조상타입의 인스턴스 멤버 개수보다 자손 타입의 인스턴스 멤버 개수가 더 많다. 이 때문에 존재하지 않는 멤버를 사용하고자 할 가능성이 있으므로 허용하지 않는다.
CaptionTv c1 = new Tv(); //에러
|
- 실제 인스턴스인 Tv의 멤버 개수보다 참조변수 c가 사용할 수 있는 멤버 개수가 더 많기에 에러가 발생한다.
- CaptionTv 클래스에는 text, caption( )이 정의되어 있기에 c1이 참초하는 인스턴스에서 text, caption( )을 참조하려고 할 수 있다. 그러나 c1이 참조하고 있는 인스턴스 Tv타입에는 해당 멤버가 존재하지 않기에 문제가 발생할 수 있다.
o 참조변수가 사용할 수 있는 멤버의 개수는 인스턴스 멤버 개수보다 같거나 적어야 한다. 즉, 참조변수 타입이 참조변수가 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 개수를 결정한다는 것이다.
조상타입의 참조변수로 자손타입의 인스턴스를 참조할 수 없다. 반대로 자손타입의 참조변수로 조상타입의 인스턴스를 참조할 수는 없다. |
※클래스는 상속을 통해 확장될 수는 있어도 축소될 수는 없어서, 조상 인스턴스의 멤버 개수는 자손 인스턴스의 멤버 개수보다 항상 적거나 같다.
※모든 참조변수는 null 또는 4byte의 주소 값이 저장되며, 참조변수의 타입은 참조할 수 있는 객체의 종류와 사용할 수 있는 멤버의 수를 결정한다.
참조변수의 형변환
o 서로 상속관계에 있는 클래스 사이에서는 자손타입 -> 조상타입 참조변수, 조상타입> 자손타입 참조변수로 형변환이 가능하다.
o 바로 위 조상이나 자손뿐만 아니라 조상의 조상도 형변환이 가능하기에 모든 참조변수는 모든 클래스 조상인 Object클래스 타입으로 형변환이 가능하다.
o 참조형 변수의 형변환에서는 자손타입의 참조변수를 조상타입으로 형변환 하는 경우에는 형변환을 생략할 수 있다.
업 캐스팅 (Up-csating) - 자손 타입의 참조형 변수를 조상 타입의 참조형 변수로 형변환 하는 것 - 조상 타입의 참조형 변수로 모든 자식 타입의 인스턴스 주소를 받을 수 있다. - 자손 타입 -> 조상타입 (형변환 생략가능) - 단, 자손 타입의 인스턴스 주소를 받더라도 원래 조상타입에 있는 멤버 변수만 사용이 가능하다.
다운 캐스팅 (Down-casting) - 조상 타입의 참조형 변수를 자손 타입의 참조형 변수로 형변환 하는 것 - 조상 타입 -> 자손타입 (형변환 생략불가)
- 클래스간 형변환은 반드시 상속관계에 있는 클래스끼리만 가능하다. |
package JavaProject;
public class Test {
public static void main(String[] args) {
FireEngine f;
Ambulance a;
// a = (Ambulance)f; //에러, 상속관계가 아닌 클래스간의 형변환 불가
// f = (FireEngine)a; //에러, 상속관계가 아닌 클래스간의 형변환 불가
Car car =null;
FireEngine fe = new FireEngine();
FireEngine fe2 = null;
fe.water();
car = fe; // car = (Car)fe;에서 형변환 생략, 업 캐스팅
// car.water(); //에러, Car타입의 참조변수로는 water()호출 불가
fe2 = (FireEngine)car; // 형변환 생략불가, 다운 캐스팅
fe2.water();
}
}
class Car{
String color;
int door;
void drive() {
System.out.println("drive, Brrrrr~");
}
void stop() {
System.out.println("Stop!!!");
}
}
class FireEngine extends Car {
void water() {
System.out.println("water!!");
}
}
class Ambulance extends Car {
void siren() {
System.out.println("siren~~~");
}
}
|
- 각각의 클래스가 같은 조상클래스라고 하더라도 서로 상속관계가 아니면 형변환이 불가능하다.
- 자손타입의 참조변수를 조상타입의 참조변수에 할당할 경우 형변환을 생략할 수 있기에 car = fe;와 같이 하였다.
- 조상타입의 참조변수를 자손타입의 참조변수에 저장할 경우 형변환을 생략할 수 없으므로 fe2 = (FireEngine)car;와 같이 명시적 형변환을 해주어야 한다.
o 자손타입의 참조변수를 조상타입의 참조변수로 형변환 하는 것은 조상타입의 참조변수가 다룰 수 있는 멤버의 개수가 실제 인스턴스(자손타입)가 가지고 있는 멤버의 개수보다 적을 것이 분명하므로 문제가 되지 않기에 형변환을 생략할 수 있도록 한 것이다.
o 그러나 조상타입의 참조변수를 자손타입의 참조변수로 형변환 하는 것은 조상타입의 참조변수가 다룰 수 있는 멤버의 개수를 늘이는 것이다. 실제 인스턴스(조상타입) 멤버 개수보다 참조변수(자손타입)가 사용할 수 있는 멤버의 개수가 더 많아지므로 문제가 발생할 가능성이 있다. 그래서 형변환을 생략할 수 없으며, 형변환을 수행하기 전에 instanceof연산자를 사용해서 참조변수가 참조하고 있는 실제 인스턴스 타입을 확인하는 것이 안전하다.
o 위 예제의 동작 과정은 다음과 같다.
1. Car car = null;
Car타입의 참조변수 car를 선언하고 null로 초기화한다.
2. FireEngine fe = new FireEngine();
FireEngine인스턴스를 생성하고 FireEngine타입의 참조변수가 참조하도록 한다.
3. car = fe; // 조상타입 <- 자손타입
참조변수 fe가 참조하고 있는 인스턴스를 참조변수 car가 참조하도록 한다. 이때 두 참조변수 타입이 다르므로 참조변수 fe가 형변환 되어야 하지만 생략되었다. 참조변수 car을 통해서도 FireEngine인스턴스를 사용할 수 있지만, fe와 달리 car는 Car타입이므로 Car클래스의 멤버가 아닌 water()는 사용할 수 없다.
4. fe2 = (FireEngine)car; // 자손 타입 <- 조상타입
참조변수 car가 참조하고 있는 인스턴스를 참조변수 fe2가 참조하도록 한다. 이때 두 참조변수 타입이 다르므로 참조변수 car를 형변환하였다. 이제 참조변수 fe2를 통해서도 FireEngine인스턴스를 사용할 수 있지만, car와는 달리 fe2는 FireEngine타입이므로 FireEngine인스턴스의 모든 멤버들을 사용할 수 있다.
o 형변환은 참조변수의 타입을 변환하는 것이지 인스턴스를 변환하는 것은 아니기 때문에 참조변수의 형변환은 인스턴스에 아무런 영향을 미치지 않는다. 단지 참조변수의 형변환을 통해서, 참조하고 있는 인스턴스에서 사용할 수 있는 멤버의 범위(개수)를 조절하는 것뿐이다.
o 따라서 참조변수 형변환시 참조하는 인스턴스는 자손클래스 일 경우에만 가능하며 조상클래스의 인스턴스를 자손 클래스 타입의 참조변수로 참조할 수 없다. 자손클래스 타입의 참조변수가 부모 클래스 인스턴스의 가지고 있지 않은 멤버를 호출할 수 있기 때문이다.
Animal[] animals = new Animal[4];
animals[0]=new Dog();
animals[1]=new Cat();
animals[2]=new Rabbit();
animals[3]=new Mouse();
// (Dog)animals[0].bark(); //에러
((Dog)animals[0]).bark();
((Cat)animals[1]).bark();
((Rabbit)animals[2]).bark();
((Mouse)animals[3]).bark();
|
- 형변환을 하여 자손타입의 멤버를 사용할 경우에 [(형변환)조상 참조변수]에 괄호를 하나 더 해서 형변환 후 자손 타입 멤버를 참조할 수 있도록 해야한다.
- 괄호를 사용하지 않고 (Dog)animals[0].bark()로 입력할 경우 직접 접근 연산자 .에 의해 animals[0].bark()가 먼저 수행되고 (Dog)형변환이 나중에 수행되기 때문에 오류가 발생한다.
public class Test {
public static void main(String[] args) {
Car car = new Car(); //Car car = new FireEngine(); 변경 시 에러 X
Car car2 = null;
FireEngine fe = null;
car.drive();
fe=(FireEngine)car; //컴파일은 OK, 실행 시 에러 발생
fe.drive();
car2=fe;
car2.drive();
}
}
|
- 컴파일은 성공하지만 실행 시 fe=(FireEngine)car; 부분에서 에러가 발생한다. 조상타입의 참조변수를 자손타입의 참조변수로 형변환 한 것이라 문제가 없어 보이지만, 문제는 car이 참조하고 있는 인스턴스가 Car타입의 인스턴스인 것이다.
- 조상타입의 참조변수로 자손타입의 인스턴스 참조는 허용이 되지만 자손타입의 참조변수로 조상타입의 인스턴스 참조는 허용이 되지 않기 때문이다.
- 컴파일 시 참조변수 간의 타입만 체크하고 인스턴스 타입은 알지 못한다. 그렇기에 컴파일은 문제가 없었지만 실행 시 에러가 발생한 것이다.
Animal ansimals2 = new Dog();
Dog dog = new Dog();
// Dog dog2 = new Animal();
// Dog dog2 = (Dog)new Animal(); // 컴파일 O, 실행 시 에러 발생
// 에러, 자손타입의 참조 변수로 조상 타입의 인스턴스를 참조할 수 없다.
|
- 서로 상속관계에 있는 타입 간의 형변환은 양방향으로 자유롭게 수행될 수 있다.
- (조상)참조변수가 가리키는 (조상)인스턴스의 자손타입으로 형변환은 허용되지 않기에 참조변수가 가리키는 인스턴스의 타입이 무엇인지 확인하는 것이 중요하다.
instanceof 연산자
o instanceof 연산자는 참조변수가 참조하고 있는 인스턴스의 실제 타입을 알아보기 위해 사용한다.
o instanceof의 왼쪽에는 참조변수를 오른쪽에는 인스턴스 타입(클래스명)이 피연산자로 위치한다. 결과값은 boolean으로 참조변수가 검사한 타입으로 형변환이 가능하다면 true를 반환하기에 주로 조건문에서 사용한다.
void dowork(Car c) {
if (c instanceof FireEngine) {
FireEngine fe = (FireEngine)c;
fe.water();
} else if (c instanceof Ambulance) {
Ambulance a = (Ambulance)c;
a.siren();
}
}
|
- 위 메서드가 호출 시 Car타입의 참조변수 c를 매개변수로 받는다. 그래서 Car클래스 또는 그 자손 클래스의 인스턴스를 넘겨 받지만 메서드 내에서는 어떤 인스턴스 인지 알 수 없다.
(조상 타입의 참조변수로 자손 타입의 인스턴스를 참조할 수 있기 때문에 참조변수 타입과 인스턴스 타입이 항상 일치하지는 않는다.)
- 그래서 instanceof 연산자를 이용하여 참조변수 c가 가리키고 있는 인스턴스 타입을 체크하고, 형변환한 다음에 작업을 해야한다.
instanceof 미사용 |
instanceof 사용 |
||
|
|
참조변수와 인스턴스의 연결 (동적 바인딩)
o 바인딩이란 실제 실행할 메소드 코드와 호출하는 코드를 연결시키는 것으로 프로그램이 실행되기 전 컴파일이 되면서 모든 메서드는 정적으로 바인딩이 된다. 컴파일 시 정적으로 바인딩 된 메서드를 실행할 당시의 객체 타입으로 기준으로 바인딩 되는 것을 동적 바인딩이라고 한다. 동적 바인딩의 성립 요건으로 상속관계로 이루어져 다형성이 적용된 경우, 메서드 오버라이딩이 되어 있으면 정적으로 바인딩 된 메서드 코드보다 오버라이딩 된 메서드 코드를 우선적으로 수행한다.
o 조상타입의 참조변수와 자손타입의 참조변수의 차이점이 사용할 수 있는 멤버의 개수라고 했으나 한 가지가 더 있다. 멤버변수가 조상 클래스와 자손 클래스에 중복으로 정의된 경우, 조상타입의 참조변수를 사용했을 때는 조상 클래스에 선언된 멤버변수가 사용되고, 자손타입의 참조변수를 사용했을 때는 자손 클래스에 선언된 멤버변수가 사용된다.
o 메서드의 경우 조상 클래스의 메서드를 자손 클래스에서 오버라이딩한 경우에는 참조 변수의 타입에 관계없이, 항상 실제 인스턴스 메서드가(오버라이딩된 메서드)가 호출되지만, 멤버변수의 경우 참조변수 타입에 따라 달라지는 것이다.
package JavaProject;
public class Test {
public static void main(String[] args) {
Parent p = new Child();
Child c = new Child();
// 중복된 멤버 변수는 참조변수 타입에 따라 사용하는 곳이 다르다.
System.out.println("p.x = " + p.x); // Parent타입, Parent 클래스의 변수 x실행
p.method(); // 오버라이딩 된 메소드 실행
System.out.println();
System.out.println("c.x = " + c.x); // Child타입, Child 클래스의 변수 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");
System.out.println("x="+x); //this.x와 같다
System.out.println("super.x="+super.x);
System.out.println("this.x="+this.x);
}
}
|
- 참조변수 p와 c는 타입은 다르지만 모두 Child인스턴스를 참조하고 있다. 그리고 Parent클래스와 Child 클래스는 서로 같은 이름의 멤버 변수를 정의하고 있다.
- 조상클래스 타입인 참조변수 p로 멤버 변수를 호출했을 때는 조상 클래스의 멤버변수를, 자손클래스 타입인 참조변수 c로 멤버 변수를 호출했을 때는 자손 클래스의 멤버변수를 사용하는 것을 볼 수 있다.
- 메서드인 method( )의 경우는 오버라이딩 되었지만 참조변수 타입에 관계없이 항상 실제 인스턴스 타입인 Child클래스에서 호출된다.
|
조상클래스 참조변수 (자손 인스턴스 참조) |
자손클래스 참조변수 (자손 인스턴스 참조) |
멤버 변수 호출 |
조상 멤버변수 |
자손 멤버변수 |
(오버라이딩)메서드 호출 |
자손 메서드 |
자손 메서드 |
'언어 > 자바의 정석' 카테고리의 다른 글
[자바의 정석] Chapter 7 객체 지향 프로그래밍II (7) - 추상클래스(abstract class) (0) | 2021.02.16 |
---|---|
[자바의 정석] Chapter 7 객체 지향 프로그래밍II (6) - 다형성(polymorphism)(2) (0) | 2021.02.16 |
[자바의 정석] Chapter 7 객체 지향 프로그래밍II (4) - 제어자(modifier) (0) | 2021.02.16 |
[자바의 정석] Chapter 7 객체 지향 프로그래밍II (3) - package와 import (0) | 2021.02.16 |
[자바의 정석] Chapter 7 객체 지향 프로그래밍II(2) - 오버라이딩(overriding) (0) | 2021.02.16 |