Java 프로그램을 실행하려면 컴파일과 실행(해석)이라는 두 단계가 필요합니다. 동시에 Java는 객체 지향 프로그래밍 언어입니다. 하위 클래스와 상위 클래스의 메서드가 동일하고 하위 클래스가 상위 클래스의 메서드를 재정의하는 경우 프로그램이 런타임에 해당 메서드를 호출할 때 상위 클래스의 메서드를 호출해야 할까요, 아니면 하위 클래스의 재정의된 메서드를 호출해야 할까요? Java를 처음 배울 때 문제가 발생해야 합니다. 여기에서는 먼저 어떤 메서드를 호출할지 또는 변수 작업을 바인딩이라고 할지 결정합니다.
Java에는 두 가지 바인딩 방법이 있습니다. 하나는 초기 바인딩이라고도 하는 정적 바인딩입니다. 다른 하나는 동적 바인딩(후기 바인딩이라고도 함)입니다.
차이점 비교
1. 정적 바인딩은 컴파일 타임에 발생하고 동적 바인딩은 런타임에 발생합니다.
2. private, static, final로 수정된 변수나 메소드를 사용하고, 정적 바인딩을 사용하세요. 가상 메서드(하위 클래스에 의해 재정의될 수 있는 메서드)는 런타임 개체를 기반으로 동적으로 바인딩됩니다.
3. 정적 바인딩은 클래스 정보를 이용하여 완성하고, 동적 바인딩은 객체 정보를 이용하여 완성해야 한다.
4. 오버로드된 메서드는 정적 바인딩을 사용하여 완성되고 재정의된 메서드는 동적 바인딩을 사용하여 완성됩니다.
오버로드된 메소드의 예
다음은 오버로드된 메서드의 예입니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 TestMain {
공개 정적 무효 메인(String[] args) {
문자열 str = 새로운 문자열();
발신자 발신자 = new Caller();
caller.call(str);
}
정적 클래스 발신자 {
공개 무효 호출(객체 obj) {
System.out.println("호출자의 개체 인스턴스");
}
공개 무효 호출(문자열 str) {
System.out.println("발신자의 문자열 인스턴스");
}
}
}
실행 결과는
다음과 같이 코드 코드를 복사합니다.
22:19 $javaTestMain
Caller의 String 인스턴스
위 코드에는 호출 메소드의 오버로드된 구현이 두 개 있습니다. 하나는 Object 유형의 객체를 매개변수로 받고, 다른 하나는 String 유형의 객체를 매개변수로 받습니다. str은 String 객체이며, String 유형 매개변수를 받는 모든 호출 메서드가 호출됩니다. 여기서 바인딩은 컴파일 타임의 매개변수 유형을 기반으로 하는 정적 바인딩입니다.
확인하다
겉모습만 봐서는 static 바인딩이 수행되었다는 것을 증명할 수 없습니다. javap를 사용하여 컴파일해 보면 알 수 있습니다.
다음과 같이 코드 코드를 복사합니다.
22:19 $ javap -c TestMain
"TestMain.java"에서 컴파일됨
공개 클래스 TestMain {
공개 테스트메인();
암호:
0: 로드_0
1: Invokespecial #1 // 메소드 java/lang/Object."<init>":()V
4: 복귀
공개 정적 무효 메인(java.lang.String[]);
암호:
0: 새로운 #2 // 클래스 java/lang/String
3: 복제
4: Invokespecial #3 // 메소드 java/lang/String."<init>":()V
7: astore_1
8: 새로운 #4 // 클래스 TestMain$Caller
11: 복제
12: Invokespecial #5 // 메서드 TestMain$Caller."<init>":()V
15: astore_2
16: 로드_2
17: 로드_1
18: Invokevirtual #6 // 메소드 TestMain$Caller.call:(Ljava/lang/String;)V
21: 복귀
}
다음 줄 18을 봤습니다. Invokevirtual #6 // Method TestMain$Caller.call:(Ljava/lang/String;)V는 실제로 정적으로 바인딩되어 있으며, 이는 String 개체를 매개 변수로 받는 호출자 메서드가 호출되었음을 확인합니다.
메서드 재정의의 예
다음과 같이 코드 코드를 복사합니다.
공개 클래스 TestMain {
공개 정적 무효 메인(String[] args) {
문자열 str = 새로운 문자열();
호출자 호출자 = new SubCaller();
caller.call(str);
}
정적 클래스 발신자 {
공개 무효 호출(문자열 str) {
System.out.println("호출자의 문자열 인스턴스");
}
}
정적 클래스 SubCaller는 Caller를 확장합니다.
@보수
공개 무효 호출(문자열 str) {
System.out.println("SubCaller의 문자열 인스턴스");
}
}
}
실행 결과는
다음과 같이 코드 코드를 복사합니다.
22:27 $javaTestMain
SubCaller의 String 인스턴스
위 코드에는 Caller에 call 메소드 구현이 있습니다. SubCaller는 Caller를 상속하고 call 메소드 구현을 다시 작성합니다. Caller 유형의 callerSub 변수를 선언했지만 이 변수는 SubCaller 개체를 가리킵니다. 결과에 따르면 Caller의 call 메소드 대신 SubCaller의 call 메소드 구현을 호출하는 것을 알 수 있다. 이러한 결과가 나오는 이유는 동적 바인딩이 런타임에 발생하고 바인딩 프로세스 중에 호출할 호출 메서드 구현의 버전을 결정해야 하기 때문입니다.
확인하다
동적 바인딩은 javap를 이용하여 직접 검증할 수 없으며, 정적 바인딩을 수행하지 않는 것이 증명되면 동적 바인딩을 수행한다는 의미이다.
다음과 같이 코드 코드를 복사합니다.
22:27 $ javap -c TestMain
"TestMain.java"에서 컴파일됨
공개 클래스 TestMain {
공개 테스트메인();
암호:
0: 로드_0
1: Invokespecial #1 // 메소드 java/lang/Object."<init>":()V
4: 복귀
공개 정적 무효 메인(java.lang.String[]);
암호:
0: 새로운 #2 // 클래스 java/lang/String
3: 복제
4: Invokespecial #3 // 메소드 java/lang/String."<init>":()V
7: astore_1
8: 새로운 #4 // 클래스 TestMain$SubCaller
11: 복제
12: Invokespecial #5 // 메서드 TestMain$SubCaller."<init>":()V
15: astore_2
16: 로드_2
17: 로드_1
18: Invokevirtual #6 // 메소드 TestMain$Caller.call:(Ljava/lang/String;)V
21: 복귀
}
위의 결과와 같이 18: Invokevirtual #6 // Method TestMain$Caller.call:(Ljava/lang/String;)V 호출하는 서브루틴을 결정할 수 없기 때문에 TestMain$SubCaller.call 대신 TestMain$Caller.call입니다. 컴파일 시 클래스는 여전히 상위 클래스의 구현이므로 런타임 시 동적 바인딩을 통해서만 처리될 수 있습니다.
다시 로드하고 다시 작성하는 경우
다음 예제는 Caller 클래스에 두 가지 호출 메서드 오버로드가 있습니다. 더 복잡한 것은 SubCaller가 Caller를 통합하고 이 두 메서드를 재정의한다는 것입니다. 사실 이 상황은 위의 두 상황이 복합된 상황이다.
다음 코드는 먼저 정적 바인딩을 수행하여 매개변수가 String 객체인 호출 메서드를 결정한 다음 런타임에 동적 바인딩을 수행하여 하위 클래스 또는 상위 클래스의 호출 구현을 실행할지 여부를 결정합니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 TestMain {
공개 정적 무효 메인(String[] args) {
문자열 str = 새로운 문자열();
발신자 callerSub = new SubCaller();
callerSub.call(str);
}
정적 클래스 발신자 {
공개 무효 호출(객체 obj) {
System.out.println("호출자의 개체 인스턴스");
}
공개 무효 호출(문자열 str) {
System.out.println("발신자의 문자열 인스턴스");
}
}
정적 클래스 SubCaller는 Caller를 확장합니다.
@보수
공개 무효 호출(객체 obj) {
System.out.println("SubCaller의 개체 인스턴스");
}
@보수
공개 무효 호출(문자열 str) {
System.out.println("SubCaller의 문자열 인스턴스");
}
}
}
실행 결과는
다음과 같이 코드 코드를 복사합니다.
22:30 $javaTestMain
SubCaller의 String 인스턴스
확인하다
위에서 소개했으므로 여기에는 디컴파일 결과만 게시하겠습니다.
다음과 같이 코드 코드를 복사합니다.
22:30 $ javap -c TestMain
"TestMain.java"에서 컴파일됨
공개 클래스 TestMain {
공개 테스트메인();
암호:
0: 로드_0
1: Invokespecial #1 // 메소드 java/lang/Object."<init>":()V
4: 복귀
공개 정적 무효 메인(java.lang.String[]);
암호:
0: 새로운 #2 // 클래스 java/lang/String
3: 복제
4: Invokespecial #3 // 메소드 java/lang/String."<init>":()V
7: astore_1
8: 새로운 #4 // 클래스 TestMain$SubCaller
11: 복제
12: Invokespecial #5 // 메서드 TestMain$SubCaller."<init>":()V
15: astore_2
16: 로드_2
17: 로드_1
18: Invokevirtual #6 // 메소드 TestMain$Caller.call:(Ljava/lang/String;)V
21: 복귀
}
궁금한 질문
동적 바인딩을 사용할 수 없나요?
실제로 이론적으로 특정 메서드의 바인딩은 정적 바인딩을 통해 달성될 수도 있습니다. 예를 들어:
다음과 같이 코드 코드를 복사합니다.
공개 정적 무효 메인(String[] args) {
문자열 str = 새로운 문자열();
최종 발신자 callerSub = new SubCaller();
callerSub.call(str);
}
예를 들어 여기서 callerSub는 subCaller의 객체를 보유하고 있으며 callerSub 변수는 final이며 호출 메소드는 즉시 실행됩니다. 이론적으로 컴파일러는 코드를 충분히 분석하여 SubCaller의 호출 메소드를 호출해야 함을 알 수 있습니다.
그런데 왜 정적 바인딩이 없나요?
Caller가 호출 메소드를 구현하는 특정 프레임워크의 BaseCaller 클래스에서 상속되고 BaseCaller가 SuperCaller에서 상속된다고 가정합니다. 호출 메소드는 SuperCaller에서도 구현됩니다.
특정 프레임워크 1.0에서 BaseCaller 및 SuperCaller를 가정합니다.
다음과 같이 코드 코드를 복사합니다.
정적 클래스 SuperCaller {
공개 무효 호출(객체 obj) {
System.out.println("SuperCaller의 개체 인스턴스");
}
}
정적 클래스 BaseCaller는 SuperCaller를 확장합니다.
공개 무효 호출(객체 obj) {
System.out.println("BaseCaller의 개체 인스턴스");
}
}
우리는 프레임워크 1.0을 사용하여 이를 구현했습니다. Caller는 BaseCaller를 상속하고 super.call 메소드를 호출합니다.
다음과 같이 코드 코드를 복사합니다.
공개 클래스 TestMain {
공개 정적 무효 메인(String[] args) {
객체 obj = 새로운 객체();
SuperCaller callerSub = new SubCaller();
callerSub.call(obj);
}
정적 클래스 Caller는 BaseCaller를 확장합니다.
공개 무효 호출(객체 obj) {
System.out.println("호출자의 개체 인스턴스");
super.call(obj);
}
공개 무효 호출(문자열 str) {
System.out.println("발신자의 문자열 인스턴스");
}
}
정적 클래스 SubCaller는 Caller를 확장합니다.
@보수
공개 무효 호출(객체 obj) {
System.out.println("SubCaller의 개체 인스턴스");
}
@보수
공개 무효 호출(문자열 str) {
System.out.println("SubCaller의 문자열 인스턴스");
}
}
}
그런 다음 이 프레임워크의 버전 1.0을 기반으로 클래스 파일을 컴파일했습니다. 정적 바인딩이 위 호출자의 super.call이 BaseCaller.call로 구현되었음을 확인할 수 있다고 가정합니다.
그런 다음 BaseCaller가 이 프레임워크 버전 1.1에서 SuperCaller의 호출 메서드를 다시 작성하지 않는다고 가정합니다. 그런 다음 super.call이 버전에서 SuperCall을 사용해야 하기 때문에 정적으로 바인딩될 수 있는 호출 구현이 버전 1.1에서 문제를 일으킬 것이라는 위의 가정이 있습니다. 1.1. BaseCaller의 호출 메소드 구현이 정적 바인딩에 의해 결정된다고 가정하지 않고 호출 메소드 구현.
따라서 실제로 정적으로 바인딩할 수 있는 것들은 보안성과 일관성을 고려하여 단순히 동적으로 바인딩하는 경우도 있습니다.
최적화 영감을 얻었나요?
동적 바인딩은 런타임에 실행할 메서드 구현이나 변수의 버전을 결정해야 하므로 정적 바인딩보다 시간이 더 많이 걸립니다.
따라서 전체 디자인에 영향을 주지 않고 private, static 또는 final을 사용하여 메서드나 변수를 수정하는 것을 고려할 수 있습니다.