public class StaticDispatch {
static abstract class Human {
}
static class Man extends Human {
}
static class Woman extends Human {
}
// 오버로딩된 메서드 (1)
public void sayHello(Human guy) {
System.out.println("Hello, guy!");
}
// 오버로딩된 메서드 (2)
public void sayHello(Man guy) {
System.out.println("Hello, gentleman!");
}
// 오버로딩된 메서드 (3)
public void sayHello(Woman guy) {
System.out.println("Hello, lady!");
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man);
sr.sayHello(woman);
}
}
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}
static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("Man said hello");
}
}
static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("Woman said hello");
}
}
public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}
import java.io.Serializable;
public class Overload {
public static void sayHello(Object arg) {
System.out.println("Hello Object");
}
public static void sayHello(int arg) {
System.out.println("Hello int");
}
public static void sayHello(long arg) {
System.out.println("Hello long");
}
public static void sayHello(Character arg) {
System.out.println("Hello Character");
}
public static void sayHello(char arg) {
System.out.println("Hello char");
}
public static void sayHello(char... arg) {
System.out.println("Hello char ...");
}
public static void sayHello(Serializable arg) {
System.out.println("Hello Serializable");
}
public static void main(String[] args) {
sayHello('a');
}
}
sayHello(char arg)
메서드 주석처리시 어떻게 되는가 ?sayHello(int arg)
메서드 주석처리시 어떻게 되는가 ?sayHello(long arg)
메서드 주석처리시 어떻게 되는가 ?sayHello(Character arg)
메서드 주석처리시 어떻게 되는가 ?sayHello(Comparable<Character> arg)
메서드가 같이 있다면 ?sayHello(Serializable arg)
메서드 주석처리시 어떻게 되는가 ?sayHello(Object arg)
메서드 주석처리시 어떻게 되는가 ?public class FieldHasNoPolymorphic {
static class Father {
public int money = 1;
public Father() {
money = 2;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am a Father, I have $" + money);
}
}
static class Son extends Father {
public int money = 3;
public Son() {
money = 4;
showMeTheMoney();
}
public void showMeTheMoney() {
System.out.println("I am a Son, I have $" + money);
}
}
public static void main(String[] args) {
Father guy = new Son();
System.out.println("This guy has $" + guy.money);
}
}
javac
라는 컴파일러를 거치고 나면 자바 바이트 코드가 된다*.class
) → 이후 JVM 위에서 실행됨클래스 파일 구조
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
...
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
...
}
클래스 파일의 구조는 부호 없는 숫자 와 테이블로 구성되어 있다
u1
(1 바이트) , u2
(2 바이트) , u4
(4 바이트) , u8
(8 바이트)_info
로 끝남클래스 파일을 십육진수 편집기 (HxD) 로 열어본 모습
javac
로 컴파일할 때는 C, C++ 와 달리 링크 단계가 없다Code_attribute {
u2 attribute_name_index;
...
u4 code_length;
u1. code[code_length]; // 바이트 코드 명령어들 (명령어 각각이 1바이트임)
}
준비는 클래스 변수를 메모리에 할당하고 초기값을 설정하는 단계이다
첫째. 인스턴스 변수가 아닌 클래스 변수만 할당된다. 인스턴스 변수는 객체가 인스턴스화할 때 객체와 함께 자바 힙에 할당된다.
둘째. 준비 단계에서 클래스 변수에 할당하는 초기값은 해당 데이터 타입의 제로 값이다.
셋째. 지역변수는 준비단계가 없다. 즉 아래 int a 가 컴파일되지 않는 것은 시스템 초기값 (0) 이 할당되지 않는 것이다
public static void main(String[] args) {
int a; // 컴파일 에러 발생
System.out.println(a);
}
<clinit>()
메서드를 실행하는 단계런타임 상수 풀
동적 링크
메서드 호출은 메서드 본문 코드를 실행하는 일과 다르다
메서드 호출 단계에서 수행하는 유일한 일은 호출할 메서드의 버전을 선택하는 것이다
링킹 단계가 존재하지 않기 때문에 클래스 파일에 저장된 모든 메서드 호출은 심벌 참조일 뿐 메서드 주소를 담은 직접 참조가 아니다
그러나 그 중 일부는 직접 참조로 변환하는데 즉, 컴파일러가 프로그램 코드를 컴파일하는 시점에 호출 대상이 정해진다
invokestatic
, invokespecial
sayHello()
메서드 중 JVM 이 Human 타입을 받는 버전을 선택하는 이유는 무엇일까 ?Human man = new Man();
// 실제 타입 변경
Human human = new Random() ? new Man() : new Woman(); // 랜덤으로 둘중 하나의 타입이 런타임에 들어갔다고 가정
// 정적 타입 변경
sr.sayHello((Man) human);
sr.sayHello((Woman) human);
Man
인지 Woman
인지는 프로그램이 해당 코드 라인을 실행할때에 마침내 선택된다human
객체의 정적 타입은 Human
이며 사용중에는 변경가능하다sayHello()
메서드를 호출할 때 강제로 변환했기 때문에 변환 결과가 Man
인지 Woman
인지 컴파일 타임에 명확해진다Human man = new Man(); // 정적 타입 = Human, 실제 타입 = Man
Human woman = new Woman(); // 정적 타입 = Human, 실제 타입 = Woman
sr.sayHello(man); // 메서드 버전 선택 필요
sr.sayHello(woman); // 메서드 버전 선택 필요
sayHello()
메서드를 두 번 호출한다.sr
객체의 sayHello()
메서드를 호출시, 오버로딩된 메서드 중 어느 버전을 호출할지는 전적으로 매개변수의 수와 타입이 기준이다man
과 woman
은 정적 타입이 같지만 실제 타입은 다르다.sayHello()
를 선택할 때 매개변수의 실제 타입이 아닌 정적 타입을 참고한다.javac
컴파일러는 매개변수의 정적 타입을 보고 어떤 오버로딩 버전을 호출할지 선택한다. 따라서 sayHello(Human)
이 호출 대상으로 선택된다javac
컴파일러가 오버로딩된 메서드 중 적절한 버전을 선택할 수 있지만, 어느 하나를 꼭 집어내지 못하여 “비교적 더 적합한” 버전으로 선택하는 경우도 많다
모호함의 주된 원인은 바로 “리터럴” 이다.
리터럴에는 명시적인 정적 타입이 없으므로 언어와 문법 규칙을 바탕으로 이해하고 유추할 수 있을 뿐이다.
문제 3번의 1번째 답은 “Hello char”
이다
리터럴 ‘a’
의 타입은 char
이므로 자연스럽게 매개변수 타입인 char
인 버전을 선택했다
문제 3번의 2번째 답은 “Hello int”
이다
자동 형 변환이 이루어진 것 이다.
a
는 숫자 97을 나타낼 수도 있다, 따라서 매개 변수 타입인 int 인 메서드도 적합하다. 문제 3번의 3번째 답은 “Hello long”
이다
이번에는 자동 형 변환이 두번 일어난다
a
를 정수 97 로 변환한 후 매개 변수 타입인 long 과 일치시키기 위해 long 타입 정수인 97L 로 다시 변환한다 char
→ int
→ long
→ float
→ double
순서로 이루어진다. 문제 3번의 4번째 답은 “Hello Character” 이다
오토박싱이 일어났다
a
의 래퍼 타입인 java.lang.Character
로 박싱하여 매개 변수 타입인 Character
인 메서드와 일치시켰다 문제 3번의 5번째 답은 "Hello Serializable"
이다
문자와 직렬화가 무슨 관련이 있을까 ?
원인은 Character
가 java.lang.Serializable
을 구현했기 때문이다
문제 3번의 6번째 답은 컴파일 오류이다
Character
는 또 다른 인터페이스인 java.lang.Comparable<Character>
도 구현한다
그러나 이는 Serializable
을 받는 메서드와 우선순위가 동일하다
문제 3번의 7번째 답은 "Hello Object"
이다
문제 3번의 8번째 답은 "Hello char ..."
이다
Man said hello
Woman said hello
Woman said hello
man
과 woman
의 정적 타입은 모두 Human
으로 똑같지만, sayHello()
를 호출하면 서로 다른 메서드를 호출하기 때문이다invokevirtual
이고 실제로 심벌 참조 (Human.sayHello()
) 도 모두 동일하다invokevirtual
명령어는 실행의 첫 단계에서 런타임 수신 객체의 실제 타입을 해석한다는 점이 중요하다invokevirtual
의 실행 로직에 있다I am a Son, I have $0
I am a Son, I have $4
This guy has $2
Father
의 생성자에서 출력하고 두 번째 줄은 Son
클래스의 생성자에 출력한 결과이다 “Son”
이라는 결과가 나왔을까 ?Father
타입의 guy
는 Son
의 객체로 초기화 된다. 따라서 guy
는 Father
타입을 참조하지만 실제 인스턴스는 Son
이며 이 과정에서 Father
와 Son
클래스의 생성자가 모두 호출된다Son
클래스가 생성될 때 암묵적으로 Father
의 생성자를 먼저 호출하는데, Father
의 생성자에서 money = 2
가 수행되고 showMeTheMoney()
가 호출된다 Son
자식클래스에서 오버라이딩 되었으므로 실제 실행되는 것은 Son
의 showMeTheMoney
, 이는 가상 메서드 호출을 실행합니다money
필드는 2
로 초기화되었지만 Son.showMeTheMoney()
메서드는 자식 클래스의 money
필드를 이용한다money
필드는 자식 클래스의 생성자가 실행될 때에 초기화되기 때문에 아직은 값이 0
인 상태이다 Son
생성자가 실행되어야지만 값을 할당함 money
로부터 값을 직접 가져왔기 때문에 2
를 출력한다invokevirtual
이라는 명령어를 사용하지 않음public class Dispatch {
static class QQ {}
static class _360 {}
public static class Father {
public void choice(QQ arg) {
System.out.println("Father chose a qq");
}
public void choice(_360 arg) {
System.out.println("Father chose a 360");
}
}
public static class Son extends Father {
public void choice(QQ arg) {
System.out.println("Son chose a qq");
}
public void choice(_360 arg) {
System.out.println("Son chose a 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.choice(new _360());
son.choice(new QQ());
}
}
Father
이냐 Son
이냐이고 다른 하나는 매개 변수 타입이 QQ
이나 _360
이냐이다Father
의 두 개의 메서드를 실행할 명령어를 생성하는데 사용된다.Father.choice(_360)
과 Father.choice(QQ)
메서드이다son.choice(QQ)
가 실행될 때 컴파일 타임에 이미 choice(QQ)
로 결정되었다 QQ
로 전달되는 인수의 실제 타입이 무엇인지는 상관이 없다 QQ
로 전달되는 인수의 실제 타입이 무엇인지는 상관하지 않는다. 이처럼 정적 타입과 실제 타입은 메서드 선택에 관여하지 않는다Father
이냐 Son
이냐 뿐.출력.
Father chose a 360
Son chose a qq
참고 )
var
라는 키워드는 오른쪽 표현식의 타입을 보고 컴파일 타임에 추론하기 때문에 정적 디스패치를 한다답.
문제1
Hello, guy!
Hello, guy!
문제2
Man said hello
Woman said hello
Woman said hello
문제3
Hello char
문제3-1
Hello int
문제 3-2
Hello long
문제 3-3
Hello Character
문제 3-4
Hello Serializable
문제 3-5
"The method sayHello(Object) is ambiguous for the type overload"
라는 메시지를 뿌리며 컴파일을 거부한다.
문제 3-6
Hello Object
문제 3-7
Hello char ...
문제 4
I am a Son, I have $0
I am a Son, I have $4
This guy has $2