다형성은 객체의 타입은 같은데 실행 결과가 다양하게 나타나는 것이다. 하나의 타입에 여러 객체를 대입함으로써 다양한 기능을 이용할 수 있도록 해준다.
자바는 부모 타입에 모든 자식 객체가 대입될 수 있다.
클래스 타입의 타입 변환은 상속관계에 있는 클래스 사이에서 발생한다. 자식 타입은 부모 타입으로 자동 타입 변환이 가능하다.
자동 타입 변환은 자식 클래스는 부모 클래스를 상속받기 때문에 부모와 동일하게 취급될 수 있다는 것이다.
'부모 클래스 변수 = 자식 클래스 타입'
Cat 클래스가 Animal 클래스를 상속 받는다고 할 때,
class Animal {
....
}
class Cat extends Animal {
....
}
Cat cat = new Cat();
Animal animal = cat;
animal과 cat 두 변수의 타입만 다를 뿐, 모두 같은 Cat 객체를 참조하게 된다. 단지 cat객체가 Animal 클래스를 상속 받았기 때문에 Animal 클래스와 동일한 취급을 받을 수 있어서 Animal 타입으로도 이용할 수 있도록 자동으로 타입 변환이 된것이다.
부모 타입으로 자동 변환된 변수는 부모 클래스에 있는 필드와 메소드에만 접근이 가능하다. 변수는 자식 객체를 참조하지만 타입은 부모 클래스이기 때문에 부모 클래스의 멤버에만 접근이 가능하다. 하지만 참조하고 있는 자식 클래스에서 부모 클래스의 메소드를 오버라이딩 했다면 해당 메소드는 부모 메소드가 아닌 오버라이딩 된 자식 메소드가 호출된다.
// Parent.java
public class Parent {
public void method1() {
System.out.println("parent-method1");
}
public void method2() {
System.out.println("parent-method2");
}
}
// Child.java
public class Child extends Parent{
public void method2() {
System.out.println("Child-method2");
}
public void method3() {
System.out.println("Child-method3");
}
}
public class ParentChild {
public static void main(String[] args) {
Parent parent = new Child();
// Parent를 상속받은 객체이기때문에 Parent 타입으로도 사용 할 수 있도록 자동 타입 변환.
parent.method1; // 부모 클래스 method1 호출
parent.method2; // 오버라이딩 된 자식 클래스의 method2 호출.
// parent.method3; // 호출 불가능
}
}
필드의 다형성
다형성은 주로 필드의 값을 다양화함으로써 실행 결과가 다르게 나오도록 구현하는데, 필드의 타입은 변함이 없지만, 실행 도중에 어떤 객체를 필드로 저장하느냐에 따라 실행 결과가 달라질 수 있다.
프로그램은 수많은 객체들이 있는데 이 객체들은 다른 객체로 교체될 수 있어야 한다.
// Tire.class
public class Tire {
// 필드
public int maxRotation; // 타이어 수명
public int accumulatedRotation; // 누적된 회전수
public String location; // 타이어 위치
// 생성자
public Tire(String location, int maxRotation) { // 객체 생성시 타이어의 위치와 수명을 받는다.
this.location = location;
this.maxRotation = maxRotation;
}
// 메소드
public boolean roll() {
++accumulatedRotation; // roll() 메소드가 호출되면 타이어의 누적 회전수 1 증가
if(accumulatedRotation < maxRotation) {
System.out.println(location + " Tire 남은 수명 : " + maxRotation - accumulatedRotation);
return true;
} else {
System.out.println(location + " Tire 펑크");
return false;
}
}
}
// Car.java
public class Car {
// 필드
Tire frontLeftTire = new Tire("앞왼쪽", 2);
Tire frontRightTire = new Tire("앞오른쪽", 3);
Tire backLeftTire = new Tire("뒤왼쪽", 4);
Tire backRightTire = new Tire("뒤오른쪽", 5);
// 메소드
int run() {
System.out.println("자동차 출발");
if(frontLeftTire.roll() == false) { stop(); return 1; };
if(frontRightTire.roll() == false) { stop(); return 2; };
if(backLeftTire.roll() == false) { stop(); return 3; };
if(backRightTire.roll() == false) { stop(); return 4; };
return 0;
}
void stop() {
System.out.println("자동차 멈춤");
}
}
run() 메소드를 통해 자동차가 달리다가 타이어들의 누적 회전수가 타이어 수명에 도달하면 stop() 메소드를 실행하고 해당 타이어 번호를 반환한다.
// Tire 클래스의 자식 클래스 HankookTire.java
public class HankookTire extends Tire {
// 생성자
public HankookTire(String location, int maxRotation) {
super(location, maxRotation); // Tire 클래스의 생성자를 호출할 때 넘겨주었다.
}
// 메소드
@Override
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation < maxRotation) {
System.out.println(location + " Hankook Tire 남은 수명 : " + maxRotation - accumulatedRotation);
return true;
} else {
System.out.println(location + " Hankook Tire 펑크");
return false;
}
}
}
// Tire 클래스의 자식 클래스 KumhoTire.java
public class KumhoTire extends Tire {
// 생성자
public KumhoTire(String location, int maxRotation) {
super(location, maxRotation); // Tire 클래스의 생성자를 호출할 때 넘겨주었다.
}
// 메소드
@Override
public boolean roll() {
++accumulatedRotation;
if(accumulatedRotation < maxRotation) {
System.out.println(location + " Kumho Tire 남은 수명 : " + maxRotation - accumulatedRotation);
return true;
} else {
System.out.println(location + " Kumho Tire 펑크");
return false;
}
}
}
// CarMain.java
public class CarMain {
public static void main(String[] args) {
Car car = new Car();
for(int i = 1; i <= 5; i++){
int problemLocation = car.run();
switch(problemLocation) {
case 1:
System.out.println("앞왼쪽 HankookTire로 교체");
car.frontLeftTire = new HankookTire("앞왼쪽", 15);
break;
case 2:
System.out.println("앞오른쪽 KumhoTire로 교체");
car.frontRightTire = new KumhoTire("앞오른쪽", 12);
break;
case 3:
System.out.println("뒤왼쪽 HankookTire로 교체");
car.backLeftTire = new HankookTire("뒤왼쪽", 13);
break;
case 4:
System.out.println("뒤오른쪽 KumhoTire로 교체");
car.backRightTire = new KumhoTire("뒤오른쪽", 18);
break;
}
System.out.println("--------------------------------------------");
}
}
}
car 객체의 run() 메소드를 실행하면 각 타이어들이 Tire 클래스의 roll() 메소드를 수행하면서 각 위치 타이어들의 누적 회전수를 1씩 증가시키고, tire 객체의 roll() 메소드로 부터 true, false를 반환받아 만약 false라면 stop() 메소드를 수행하고 타이어의 위치를 반환한다. 그리고 반환 받은 타이어 위치를 problemLocation으로 저장하고 car 객체의 (frontLeft)Tire 필드를 HankookTire 객체 혹은 KumhoTire 객체로 변경하고 반복문을 계속 돈다. car 객체의 run() 메소드가 계속 실행되면서 car 객체의 (frontLeft)Tire 필드가 HankookTire 객체를 받고 있기 때문에 run() 메소드 속의 (frontLeft)Tire 필드는 HankookTire 객체의 roll() 메소드를 수행하게 된다. 필드의 타입은 Tire 타입으로 바뀌지 않지만 Tire 클래스를 상속받은 자식 클래스 객체의 메소드를 사용함으로써 결과가 달라지는 것이다.
매개 변수의 다형성
자동 타입 변환은 필드의 값을 대입할 때에도 발생하지만, 메소드를 호출할 때 많이 발생한다.
매개값을 다양화하기 위해 매개 변수에 자식 타입 객체를 지정할 수도 있다.
// 부모 클래스 Vehicle.java
public class Vehicle {
public void run() {
System.out.println("차가 달린다");
}
}
// Vehicle을 이용하는 Driver.java
public class Driver {
public void drive(Vehicle vehicle) {
vehicle.run(); // Vehicle class에 있는 run() 메소드
}
}
// Vehicle의 자식 클래스 Bus.java
public class Bus extends Vehicle{
@Override
public void run() {
System.out.println("버스가 달린다");
}
}
// Vehicle의 자식 클래스 Taxi.java
public class Taxi extends Vehicle {
@Override
public void run() {
System.out.println("택시가 달린다");
}
}
// 실행 클래스 VehicleRun.java
public class VehicleRun {
public static void main(String[] args) {
Driver driver = new Driver();
Bus bus = new Bus();
Taxi taxi = new Taxi();
driver.drive(bus);
driver.drive(taxi);
}
}
drive 메소드의 매개변수는 Vehicle 타입이지만 Vehicle을 상속받은 bus 객체와 taxi 객체 또한 부모와 같은 취급을 받기 때문에 매개변수로 들어 갈 수 있고, drive 메소드에서 bus 객체와 taxi 객체의 오버라이딩 된 run() 메소드를 실행해서 다른 결과를 만들어 낸다. 매개변수의 다형성이다.
강제 타입 변환
부모 타입을 자식 타입으로 변환하는 것을 말한다. 정확히는 자식 타입이 부모 타입으로 자동 변환 된 후, 다시 자식 타입으로 변환할 때 강제 타입 변환을 사용하는 것이다.
자식 타입이 부모 타입으로 자동 변환 되면, 부모 타입의 필드와 메소드만 사용 가능 해진다. 만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 되돌려 놓고 사용하는 것이다.
Parent parent = new Child(); // Parent 타입으로 자동 변환된 Child 객체를 저장한 parent 변수
Child child = (Child) parent; // Child 객체가 있는 Parent 타입의 parent 변수를 Child 타입으로 강제 변환.
상속 관계 일 때 가능한 것이다.
// Parent.java
public class Parent {
public String field1;
public void method1() {
System.out.println("parent-method1");
}
public void method2() {
System.out.println("parent-method2");
}
}
// Child.java
public class Child extends Parent{
public String field2;
public void method2() {
System.out.println("child-method2");
}
public void method3() {
System.out.println("child-method3");
}
}
// Main.java
public class Main {
public static void main(String[] args) {
Parent parent = new Child();
parent.field1 = "data1";
parent.method1();
parent.method2();
Child child = (Child) parent;
child.field2 = "data2";
child.method2();
child.method3();
}
}
부모 필드와 메소드 밖에 사용 못하는 parent 변수를 강제로 타입 변환해서 child 변수에 저장해 자식 필드와 메소드를 호출 가능 하게 만들었다.