Po dłuższej pracy z kodem bajtowym Javy i przeprowadzeniu dodatkowych badań w tej sprawie, oto podsumowanie moich ustaleń:
Wykonaj kod w konstruktorze przed wywołaniem super konstruktora lub konstruktora pomocniczego
W języku programowania Java (JPL) pierwsza instrukcja konstruktora musi być wywołaniem super konstruktora lub innego konstruktora tej samej klasy. Nie dotyczy to kodu bajtowego języka Java (JBC). W kodzie bajtowym wykonanie dowolnego kodu przed konstruktorem jest absolutnie uzasadnione, o ile:
- Po pewnym czasie po tym bloku kodu wywoływany jest inny zgodny konstruktor.
- To wywołanie nie znajduje się w instrukcji warunkowej.
- Przed wywołaniem konstruktora żadne pole skonstruowanej instancji nie jest odczytywane i żadna z jej metod nie jest wywoływana. Oznacza to następny element.
Ustaw pola instancji przed wywołaniem super konstruktora lub konstruktora pomocniczego
Jak wspomniano wcześniej, ustawienie wartości pola instancji przed wywołaniem innego konstruktora jest całkowicie legalne. Istnieje nawet stary hack, który umożliwia wykorzystanie tej „funkcji” w wersjach Java przed 6:
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();
}
}
W ten sposób pole można ustawić przed wywołaniem super konstruktora, co jednak nie jest już możliwe. W JBC to zachowanie nadal można zaimplementować.
Rozgałęzienie wywołania super konstruktora
W Javie nie jest możliwe zdefiniowanie wywołania konstruktora, takiego jak
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
Aż do wersji Java 7u23 weryfikator HotSpot VM pomijał tę kontrolę, dlatego było to możliwe. Było to wykorzystywane przez kilka narzędzi do generowania kodu jako rodzaj włamania, ale implementacja takiej klasy nie jest już legalna.
Ten ostatni był tylko błędem w tej wersji kompilatora. W nowszych wersjach kompilatora jest to znowu możliwe.
Zdefiniuj klasę bez konstruktora
Kompilator Java zawsze implementuje co najmniej jeden konstruktor dla dowolnej klasy. W kodzie bajtowym Java nie jest to wymagane. Pozwala to na tworzenie klas, których nie można zbudować nawet przy użyciu odbicia. Jednak używanie sun.misc.Unsafe
nadal pozwala na tworzenie takich instancji.
Zdefiniuj metody z identycznym podpisem, ale z innym typem zwracania
W JPL metoda jest identyfikowana jako unikalna na podstawie jej nazwy i surowych typów parametrów. W JBC dodatkowo brany jest pod uwagę surowy typ zwrotu.
Zdefiniuj pola, które nie różnią się nazwą, ale tylko typem
Plik klasy może zawierać kilka pól o tej samej nazwie, o ile deklarują one inny typ pola. JVM zawsze odwołuje się do pola jako do krotki zawierającej nazwę i typ.
Rzucaj niezadeklarowane, zaznaczone wyjątki bez przechwytywania ich
Środowisko wykonawcze Java i kod bajtowy Java nie są świadome koncepcji sprawdzanych wyjątków. Tylko kompilator języka Java sprawdza, czy sprawdzane wyjątki są zawsze albo przechwytywane, albo deklarowane, jeśli są zgłaszane.
Użyj dynamicznego wywołania metody poza wyrażeniami lambda
Tak zwane dynamiczne wywołanie metody może być używane do wszystkiego, nie tylko do wyrażeń lambda Javy. Korzystanie z tej funkcji umożliwia na przykład przełączanie logiki wykonywania w czasie wykonywania. Wiele dynamicznych języków programowania, które sprowadzają się do JBC, poprawiło swoją wydajność , korzystając z tej instrukcji. W kodzie bajtowym Javy można również emulować wyrażenia lambda w Javie 7, gdzie kompilator nie zezwalał jeszcze na jakiekolwiek użycie dynamicznego wywołania metody, podczas gdy maszyna JVM już zrozumiała instrukcję.
Użyj identyfikatorów, które zwykle nie są uważane za legalne
Czy kiedykolwiek chciałeś użyć spacji i podziału wiersza w nazwie swojej metody? Stwórz własny JBC i powodzenia w przeglądaniu kodu. Jedynymi nielegalnych znaków dla identyfikatorów są .
, ;
, [
i /
. Ponadto metody, które nie są nazwane <init>
lub <clinit>
nie mogą zawierać <
i >
.
Ponownie przypisz final
parametry lub this
odniesienie
final
parametry nie istnieją w JBC i mogą zostać ponownie przypisane. Każdy parametr, w tym this
odwołanie, jest przechowywany tylko w prostej tablicy w JVM, co pozwala na ponowne przypisanie this
odwołania w indeksie 0
w ramach pojedynczej ramki metody.
Ponownie przypisz final
pola
Dopóki w konstruktorze jest przypisane ostatnie pole, dozwolone jest ponowne przypisanie tej wartości lub nawet jej całkowity brak. Dlatego następujący dwaj konstruktorzy są legalni:
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
W przypadku static final
pól dozwolone jest nawet ponowne przypisanie pól poza inicjatorem klasy.
Traktuj konstruktory i inicjator klasy tak, jakby były metodami
Jest to raczej cecha koncepcyjna, ale konstruktorzy nie są traktowani inaczej w JBC niż zwykłe metody. Tylko weryfikator JVM zapewnia, że konstruktorzy wywołują innego legalnego konstruktora. Poza tym jest tylko konwencją nazewnictwa języka Java, która wymaga wywołania konstruktorów <init>
i wywołania inicjatora klasy <clinit>
. Poza tą różnicą reprezentacja metod i konstruktorów jest identyczna. Jak zauważył Holger w komentarzu, można nawet zdefiniować konstruktory z typami zwracanymi innymi niż void
lub inicjatorem klasy z argumentami, nawet jeśli nie jest możliwe wywołanie tych metod.
Twórz asymetryczne rekordy * .
Podczas tworzenia rekordu
record Foo(Object bar) { }
javac wygeneruje plik klasy z pojedynczym polem o nazwie bar
, metodą dostępu o nazwie bar()
i konstruktorem z pojedynczą nazwą Object
. Dodatkowo bar
dodawany jest atrybut rekordu dla . Poprzez ręczne generowanie rekordu można utworzyć inny kształt konstruktora, pominąć pole i inaczej zaimplementować akcesor. Jednocześnie nadal można przekonać interfejs API odbicia, że klasa reprezentuje rzeczywisty rekord.
Wywołaj dowolną super metodę (aż do Java 1.1)
Jest to jednak możliwe tylko dla wersji Java 1 i 1.1. W JBC metody są zawsze wysyłane w jawnym typie docelowym. Oznacza to, że dla
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"); }
}
można było zaimplementować Qux#baz
wywołanie Foo#baz
podczas przeskakiwania Bar#baz
. Chociaż nadal jest możliwe zdefiniowanie jawnego wywołania w celu wywołania innej implementacji super metody niż bezpośrednia superklasa, nie ma to już żadnego wpływu na wersje Java po 1.1. W Javie 1.1 to zachowanie było kontrolowane przez ustawienie ACC_SUPER
flagi, która umożliwiłaby to samo zachowanie, które wywołuje tylko bezpośrednią implementację superklasy.
Zdefiniuj niewirtualne wywołanie metody zadeklarowanej w tej samej klasie
W Javie nie ma możliwości zdefiniowania klasy
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
Powyższy kod zawsze spowoduje wywołanie RuntimeException
when foo
na wystąpieniu Bar
. Nie jest możliwe zdefiniowanie Foo::foo
metody wywoływania własnej bar
metody zdefiniowanej w Foo
. Ponieważ bar
jest to nieprywatna metoda wystąpienia, wywołanie jest zawsze wirtualne. Z kodu bajtowego, można jednak określić inwokację używać INVOKESPECIAL
kod operacji, która łączy się bezpośrednio ze bar
wywołanie metody w Foo::foo
celu Foo
„s wersji. Ten kod operacji jest zwykle używany do implementacji wywołań super metod, ale możesz ponownie użyć kodu operacji, aby zaimplementować opisane zachowanie.
Adnotacje drobnoziarniste
W Javie adnotacje są stosowane zgodnie z ich @Target
deklaracją. Dzięki manipulacji kodem bajtowym możliwe jest definiowanie adnotacji niezależnie od tej kontrolki. Możliwe jest również na przykład opisanie typu parametru bez opisywania parametru, nawet jeśli @Target
adnotacja dotyczy obu elementów.
Zdefiniuj dowolny atrybut dla typu lub jego elementów członkowskich
W języku Java możliwe jest tylko definiowanie adnotacji dla pól, metod lub klas. W JBC można w zasadzie osadzić dowolne informacje w klasach Java. Aby skorzystać z tych informacji, nie możesz już polegać na mechanizmie ładowania klas Java, ale musisz samodzielnie wyodrębnić metainformacje.
Przelewowe i niejawnie Przypisywanie byte
, short
, char
i boolean
wartości
Te ostatnie typy pierwotne nie są zwykle znane w JBC, ale są definiowane tylko dla typów tablic lub deskryptorów pól i metod. W instrukcjach kodu bajtowego wszystkie wymienione typy zajmują 32-bitową przestrzeń, co pozwala na ich przedstawienie jako int
. Oficjalnie tylko int
, float
, long
i double
typy kodu bajtowego istnieją w których wszyscy potrzebujemy wyraźnej konwersji przez rządy weryfikatora JVM za.
Nie zwalniaj monitora
synchronized
Blok jest w rzeczywistości składa się z dwóch sprawozdań, jeden do zdobycia i do uwolnienia jednego monitora. W JBC możesz go zdobyć bez zwalniania go.
Uwaga : w ostatnich implementacjach HotSpot prowadzi to zamiast tego do an IllegalMonitorStateException
na końcu metody lub do niejawnego wydania, jeśli metoda jest zakończona przez sam wyjątek.
Dodaj więcej niż jedną return
instrukcję do inicjatora typu
W Javie nawet trywialny inicjator typu, taki jak
class Foo {
static {
return;
}
}
jest nielegalne. W kodzie bajtowym inicjator typu jest traktowany jak każda inna metoda, tj. Instrukcje powrotu można zdefiniować w dowolnym miejscu.
Twórz nieredukowalne pętle
Kompilator Java konwertuje pętle na instrukcje goto w kodzie bajtowym Java. Takie instrukcje mogą służyć do tworzenia nieredukowalnych pętli, czego kompilator języka Java nigdy nie robi.
Zdefiniuj rekurencyjny blok catch
W kodzie bajtów Java można zdefiniować blok:
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
Podobna instrukcja jest tworzona niejawnie w przypadku użycia synchronized
bloku w języku Java, w którym każdy wyjątek podczas zwalniania monitora powraca do instrukcji zwalniania monitora. Zwykle żaden wyjątek nie powinien wystąpić w takiej instrukcji, ale gdyby tak się stało (np. Przestarzałe ThreadDeath
), monitor nadal zostałby zwolniony.
Wywołaj dowolną metodę domyślną
Kompilator Java wymaga spełnienia kilku warunków, aby umożliwić wywołanie metody domyślnej:
- Metoda musi być najbardziej szczegółowa (nie może być nadpisywana przez interfejs podrzędny, który jest implementowany przez dowolny typ, w tym nadtypy).
- Typ interfejsu metody domyślnej musi być implementowany bezpośrednio przez klasę, która wywołuje metodę domyślną. Jeśli jednak interfejs
B
rozszerza interfejs, A
ale nie przesłania metody w programie A
, nadal można wywołać metodę.
W przypadku kodu bajtowego Java liczy się tylko drugi warunek. Pierwsza jest jednak nieistotna.
Wywołaj super metodę na instancji, która nie jest this
Kompilator Java pozwala tylko na wywołanie metody super (lub domyślnej interfejsu) w instancjach this
. W kodzie bajtowym możliwe jest jednak również wywołanie metody super na instancji tego samego typu, podobnie jak poniżej:
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
Uzyskaj dostęp do członków syntetycznych
W kodzie bajtowym Javy możliwy jest bezpośredni dostęp do składowych syntetycznych. Na przykład zastanów się, jak w poniższym przykładzie Bar
uzyskuje się dostęp do zewnętrznej instancji innej instancji:
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
Zwykle dotyczy to każdego pola, klasy lub metody syntetycznej.
Zdefiniuj niezsynchronizowane informacje o typie ogólnym
Chociaż środowisko wykonawcze Java nie przetwarza typów ogólnych (po tym, jak kompilator języka Java zastosuje wymazywanie typów), te informacje są nadal dołączane do skompilowanej klasy jako informacje meta i udostępniane za pośrednictwem interfejsu API odbicia.
Weryfikator nie sprawdza spójności tych String
wartości zakodowanych w metadanych. W związku z tym można zdefiniować informacje o typach ogólnych, które nie pasują do usunięcia. W zasadzie prawdziwe mogą być następujące stwierdzenia:
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
Ponadto podpis można zdefiniować jako nieprawidłowy, tak aby zgłaszany był wyjątek czasu wykonywania. Ten wyjątek jest generowany, gdy informacje są uzyskiwane po raz pierwszy, ponieważ są oceniane leniwie. (Podobnie do wartości adnotacji z błędem).
Dołącz meta informacje o parametrach tylko dla niektórych metod
Kompilator Java umożliwia osadzanie nazwy parametru i informacji o modyfikatorze podczas kompilowania klasy z parameter
włączoną flagą. W formacie pliku klasy Java informacje te są jednak przechowywane według metody, co umożliwia osadzenie takich informacji o metodzie tylko dla niektórych metod.
Zepsuć rzeczy i mocno zepsuć JVM
Na przykład w kodzie bajtowym Java można zdefiniować wywołanie dowolnej metody dowolnego typu. Zwykle weryfikator narzeka, jeśli typ nie jest znany takiej metody. Jeśli jednak wywołasz nieznaną metodę na tablicy, znalazłem błąd w niektórych wersjach JVM, w którym weryfikator to przegapi, a Twoja JVM zakończy pracę po wywołaniu instrukcji. Nie jest to jednak żadna funkcja, ale technicznie jest to coś, co nie jest możliwe w skompilowanej javac Javie. Java ma swego rodzaju podwójną walidację. Pierwsza weryfikacja jest stosowana przez kompilator języka Java, a druga przez maszynę JVM podczas ładowania klasy. Pomijając kompilator, możesz znaleźć słaby punkt w walidacji weryfikatora. Jest to jednak raczej ogólne stwierdzenie niż funkcja.
Dodaj adnotacje do typu odbiornika konstruktora, gdy nie ma klasy zewnętrznej
Od wersji Java 8 niestatyczne metody i konstruktory klas wewnętrznych mogą deklarować typ odbiornika i dodawać do nich adnotacje. Konstruktorzy klas najwyższego poziomu nie mogą dodawać adnotacji do swojego typu odbiorcy, ponieważ większość z nich nie deklaruje.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Ponieważ Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
jednak zwraca AnnotatedType
reprezentację Foo
, możliwe jest dołączenie adnotacji typu dla Foo
konstruktora bezpośrednio w pliku klasy, gdzie te adnotacje są później odczytywane przez interfejs API odbicia.
Użyj nieużywanych / starszych instrukcji kodu bajtowego
Ponieważ inni go nazwali, dołączę to również. Wcześniej Java korzystała z podprogramów w instrukcjach JSR
i RET
. JBC znało nawet własny typ adresu zwrotnego do tego celu. Jednak użycie podprogramów spowodowało nadmierną komplikację statycznej analizy kodu, dlatego te instrukcje nie są już używane. Zamiast tego kompilator Java powieli kod, który kompiluje. Jednak w zasadzie tworzy to identyczną logikę, dlatego tak naprawdę nie uważam, aby osiągnąć coś innego. Podobnie możesz na przykład dodać rozszerzenieNOOP
instrukcja kodu bajtowego, która nie jest używana przez kompilator Javy, ale tak naprawdę nie pozwoliłaby na osiągnięcie czegoś nowego. Jak wskazano w kontekście, wspomniane „instrukcje dotyczące funkcji” zostały teraz usunięte z zestawu prawnych rozkazów, co czyni je jeszcze mniej ważnymi.