자바 다형성 이해하기: 개념과 실전 활용
다형성(Polymorphism)은 객체가 여러 형태를 취할 수 있는 능력입니다. OOP에서 다형성이 가장 흔히 사용되는 경우는 부모 클래스 참조가 자식 클래스 객체를 참조할 때입니다.
두 개 이상의 IS-A 테스트를 통과할 수 있는 자바 객체는 다형적(polymorphic)이라고 간주됩니다. 자바에서 모든 자바 객체는 다형적입니다. 어떤 객체든 자신의 타입과 Object 클래스에 대해 IS-A 테스트를 통과하기 때문입니다.
객체에 접근하는 유일한 방법은 참조 변수를 통하는 것임을 아는 것이 중요합니다. 참조 변수는 오직 한 가지 타입만 가질 수 있습니다. 한 번 선언되면 참조 변수의 타입은 변경할 수 없습니다.
참조 변수는 final로 선언되지 않은 한 다른 객체에 재할당될 수 있습니다. 참조 변수의 타입은 객체에서 호출할 수 있는 메서드를 결정합니다.
참조 변수는 선언된 타입의 객체나 그 하위 타입의 객체를 참조할 수 있습니다. 참조 변수는 클래스나 인터페이스 타입으로 선언할 수 있습니다.
예제
public interface Vegetarian { }
public class Animal { }
public class Deer extends Animal implements Vegetarian { }
이제 Deer 클래스는 다중 상속을 가지므로 다형적이라고 간주됩니다. 위 예제에 대해 다음이 참입니다.
- Deer IS-A Animal
- Deer IS-A Vegetarian
- Deer IS-A Deer
- Deer IS-A Object
참조 변수 사실을 Deer 객체 참조에 적용하면 다음 선언들이 합법적입니다.
예제
Deer d = new Deer();
Animal a = d;
Vegetarian v = d;
Object o = d;
모든 참조 변수 d, a, v, o는 힙에 있는 같은 Deer 객체를 참조합니다.
가상 메서드(Virtual Methods)
이 섹션에서는 자바에서 오버라이드된 메서드의 동작이 클래스 설계 시 다형성을 활용하는 데 어떻게 도움이 되는지 보여드리겠습니다.
이미 메서드 오버라이딩을 다뤘습니다. 자식 클래스는 부모 클래스의 메서드를 오버라이드할 수 있습니다. 오버라이드된 메서드는 부모 클래스에서 본질적으로 숨겨지며, 자식 클래스가 오버라이딩 메서드 내에서 super 키워드를 사용하지 않는 한 호출되지 않습니다.
예제
/* File name: Employee.java */
public class Employee {
private String name;
private String address;
private int number;
public Employee(String name, String address, int number) {
System.out.println("Constructing an Employee");
this.name = name;
this.address = address;
this.number = number;
}
public void mailCheck() {
System.out.println("Mailing a check to " + this.name + " " + this.address);
}
public String toString() {
return name + " " + address + " " + number;
}
public String getName() { return name; }
public String getAddress() { return address; }
public void setAddress(String newAddress) { address = newAddress; }
public int getNumber() { return number; }
}
이제 Employee 클래스를 다음과 같이 확장한다고 가정해 보세요.
/* File name: Salary.java */
public class Salary extends Employee {
private double salary; // 연봉
public Salary(String name, String address, int number, double salary) {
super(name, address, number);
setSalary(salary);
}
public void mailCheck() {
System.out.println("Within mailCheck of Salary class ");
System.out.println("Mailing check to " + getName() + " with salary " + salary);
}
public double getSalary() { return salary; }
public void setSalary(double newSalary) {
if (newSalary >= 0.0) {
salary = newSalary;
}
}
public double computePay() {
System.out.println("Computing salary pay for " + getName());
return salary / 52;
}
}
다음 프로그램을 주의 깊게 살펴보고 출력을 예측해 보세요.
/* File name: VirtualDemo.java */
public class VirtualDemo {
public static void main(String[] args) {
Salary s = new Salary("Mohd Mohtashim", "Ambehta, UP", 3, 3600.00);
Employee e = new Salary("John Adams", "Boston, MA", 2, 2400.00);
System.out.println("Call mailCheck using Salary reference --");
s.mailCheck();
System.out.println("\n Call mailCheck using Employee reference--");
e.mailCheck();
}
}
실행 결과
Constructing an Employee
Constructing an Employee
Call mailCheck using Salary reference --
Within mailCheck of Salary class
Mailing check to Mohd Mohtashim with salary 3600.0
Call mailCheck using Employee reference--
Within mailCheck of Salary class
Mailing check to John Adams with salary 2400.0
여기서 두 개의 Salary 객체를 인스턴스화합니다. 하나는 Salary 참조 s로, 다른 하나는 Employee 참조 e로.
s.mailCheck()를 호출할 때, 컴파일러는 컴파일 타임에 Salary 클래스의 mailCheck()를 보고, JVM은 런타임에 Salary 클래스의 mailCheck()를 호출합니다.
e에서 mailCheck()는 다릅니다. e는 Employee 참조이기 때문입니다. 컴파일러가 e.mailCheck()를 볼 때, Employee 클래스의 mailCheck() 메서드를 봅니다.
여기서 컴파일 타임에는 컴파일러가 Employee의 mailCheck()를 사용해 이 문장을 검증합니다. 하지만 런타임에는 JVM이 Salary 클래스의 mailCheck()를 호출합니다.
이 동작을 가상 메서드 호출(virtual method invocation)이라고 하며, 이런 메서드들을 가상 메서드(virtual methods)라고 합니다. 오버라이드된 메서드는 컴파일 타임에 소스 코드에서 사용된 참조의 데이터 타입이 무엇이든 런타임에 호출됩니다.