코드 실행 순서를 맞춰보자
부모 클래스가 버젓이 print를 가지고 있는데도, 부모 클래스에서 자식의 print가 실행된다!?
런타임에는 무슨일이?
왜 이렇게 작동할까?
super와 this를 이용한 오버로딩 메소드 호출
아래 코드가 어떻게 작동되는지
100% 확신이 있으시다면 뒤로 가셔도 좋습니다 😇😇
class Base {
Base() {
System.out.println("Base Class Constructor");
print();
}
void print() {
System.out.println("Base");
}
}
class Derived extends Base {
int x;
Derived(int x) {
System.out.println("Derived Class Constructor");
this.x = x;
}
@Override
void print() {
System.out.println("Derived " + x);
}
}
public class Main {
public static void main(String[] args) {
Derived d = new Derived(10);
d.print();
}
}
// <정답>
// Base Class Constructor
// Derived 0
// Derived Class Constructor
// Derived 10
함정은 Base의 생성자에 있는 print 메소드에 있습니다.
실행 흐름은 이렇습니다!
자 그럼, 왜 이렇게 작동하는지 궁금해지지 않나요..ㅎㅎ
컴파일 타임에는 뭘하나 싶으니 일단 컴파일부터 해본다..
일단 상속을 받은 자식의 생성자에서 부모의 생성자가 먼저 실행되는 것을 확인가능하다.
또 부모와 자식 모두 같은 시그니처의 void print() 메소드를 가지고 있는 것을 통해서 오버라이딩이 이루어지고 있다는 것을 확인할 수 있다.
class Base {
Base();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Base Class Constructor
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_0
13: invokevirtual #5 // Method print:()V
16: return
void print();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String Base
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
class Derived extends Base {
int x;
Derived(int);
Code:
0: aload_0
1: invokespecial #1 // Method Base."<init>":()V
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String Derived Class Constructor
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_0
13: iload_1
14: putfield #5 // Field x:I
17: return
void print();
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #5 // Field x:I
7: invokedynamic #6, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
12: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
15: return
}
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class Derived
3: dup
4: bipush 10
6: invokespecial #3 // Method Derived."<init>":(I)V
9: astore_1
10: aload_1
11: invokevirtual #4 // Method Derived.print:()V
14: return
}
타입은 인터페이스를 보장해줄 뿐, 실행은 실제 메모리에 있는 내용을 따른다.
dog2는 Animal 클래스가 가지고 있는 인터페이스만 사용할 수 있도록 한정하지만,
메소드 실행시에는 메모리 주소에 들어있는 객체의 상태를 따라간다.
(Method Animal.sleep()V 라고 되어있으나 Dog의 Sleep이 실행됨)
class Animal {
eat() {}
sleep() {
System.out.println("animal");
}
}
class Dog extends Animal {
bark() {}
@Override
sleep() {
System.out.println("dog");
}
}
public class Main {
public static void main(String[] args) {
Dog dog1 = new Dog();
dog1.bark();
dog1.eat();
dog1.sleep(); // "dog"
Animal dog2 = new Dog();
dog2.bark(); // 실행불가능
dog2.eat();
dog2.sleep(); // "dog"
}
}
/* Main의 바이트 코드 */
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class Dog
3: dup
4: invokespecial #3 // Method Dog."<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #4 // Method Dog.bark:()V
12: aload_1
13: invokevirtual #5 // Method Dog.eat:()V
16: aload_1
17: invokevirtual #6 // Method Dog.sleep:()V
20: new #2 // class Dog
23: dup
24: invokespecial #3 // Method Dog."<init>":()V
27: astore_2
28: aload_2
29: invokevirtual #7 // Method Animal.eat:()V
32: aload_2
33: invokevirtual #8 // Method Animal.sleep:()V
36: return
}
객체지향은 다형성의 특징을 이용하고 있기 때문에, 부모를 상속받은 다양한 종류의
자식 객체가 부모 타입의 객체에 할당이 가능하다.
아래 코드처럼 코드가 실행되기 이전에 어떤 객체가 할당될지 알 수 없는 경우가 있기때문에, 자바는 오버라이딩에 대해서 동적 바인딩을 하도록 만들어졌다고 생각한다.
class Animal {
void sleep() {
System.out.println("animal");
}
}
class Dog extends Animal {
@Override
void sleep() {
System.out.println("dog");
}
}
class Cat extends Animal {
@Override
void sleep() {
System.out.println("cat");
}
}
public class Main {
public static void main(String[] args) {
int randomNum = (int) (Math.random() * 10);
Animal animal;
if (randomNum > 5) {
animal = new Dog();
} else {
animal = new Cat();
}
animal.sleep();
}
}
/* 바이트 코드 */
public class Main {
public Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method java/lang/Math.random:()D
3: ldc2_w #3 // double 10.0d
6: dmul
7: d2i
8: istore_1
9: iload_1
10: iconst_5
11: if_icmple 25
14: new #5 // class Dog
17: dup
18: invokespecial #6 // Method Dog."<init>":()V
21: astore_2
22: goto 33
25: new #7 // class Cat
28: dup
29: invokespecial #8 // Method Cat."<init>":()V
32: astore_2
33: aload_2
34: invokevirtual #9 // Method Animal.sleep:()V
37: return
}
this
Animal 클래스에서 사용된 this는 호출된 시점의 객체를 지칭한다.
new Animal()을 통해서 호출되었다면 this는 animal 객체를,
new Dog()를 통해서 호출되었다면 this는 dog 객체를 지칭한다.
Dog클래스는 이미 컴파일타임에 sleep이 오버라이딩 되어서 dog객체의 sleep은
오버라이딩 된 sleep이다.
(개인적으로는 Dog를 생각할 때 Animal은 생각하지 않고, Animal의 초기화 과정이 Dog의 생성자의 가장 처음에 실행된다고 생각하려한다.)
super
super는 this와는 다르게 해당 객체의 부모를 명시적으로 가르킨다.
따라서 오버라이딩 여부와 관계없이 실제 부모 클래스가 가지고 있는 메소드를
가리킨다.
class Animal {
Animal() {
this.sleep();
}
void sleep() {
System.out.println("animal");
}
}
class Dog extends Animal {
Dog() {
super();
super.sleep();
}
@Override
void sleep() {
System.out.println("dog");
}
}
public class Main {
public static void main(String[] args) {
Dog dog1 = new Dog();
}
}
/* 바이트 코드 */
class Animal {
Animal();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: invokevirtual #2 // Method sleep:()V
8: return
void sleep();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String animal
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
class Dog extends Animal {
Dog();
Code:
0: aload_0
1: invokespecial #1 // Method Animal."<init>":()V
4: aload_0
5: invokespecial #2 // Method Animal.sleep:()V
8: return
void sleep();
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String dog
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
공감하며 읽었습니다. 좋은 글 감사드립니다.