이 기사에서는 Java의 정적 디스패치 및 동적 디스패치에 대해 소개합니다(코드 예제). 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.
최근 JVM에 대한 지식을 복습하면서 정적 디스패치와 동적 디스패치에 대한 이해가 다소 혼란스러워서 직접 코드를 작성하고 지식을 분석에 통합해 보았습니다.
다음 코드가 있습니다. 각 코드는 무엇을 출력하나요?
package com.khlin.my.test; class Base { public static void foo() { System.out.println("Base.foo() invoked"); } public void bar(int c) { System.out.println("Base.bar(int) invoked"); } public void bar(Character c) { System.out.println("Base.bar(Character) invoked"); } public void baz(Object o) { System.out.println("Base.baz(Object) invoked"); } public void baz(Integer i) { System.out.println("Base.baz(Integer) invoked"); } } class Child extends Base { public static void foo() { System.out.println("Child.foo() invoked"); } public void bar(Character c) { System.out.println("Child.bar(Character) invoked"); } public void bar(char c) { System.out.println("Child.bar(char) invoked"); } } public class App { public static void main(String[] args) { Base child = new Child(); System.out.println("第1段输出:"); child.foo(); child.bar(new Character('C')); System.out.println("第2段输出:"); Object integer = new Integer(100); child.baz(integer); System.out.println("第3段输出:"); child.bar('C'); } }
코드 컴파일부터 메소드 호출까지 전체 과정을 간략하게 소개하겠습니다.
· Compile
먼저 첫 번째 단락의 출력을 살펴보세요. child.foo()가 상위 클래스 또는 하위 클래스의 정적 메소드를 호출하고 있습니까?
컴파일 단계에서 정적 디스패치가 발생합니다.
1 Base child = new Child();
객체를 생성할 때 위 그림과 같이 Base를 변수의 정적 유형(Static Type) 또는 겉보기 유형(Apparent Type)이라고 하며, 다음 Child를 변수의 실제 유형( 실제 유형).
메서드 실행 버전을 찾기 위해 정적 유형을 사용하는 모든 디스패치 작업을 정적 디스패치라고 합니다. 정적 디스패치의 일반적인 응용 프로그램은 컴파일 단계에서 발생하는 메서드 오버로딩이므로 정적으로 디스패치되도록 결정된 작업이 실제로 가상 머신에서 실행되지 않습니다.
메서드의 수신자(Reciever)와 메소드의 매개변수를 총칭하여 메소드의 변수라고 합니다. 디스패치는 몇 가지 유형의 변수를 기반으로 하느냐에 따라 단일 디스패치와 다중 디스패치로 나눌 수 있습니다.
정적 디스패치 중에 대상 메소드를 선택하는 기준은 두 가지입니다. 하나는 정적 유형이 Base인지 Child인지이고, 다른 하나는 메소드의 매개변수 유형입니다. 따라서 정적 디스패치는 다중 디스패치입니다.
다음으로 “첫 번째 문단 출력” 코드에서 생성된 명령어를 살펴보겠습니다. javap -v App.class 명령어를 통해 얻은 결과는 위의 분석과 일치하는 18행과 31행에서 두 명령어의 기호 참조를 볼 수 있습니다. 자식의 정적 유형은 Base이므로 메서드는 None을 통해 Base 클래스가 선택됩니다. 매개변수 및 문자 유형에 따라 각각 메소드 버전이 결정됩니다.
그러나 결국 둘의 동작은 다릅니다. child.foo()는 정적 유형인 Base의 foo()를 호출하는 반면, child.bar(new Character('C'))는 실제 유형인 Child를 호출합니다. 방법.
이유는 두 명령어가 다르기 때문입니다: Invokestatic과 Invokevirtual
Java 가상 머신은 5가지 메소드 호출 바이트코드 명령어를 제공합니다:
invokestatic: 정적 메소드 호출
invokespecial: 인스턴스 생성자 호출< 메서드 및 상위 클래스 메서드
invokevirtual: 모든 가상 메서드 호출
invokeinterface: 인터페이스 메서드를 호출한 다음 런타임에 이 인터페이스를 구현하는 객체를 결정합니다.
invokedynamic: 먼저 런타임에 이를 동적으로 해결합니다. 콜 포인트에서 참조하는 메서드 그런 다음 이전 4개 호출 명령어의 디스패치 로직이 JVM(Java Virtual Machine) 내에서 구체화되고, Invokedynamic은 사용자가 설정한 부팅 방법에 따라 결정됩니다.
구체적인 이유는 다음 단계(클래스 로딩 구문 분석)에서 다양한 명령어가 다르게 동작하기 때문입니다. 지금은 잠시 제쳐두고 두 번째 단락의 명령어 출력을 살펴보겠습니다.
정적 디스패치 중에는 메소드에 전달된 매개변수의 정적 유형에 따라 호출할 메소드 버전이 결정되는 것을 볼 수 있습니다. baz(Integer) 메소드가 있지만, 들어오는 매개변수 정수 정적 유형은 Object이므로 baz(Object)가 호출됩니다. 세 번째 단락의 지침 출력을 살펴보겠습니다. 기호 참조는 여전히 Base 클래스의 메서드여야 하지만(Child 클래스에 동일한 매개변수를 가진 bar(char c) 메서드가 있음에도 불구하고) Base 메소드에 동일한 매개변수(char 유형)가 없으면 오류가 보고되지 않나요? 어떤 메소드가 호출될까요?
컴파일러가 메서드의 오버로드된 버전을 결정할 수 있지만 많은 경우 이 오버로드된 버전은 "유일한" 버전이 아니며 "더 적절한" 버전만 결정할 수 있는 경우가 많습니다.
· 클래스 로딩 구문 분석 구문 분석 단계는 가상 머신이 상수 풀의 기호 참조를 직접 참조로 바꾸는 프로세스입니다.
invokestatic 및 Invokespecial 명령어로 메서드를 호출할 수 있는 한, 고유한 호출 버전은 구문 분석 단계에서 결정될 수 있습니다. 이 조건을 충족하는 네 가지 범주는 정적 메서드, 개인 메서드, 인스턴스 생성자 및 상위 클래스 메서드입니다. . 기호 참조는 클래스에 로드되어 메서드에 대한 직접 참조로 확인됩니다. 이러한 메서드는 비가상 메서드라고 할 수 있으며, 다른 메서드는 비가상 메서드라고 합니다(최종 메서드 제외).
invokevirtual 명령어를 사용하여 최종 수정된 메서드를 호출하지만 재정의할 수 없고 다른 버전이 없기 때문에 이 메서드도 비가상 메서드입니다.
출력의 첫 번째 단락으로 돌아가서 child.foo()는 호출 정적 명령이므로 구문 분석 단계에서 직접 참조로 대체되고 특정 클래스가 결정되므로 정적 유형 Base.foo() 호출됩니다.
그리고 child.bar(new Character('C'))는 Invokevirtual입니다. 이 단계에서는 호출된 메서드의 서명을 확인할 수 있지만 메서드 수신자의 실제 유형은 아직 확인할 수 없습니다. 동적 디스패치에 의해 결정됩니다. 클러스터 효과가 하나만 있기 때문에 동적 디스패치는 단일 디스패치입니다.
메서드 수신자의 실제 유형은 다음 단계에서 결정됩니다.
· 런타임 시 메소드 호출
런타임 시 실제 유형을 기반으로 메소드 실행 버전을 결정하는 디스패치 프로세스를 동적 디스패치라고 합니다.
최종 출력은 다음과 같습니다.
위 내용은 Java의 정적 디스패치 및 동적 디스패치 소개(코드 예)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!