Java 바이트 코드로 꽤 오랫동안 작업 하고이 문제에 대한 추가 연구를 한 후 내 결과를 요약 한 것입니다.
수퍼 생성자 또는 보조 생성자를 호출하기 전에 생성자에서 코드를 실행하십시오.
JPL (Java Programming Language)에서 생성자의 첫 번째 명령문은 수퍼 생성자 또는 동일한 클래스의 다른 생성자를 호출해야합니다. JBC (Java byte code)에는 해당되지 않습니다. 바이트 코드 내에서는 다음과 같은 경우 생성자 전에 코드를 실행하는 것이 합법적입니다.
- 이 코드 블록 다음에 호환 가능한 다른 생성자가 호출됩니다.
- 이 호출은 조건문 내에 없습니다.
- 이 생성자 호출 전에 생성 된 인스턴스의 필드를 읽지 않고 해당 메소드를 호출하지 않습니다. 이것은 다음 항목을 의미합니다.
수퍼 생성자 또는 보조 생성자를 호출하기 전에 인스턴스 필드 설정
앞에서 언급했듯이 다른 생성자를 호출하기 전에 인스턴스의 필드 값을 설정하는 것이 합법적입니다. 6 이전의 Java 버전에서이 "기능"을 이용할 수있는 레거시 해킹도 있습니다.
class Foo {
public String s;
public Foo() {
System.out.println(s);
}
}
class Bar extends Foo {
public Bar() {
this(s = "Hello World!");
}
private Bar(String helper) {
super();
}
}
이런 식으로 슈퍼 생성자가 호출되기 전에 필드를 설정할 수 있지만 더 이상 가능하지 않습니다. JBC에서는이 동작을 계속 구현할 수 있습니다.
수퍼 생성자 호출 분기
Java에서는 다음과 같은 생성자 호출을 정의 할 수 없습니다
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
그러나 Java 7u23까지 HotSpot VM의 검증기는이 확인을 놓쳤습니다. 이것이 가능했던 이유입니다. 이것은 여러 코드 생성 도구에서 일종의 핵으로 사용되었지만 더 이상 이와 같은 클래스를 구현하는 것은 합법적이지 않습니다.
후자는이 컴파일러 버전의 버그 일뿐입니다. 최신 컴파일러 버전에서는 다시 가능합니다.
생성자가없는 클래스 정의
Java 컴파일러는 항상 모든 클래스에 대해 하나 이상의 생성자를 구현합니다. Java 바이트 코드에서는 필요하지 않습니다. 리플렉션을 사용할 때에도 생성 할 수없는 클래스를 만들 수 있습니다. 그러나 sun.misc.Unsafe
계속 사용하면 그러한 인스턴스를 작성할 수 있습니다.
서명은 동일하지만 리턴 유형이 다른 메소드 정의
JPL에서 메소드는 이름 및 원시 매개 변수 유형으로 고유 한 것으로 식별됩니다. JBC에서는 원시 반품 유형이 추가로 고려됩니다.
이름이 아니라 유형별로 다른 필드를 정의하십시오.
클래스 파일은 다른 필드 유형을 선언하는 한 동일한 이름의 여러 필드를 포함 할 수 있습니다. JVM은 항상 필드를 이름 및 유형의 튜플이라고합니다.
선언되지 않은 확인 된 예외를 포착하지 않고 던져
Java 런타임 및 Java 바이트 코드는 확인 된 예외의 개념을 인식하지 못합니다. 확인 된 예외가 발생하면 항상 포착되거나 선언되었는지 확인하는 것은 Java 컴파일러뿐입니다.
람다 식 외부에서 동적 메서드 호출 사용
소위 동적 메소드 호출 은 Java의 람다 표현식뿐만 아니라 무엇이든 사용할 수 있습니다. 이 기능을 사용하면 예를 들어 런타임시 실행 로직을 전환 할 수 있습니다. JBC로 요약되는 많은 동적 프로그래밍 언어는 이 명령을 사용하여 성능 을 향상 시켰습니다 . Java 바이트 코드에서는 Java 7에서 람다 식을 에뮬레이션 할 수 있습니다. 여기서 JVM은 이미 명령을 이해하는 동안 컴파일러에서 동적 메서드 호출을 사용할 수 없습니다.
일반적으로 합법적이지 않은 식별자 사용
메서드 이름에 공백과 줄 바꿈을 사용하는 것을 좋아 한 적이 있습니까? 코드 검토를위한 JBC와 행운을 만드십시오. 식별자에 대한 유일한 불법 문자는 .
, ;
, [
와 /
. 또한, 방법은 이름이되지 않은 <init>
또는 <clinit>
포함 할 수 없습니다 <
와 >
.
final
매개 변수 또는 this
참조 재 할당
final
매개 변수는 JBC에 존재하지 않으므로 결과적으로 다시 지정할 수 있습니다. this
참조를 포함한 모든 매개 변수 는 단일 메소드 프레임 내의 this
색인 0
에서 참조 를 재 지정할 수있는 JVM 내의 간단한 배열에만 저장됩니다 .
final
필드 재 할당
최종 필드가 생성자 내에서 할당되는 한이 값을 다시 할당하거나 값을 전혀 할당하지 않는 것이 합법적입니다. 따라서 다음 두 생성자가 합법적입니다.
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
들어 static final
필드, 그것도 클래스 초기화 이외의 분야를 재 할당 할 수 있습니다.
생성자와 클래스 이니셜 라이저를 마치 메소드처럼 취급
이것은 개념적인 기능에 가깝지만 생성자는 일반 메소드와 JBC 내에서 다르게 취급되지 않습니다. 생성자가 다른 합법적 인 생성자를 호출하도록 보장하는 것은 JVM 검증 자뿐입니다. 그 외에는 생성자가 호출 <init>
되고 클래스 이니셜 라이저가 호출 되는 것은 Java 명명 규칙 일뿐 <clinit>
입니다. 이 차이점 외에도 메소드와 생성자의 표현은 동일합니다. Holger가 주석에서 지적했듯이 void
이러한 메소드를 호출 할 수는 없지만 인수가 아닌 클래스 이니셜 라이저가 아닌 반환 유형으로 생성자를 정의 할 수도 있습니다 .
비대칭 레코드를 만듭니다 * .
레코드를 만들 때
record Foo(Object bar) { }
javac는라는 단일 필드 bar
, 접근 자 메서드 bar()
및 단일 생성자를 사용하여 클래스 파일을 생성합니다 Object
. 또한에 대한 레코드 속성 bar
이 추가됩니다. 레코드를 수동으로 생성하면 다른 생성자 모양을 만들어 필드를 건너 뛰고 접근자를 다르게 구현할 수 있습니다. 동시에 리플렉션 API가 클래스가 실제 레코드를 나타낸다고 믿도록 만들 수 있습니다.
수퍼 메소드 호출 (Java 1.1까지)
그러나 이것은 Java 버전 1 및 1.1에서만 가능합니다. JBC에서 메소드는 항상 명시적인 대상 유형으로 전달됩니다. 이것은
class Foo {
void baz() { System.out.println("Foo"); }
}
class Bar extends Foo {
@Override
void baz() { System.out.println("Bar"); }
}
class Qux extends Bar {
@Override
void baz() { System.out.println("Qux"); }
}
뛰어 넘는 동안 Qux#baz
호출 하도록 구현할 수있었습니다 . 직접 수퍼 클래스보다 다른 수퍼 메소드 구현을 호출하기 위해 명시 적 호출을 정의 할 수는 있지만, 1.1 이후 Java 버전에서는 더 이상 영향을 미치지 않습니다. Java 1.1에서이 동작은 직접 수퍼 클래스의 구현 만 호출하는 것과 동일한 동작을 가능하게하는 플래그를 설정하여 제어되었습니다 .Foo#baz
Bar#baz
ACC_SUPER
동일한 클래스에서 선언 된 메소드의 비가 상 호출 정의
Java에서는 클래스를 정의 할 수 없습니다
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
위의 코드는 의 인스턴스에서 RuntimeException
when foo
이 호출 될 때 항상 발생 Bar
합니다. 에 정의 된 자체 메소드 Foo::foo
를 호출하기 위해 메소드 를 정의 할 수 없습니다 . 으로 비 개인 인스턴스 방법, 호출은 항상 가상입니다. 바이트 코드로, 하나는하지만 사용하는 호출을 정의 할 수 있습니다 직접 연결 연산 코드 에 메소드 호출 에 의 버전. 이 opcode는 일반적으로 수퍼 메소드 호출을 구현하는 데 사용되지만 설명 된 동작을 구현하기 위해 opcode를 재사용 할 수 있습니다. bar
Foo
bar
INVOKESPECIAL
bar
Foo::foo
Foo
세밀한 유형 주석
Java에서는 주석이 @Target
선언 한 주석 에 따라 주석이 적용됩니다 . 바이트 코드 조작을 사용하면이 컨트롤과 독립적으로 주석을 정의 할 수 있습니다. 또한 @Target
주석이 두 요소 모두에 적용 되더라도 매개 변수에 주석을 달지 않고 매개 변수 유형에 주석을 달 수 있습니다 .
유형 또는 해당 구성원에 대한 속성을 정의하십시오.
Java 언어 내에서는 필드, 메소드 또는 클래스에 대한 주석 만 정의 할 수 있습니다. JBC에서는 기본적으로 모든 정보를 Java 클래스에 임베드 할 수 있습니다. 그러나이 정보를 사용하기 위해 더 이상 Java 클래스로드 메커니즘에 의존 할 수 없지만 메타 정보를 직접 추출해야합니다.
오버플로 및 암시 적 byte
으로 short
, char
및 boolean
값 할당
후자의 기본 유형은 JBC에서 일반적으로 알려져 있지 않지만 배열 유형 또는 필드 및 메소드 디스크립터에 대해서만 정의됩니다. 바이트 코드 명령어 내에서 이름이 지정된 모든 유형은 32 비트의 공백을 사용하여을 나타낼 수 있습니다 int
. 공식적으로 만 int
, float
, long
및 double
유형 바이트 코드 내에 존재하는 모든 필요 JVM의 검증의 규정에 의한 명시 적 변환을.
모니터를 놓지 마십시오
synchronized
블록은 실제로 두 개의 문, 획득 한 하나의 모니터를 해제하기로 구성되어 있습니다. JBC에서는 릴리스하지 않고 구매할 수 있습니다.
참고 : 최근 HotSpot 구현 IllegalMonitorStateException
에서 메소드 자체가 예외로 종료되면 메소드 종료시 또는 암시 적으로 해제됩니다.
return
형식 초기화에 둘 이상의 문 추가
Java에서는 사소한 유형의 초기화 프로그램조차도
class Foo {
static {
return;
}
}
불법입니다. 바이트 코드에서 타입 이니셜 라이저는 다른 방법과 같이 취급됩니다. 즉, return 문은 어디에서나 정의 할 수 있습니다.
돌이킬 수없는 루프 만들기
Java 컴파일러는 루프를 Java 바이트 코드의 goto 문으로 변환합니다. 이러한 명령문은 Java 컴파일러가 절대로 반복 할 수없는 루프를 만드는 데 사용될 수 있습니다.
재귀 캐치 블록 정의
Java 바이트 코드에서 블록을 정의 할 수 있습니다.
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
synchronized
모니터를 해제하는 동안 예외가이 모니터 해제 지시로 리턴되는 Java에서 블록을 사용할 때 유사한 명령문이 내재적으로 작성됩니다 . 일반적으로 이러한 명령어에서는 예외가 발생하지 않지만 명령이 사용되지 않는 경우 (예 : 사용되지 않음 ThreadDeath
) 모니터는 계속 해제됩니다.
기본 방법을 호출
Java 컴파일러는 기본 메소드의 호출을 허용하기 위해 몇 가지 조건이 충족되어야합니다.
- 이 방법은 가장 구체적인 방법이어야합니다 ( 슈퍼 유형을 포함하여 모든 유형으로 구현되는 하위 인터페이스로 재정의 해서는 안 됨 ).
- 기본 메소드의 인터페이스 유형은 기본 메소드를 호출하는 클래스에 의해 직접 구현되어야합니다. 그러나 인터페이스가 인터페이스를
B
확장 A
하지만의 메소드를 재정의하지 않으면 A
메소드를 계속 호출 할 수 있습니다.
Java 바이트 코드의 경우 두 번째 조건 만 계산됩니다. 그러나 첫 번째는 관련이 없습니다.
그렇지 않은 인스턴스에서 수퍼 메소드를 호출하십시오. this
Java 컴파일러는의 인스턴스에서 수퍼 (또는 인터페이스 기본값) 메소드 만 호출 할 수 있습니다 this
. 그러나 바이트 코드에서는 다음과 유사한 유형의 인스턴스에서 super 메소드를 호출 할 수도 있습니다.
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
합성 멤버 액세스
Java 바이트 코드에서는 합성 멤버에 직접 액세스 할 수 있습니다. 예를 들어, 다음 예에서 다른 Bar
인스턴스 의 외부 인스턴스에 액세스 하는 방법을 고려하십시오 .
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
이것은 일반적으로 모든 합성 분야, 클래스 또는 방법에 해당됩니다.
비동기 일반 유형 정보 정의
Java 런타임은 일반 유형을 처리하지 않지만 (Java 컴파일러가 유형 삭제를 적용한 후)이 정보는 여전히 메타 정보로 컴파일 된 클래스에 연결되며 리플렉션 API를 통해 액세스 할 수 있습니다.
검증기는 이러한 메타 데이터 String
인코딩 된 값 의 일관성을 검사하지 않습니다 . 따라서 삭제와 일치하지 않는 일반 유형에 대한 정보를 정의 할 수 있습니다. 아쉽게도 다음과 같은 주장이 사실 일 수 있습니다.
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
또한 런타임 예외가 발생하도록 서명을 유효하지 않은 것으로 정의 할 수 있습니다. 이 예외는 정보가 게으르게 평가 될 때 처음으로 정보에 액세스 할 때 발생합니다. 오류가있는 주석 값과 유사합니다.
특정 방법에 대해서만 매개 변수 메타 정보 추가
Java 컴파일러를 사용하면 parameter
플래그가 활성화 된 클래스를 컴파일 할 때 매개 변수 이름 및 수정 자 정보를 포함 할 수 있습니다. 그러나 Java 클래스 파일 형식에서이 정보는 메소드별로 저장되므로 특정 메소드에 대해 이러한 메소드 정보 만 임베드 할 수 있습니다.
혼란스러운 것들과 JVM 충돌
예를 들어, Java 바이트 코드에서 모든 유형의 메소드를 호출하도록 정의 할 수 있습니다. 일반적으로 검증자는 유형이 그러한 방법을 알지 못하면 불만을 제기합니다. 그러나 배열에서 알 수없는 메소드를 호출하면 일부 JVM 버전에서 검증자가 이것을 놓치고 명령이 호출되면 JVM이 종료되는 버그를 발견했습니다. 이것은 거의 기능이 아니지만 기술적으로 javac 컴파일 Java 로는 불가능합니다 . Java에는 이중 유효성 검사가 있습니다. 첫 번째 유효성 검사는 Java 컴파일러에 의해 적용되고 두 번째 유효성 검사는 클래스가로드 될 때 JVM에 의해 적용됩니다. 컴파일러를 건너 뛰면 검증기의 유효성 검사에서 약점을 찾을 수 있습니다. 그러나 이것은 기능보다는 일반적인 진술입니다.
외부 클래스가 없을 때 생성자의 수신자 유형에 주석 달기
Java 8부터 내부 클래스의 비 정적 메소드 및 생성자는 수신자 유형을 선언하고 이러한 유형에 주석을 달 수 있습니다. 최상위 클래스의 생성자는 가장 많이 선언하지 않으므로 수신자 유형에 주석을 달 수 없습니다.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
그러나 AnnotatedType
표현을 반환 하므로 클래스 파일에 직접 생성자에 Foo
대한 유형 주석을 포함시킬 수 있습니다 Foo
.이 주석은 나중에 리플렉션 API에서 읽습니다.
사용되지 않은 / 레거시 바이트 코드 명령어 사용
다른 사람들이 그것을 지명했기 때문에 나는 그것을 포함시킬 것입니다. Java는 이전에는 JSR
and RET
문 으로 서브 루틴을 사용 하고 있었습니다. JBC는이 목적을 위해 자체 유형의 반송 주소도 알고있었습니다. 그러나 서브 루틴을 사용하면 정적 코드 분석이 지나치게 복잡 해져서 이러한 명령어가 더 이상 사용되지 않습니다. 대신 Java 컴파일러는 컴파일하는 코드를 복제합니다. 그러나 이것은 기본적으로 동일한 논리를 생성하므로 다른 것을 달성하기 위해 실제로 논리를 고려하지 않습니다. 마찬가지로 예를 들어NOOP
바이트 코드 명령은 Java 컴파일러에서 사용되지 않지만 실제로는 새로운 것을 달성 할 수는 없습니다. 문맥에서 지적한 바와 같이, 이러한 언급 된 "기능 지침"은 이제 법적인 opcode 세트에서 제거되어 기능을 더 적게 만듭니다.