제가 앞으로 설명하는 방법은 "스프링 입문을 위한 자바 객체 지향의 원리와 이해"를 공부한 내용을 바탕으로 합니다. 그래서 제가 그 책을 통해 공부한 내용을 토대로 새로운 코드를 분석해볼 것입니다.
package casting;
public class Parent {
int age;
Parent() {};
Parent(int age){
this.age = age;
}
void printInfo(){
System.out.println("Parent Call !!");
}
}
package casting;
public class Son extends Parent{
String name;
Son() {}
Son(int age, String name) {
super(age);
this.name =name;
}
@Override
void printInfo() {
System.out.println("Son Call !!");
}
}
package casting;
public class Daughter extends Parent {
String name;
Daughter() {}
Daughter(int age, String name) {
super(age);
this.name =name;
}
@Override
void printInfo() {
System.out.println("Daughter Call !!");
}
void sing() {
System.out.println("La La La");
}
}
package casting;
public class Test {
public static void main(String[] args) {
System.out.println("Parent p = new Son();");
Parent p = new Son();
System.out.println("printInfo is overrided in Son class.");
p.printInfo(); // 문제 1 : 출력 결과는? -> 오버라이딩된 경우
System.out.println();
try {
Son c = (Son) new Parent(); // 문제 2 : 에러 종류는?
} catch(Exception e) {
System.out.println("Son c = (Son) new Parent(); : Error");
e.printStackTrace();
}
System.out.println();
Parent human = new Daughter();
System.out.println("Parent human = new Daughter();");
System.out.println("printInfo is overrided in Daughter class.");
human.printInfo();
System.out.println();
if(human instanceof Daughter) {
System.out.println("human instanceof Daughter : True");
} else {
System.out.println("human instanceof Daughter : False");
}
if(human instanceof Son) {
System.out.println("human instanceof Son : True");
} else {
System.out.println("human instanceof Son : False");
}
if(human instanceof Parent) {
System.out.println("human instanceof Parent : True");
} else {
System.out.println("human instanceof Parent : False");
}
System.out.println();
if((Daughter)human instanceof Daughter) {
System.out.println("(Daughter)human instanceof Daughter : True");
} else {
System.out.println("(Daughter)human instanceof Daughter : False");
}
try {
if((Son)human instanceof Son) {
System.out.println("(Son)human instanceof Son : True");
}
} catch(Exception e) {
System.out.println("(Son)human instanceof Son : Error");
e.printStackTrace();
}
System.out.println();
Daughter d = new Daughter();
if((Parent)d instanceof Parent) {
System.out.println("(Parent)d instanceof Parent : True");
} else {
System.out.println("(Parent)d instanceof Parent : False");
}
if((Parent)d instanceof Daughter) {
System.out.println("(Parent)d instanceof Daughter : True");
} else {
System.out.println("(Parent)d instanceof Daughter : False");
}
}
}
메인 메서드가 실행된 직후의 프로그램 메모리의 데이터 저장 영역의 모습이다.
스택 영역 내부의 스택 프레임 내부에 객체 참조 변수가 저장되고, 그것에 힙 영역에 할당된 객체의 주소가 포인터로서 대입된 모습을 그림으로 나타냈다. 이때 p는 Parent 객체 참조 변수인데, Son 클래스의 인스턴스가 할당됐다. 이런 경우에 클래스의 묵시적 형변환(업캐스팅)이 발생하여 위와 같이 포인터가 Parent 객체에 가게 된다. 검은색 박스가 객체 참조 변수가 접근할 수 있는 영역이다.
오버라이딩된 메서드는 아무리 업캐스팅한 뒤에 호출하여도 재정의된 내용대로 호출된다. 이러한 관계는 그림에서 확인할 수 있다.
Son 객체 참조 변수인 c에 Parent 클래스의 인스턴스를 할당하여 java.lang.ClassCastException이 발생하였다. 이는 그림에서도 볼 수 있듯이, Parent 클래스의 인스턴스에는 애초에 Son 클래스의 인스턴스가 존재하지 않는다. 그래서 참조조차 할 수 없는 것이다.
java.lang.ClassCastException 예외가 발생!
Parent 객체 참조 변수인 human에 Daughter 클래스의 인스턴스가 할당된 모습이다. 이때 Daughter 인스턴스에는 Parent 인스턴스가 포함되어 있기에 자연스럽게 human은 Parent 클래스의 인스턴스를 참조하게 되고 접근 가능한 영역을 검은색 박스로 표현하였다. 이와 같이 자연스럽게 형변환 된 경우를 up casting이라고 한다.
위에서와 같이 재정의된 메서드가 호출된다. 아무리 super 클래스에서 호출을 하였더라도 말이다. 애초에 생각해보면 힙 영역에 할당하기를 Daughter 클래스의 인스턴스를 할당했기에 당연한 결과이기도 하다.
instanceof를 위와 같이 힙 영역에 어떤 객체가 할당되었는지 그림으로 확인하며 그 결과값을 쉽게 예측할 수 있다. 즉, 힙 영역에 instanceof가 확인하려는 객체가 존재하는지 유무에 따라서 True, False가 나뉜다. 이때, 상속 받은 객체는 상위 객체를 포함하여 할당된다는 것을 기억하자. 그래야만 그림과 같이 상상하고 코드를 이해할 수 있다.
human을 Down Casting한 경우에도 마찬가지이다. 결국 instanceof 연산자가 확인하려는 객체가 실질적으로 어떤 객체들을 포함하고 있는지를 판단할 수 있어야 한다.
이번에는 Daughter 객체 참조 변수가 Daughter 클래스의 인스턴스를 참조하고 있는 경우에 instanceof 연산자를 확인해보았다.동시에 Up casting도 해보았다. 그럼에도 불구하고 힙 영역에 실질적으로 어떤 객체가 할당되어있고, 이것을 어떤 객체 참조 변수가 참조하고 있고, 마지막으로 instanceof 연산자로 확인하려는 객체가 무엇인지 판단한다면, 손쉽게 그 결과값을 에측할 수 있다.
이처럼 그림을 통해서 메모리 영역을 상상할 수 있다면 코드가 매우 직관적으로 판단할 수 있다.
이것은 비단 instanceof와 형변환뿐 만이 아니다. 정적 멤버를 왜 굳이 클래스.메서드 등과 같은 형태로 접근해야 하는지 그림을 통해서 파악할 수도 있다. 결론부터 말하자면 이는 물리적 관점에서 효율적이기 때문이다. 자세한 내용은 위에서 언급한 책에서 확인해보길 바란다.
틀린 점 있다면 언제든 지적 부탁 드립니다! 감사합니다!