Funkcje kodu bajtowego nie są dostępne w języku Java

146

Czy są obecnie (Java 6) rzeczy, które można wykonać w kodzie bajtowym Java, a których nie można wykonać w języku Java?

Wiem, że oba są kompletne w Turingu, więc przeczytaj „może”, ponieważ „może działać znacznie szybciej / lepiej lub po prostu w inny sposób”.

Myślę o dodatkowych kodach bajtowych, takich jak invokedynamic, których nie można wygenerować za pomocą Javy, z wyjątkiem tego, że konkretny dotyczy przyszłej wersji.

Bart van Heukelom
źródło
3
Zdefiniuj „rzeczy”. Ostatecznie język Java i kod bajtowy Javy są kompletne w Turingu ...
Michael Borgwardt
2
To prawdziwe pytanie; czy jest jakaś korzyść z programowania w kodzie bajtowym, np. przy użyciu Jasmin zamiast Javy?
Peter Lawrey
2
Jak rolw asemblerze, którego nie można pisać w C ++.
Martijn Courteaux
1
Jest to bardzo słaby kompilator optymalizujący, którego nie można skompilować (x<<n)|(x>>(32-n))do rolinstrukcji.
Random832

Odpowiedzi:

62

O ile wiem, w kodach bajtowych obsługiwanych przez Javę 6 nie ma żadnych głównych funkcji, które nie są również dostępne z kodu źródłowego Javy. Głównym tego powodem jest oczywiście to, że kod bajtowy Java został zaprojektowany z myślą o języku Java.

Istnieją jednak pewne funkcje, które nie są tworzone przez współczesne kompilatory Java:

  • ACC_SUPERFlag :

    Jest to flaga, którą można ustawić dla klasy i określa, w jaki sposób invokespecialjest obsługiwany konkretny narożnik kodu bajtowego dla tej klasy. Jest ustawiana przez wszystkie współczesne kompilatory Javy (gdzie „modern” to> = Java 1.1, jeśli dobrze pamiętam) i tylko starożytne kompilatory Javy tworzyły pliki klas, w których ta opcja nie była ustawiona. Ta flaga istnieje tylko ze względu na kompatybilność wsteczną. Należy zauważyć, że począwszy od języka Java 7u51, ACC_SUPER jest całkowicie ignorowany ze względów bezpieczeństwa.

  • Plik jsr/ retbytecodes.

    Te kody bajtowe zostały użyte do implementacji podprogramów (głównie do implementacji finallybloków). Nie są już produkowane od wersji Java 6 . Powodem ich dezaktualizacji jest to, że bardzo komplikują statyczną weryfikację bez większego zysku (tj. Kod, który używa, można prawie zawsze ponownie zaimplementować z normalnymi skokami z bardzo niewielkim narzutem).

  • Posiadanie dwóch metod w klasie, które różnią się tylko zwracanym typem.

    Specyfikacja języka Java nie zezwala na stosowanie dwóch metod w tej samej klasie, jeśli różnią się one tylko typem zwracanym (tj. Ta sama nazwa, ta sama lista argumentów, ...). Jednak specyfikacja JVM nie ma takiego ograniczenia, więc plik klasy może zawierać dwie takie metody, po prostu nie ma możliwości utworzenia takiego pliku klasy przy użyciu zwykłego kompilatora Java. W tej odpowiedzi jest ładny przykład / wyjaśnienie .

Joachim Sauer
źródło
5
Mógłbym dodać inną odpowiedź, ale równie dobrze możemy uczynić twoją odpowiedź kanoniczną. Możesz chcieć wspomnieć, że podpis metody w kodzie bajtowym zawiera typ zwracany . Oznacza to, że możesz mieć dwie metody z dokładnie tymi samymi typami parametrów, ale różnymi typami zwracanymi. Zobacz tę dyskusję: stackoverflow.com/questions/3110014/is-this-valid-java/…
Adam Paynter
8
Możesz mieć nazwy klas, metod i pól zawierające prawie dowolny znak. Pracowałem nad jednym projektem, w którym „pola” miały w nazwach spacje i łączniki. : P
Peter Lawrey
3
@Peter: Mówiąc o znakach systemu plików, natknąłem się na obfuskatora, który zmienił nazwę klasy na, aa inną na Awewnątrz pliku JAR. Zajęło mi około pół godziny rozpakowywania na komputerze z systemem Windows, zanim zdałem sobie sprawę, gdzie są brakujące klasy. :)
Adam Paynter
3
@JoachimSauer: parafrazując JVM Spec, strona 75: nazwy klasy, metody, pola i zmienne lokalne mogą zawierać dowolny znak z wyjątkiem '.', ';', '[', lub '/'. Nazwy metod są takie same, ale nie mogą też zawierać '<'lub '>'. (Z godnymi uwagi wyjątkami <init>i <clinit>na przykład konstruktorami statycznymi.) Powinienem zwrócić uwagę, że jeśli ściśle przestrzegasz specyfikacji, nazwy klas są w rzeczywistości znacznie bardziej ograniczone, ale ograniczenia nie są wymuszane.
leviathanbadger
3
@JoachimSauer: także mój własny nieudokumentowany dodatek: język java zawiera znak "throws ex1, ex2, ..., exn"jako część sygnatur metod; nie możesz dodawać klauzul wyrzucających wyjątki do przesłoniętych metod. ALE, JVM nie może się mniej przejmować. Tak więc tylko finalmetody są naprawdę gwarantowane przez JVM jako wolne od wyjątków - oczywiście oprócz RuntimeExceptions i Errors. To tyle, jeśli chodzi o sprawdzoną obsługę wyjątków: D
leviathanbadger
401

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.Unsafenadal 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 finalparametry lub thisodniesienie

finalparametry nie istnieją w JBC i mogą zostać ponownie przypisane. Każdy parametr, w tym thisodwołanie, jest przechowywany tylko w prostej tablicy w JVM, co pozwala na ponowne przypisanie thisodwołania w indeksie 0w ramach pojedynczej ramki metody.

Ponownie przypisz finalpola

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 finalpó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ż voidlub 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 bardodawany 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#bazwywołanie Foo#bazpodczas 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_SUPERflagi, 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 RuntimeExceptionwhen foona wystąpieniu Bar. Nie jest możliwe zdefiniowanie Foo::foometody wywoływania własnej bar metody zdefiniowanej w Foo. Ponieważ barjest to nieprywatna metoda wystąpienia, wywołanie jest zawsze wirtualne. Z kodu bajtowego, można jednak określić inwokację używać INVOKESPECIALkod operacji, która łączy się bezpośrednio ze barwywołanie metody w Foo::foocelu 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 @Targetdeklaracją. 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 @Targetadnotacja 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, chari booleanwartoś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, longi doubletypy kodu bajtowego istnieją w których wszyscy potrzebujemy wyraźnej konwersji przez rządy weryfikatora JVM za.

Nie zwalniaj monitora

synchronizedBlok 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 IllegalMonitorStateExceptionna końcu metody lub do niejawnego wydania, jeśli metoda jest zakończona przez sam wyjątek.

Dodaj więcej niż jedną returninstrukcję 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 synchronizedbloku 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:

  1. 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).
  2. Typ interfejsu metody domyślnej musi być implementowany bezpośrednio przez klasę, która wywołuje metodę domyślną. Jeśli jednak interfejs Brozszerza interfejs, Aale 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 Baruzyskuje 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 Stringwartoś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 parameterwłą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 AnnotatedTypereprezentację Foo, możliwe jest dołączenie adnotacji typu dla Fookonstruktora 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 JSRi 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ć rozszerzenieNOOPinstrukcja 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.

Rafael Winterhalter
źródło
3
Jeśli chodzi o nazwy metod, możesz mieć więcej niż jedną <clinit>metodę, definiując metody z nazwą, <clinit>ale akceptując parametry lub mające voidtyp niezwracający. Ale te metody nie są zbyt przydatne, JVM je zignoruje, a kod bajtowy nie może ich wywołać. Jedynym zastosowaniem byłoby zmylenie czytelników.
Holger
2
Właśnie odkryłem, że JVM Oracle wykrywa niewydany monitor przy wyjściu metody i generuje komunikat, IllegalMonitorStateExceptionjeśli pominąłeś monitorexitinstrukcję. A w przypadku wyjątkowego wyjścia z metody, które nie powiodło się monitorexit, po cichu resetuje monitor.
Holger
1
@Holger - tego nie wiedziałem, wiem, że było to możliwe przynajmniej we wcześniejszych maszynach JVM, JRockit ma nawet własny handler do tego typu implementacji. Zaktualizuję wpis.
Rafael Winterhalter
1
Cóż, specyfikacja JVM nie nakazuje takiego zachowania. Po prostu odkryłem to, ponieważ próbowałem utworzyć wiszącą wewnętrzną blokadę za pomocą takiego niestandardowego kodu bajtowego.
Holger
3
Ok, znalazłem odpowiednią specyfikację : „ Blokowanie strukturalne to sytuacja, w której podczas wywołania metody każde wyjście na danym monitorze pasuje do poprzedniego wpisu na tym monitorze. Ponieważ nie ma pewności, że cały kod przesłany do Wirtualnej Maszyny Javy będzie wykonywać blokowanie strukturalne, implementacje Wirtualnej Maszyny Javy są dozwolone, ale nie muszą egzekwować obu poniższych dwóch reguł gwarantujących blokowanie strukturalne. … ”
Holger
14

Oto kilka funkcji, które można wykonać w kodzie bajtowym Java, ale nie w kodzie źródłowym Java:

  • Zgłaszanie sprawdzonego wyjątku z metody bez deklarowania, że ​​metoda go zgłasza. Wyjątki zaznaczone i niezaznaczone są sprawdzane tylko przez kompilator Java, a nie przez JVM. Z tego powodu Scala może na przykład rzucać sprawdzone wyjątki od metod bez ich deklarowania. Chociaż w przypadku generycznych języków Java istnieje obejście zwane podstępnym rzutem .

  • Posiadanie dwóch metod w klasie, które różnią się tylko typem zwracania, jak już wspomniano w odpowiedzi Joachima : Specyfikacja języka Java nie zezwala na dwie metody w tej samej klasie, jeśli różnią się one tylko typem zwracania (tj. Ta sama nazwa, ta sama lista argumentów, ...). Jednak specyfikacja JVM nie ma takiego ograniczenia, więc plik klasy może zawierać dwie takie metody, po prostu nie ma możliwości utworzenia takiego pliku klasy przy użyciu zwykłego kompilatora Java. W tej odpowiedzi jest ładny przykład / wyjaśnienie .

Esko Luontola
źródło
4
Zauważ, że nie jest to sposób, aby zrobić pierwszą rzeczą w Javie. Czasami nazywa się to podstępnym rzutem .
Joachim Sauer
Teraz to podstępne! : D Dzięki za udostępnienie.
Esko Luontola
Myślę, że możesz również użyć Thread.stop(Throwable)do podstępnego rzutu. Zakładam jednak, że ten już połączony jest szybszy.
Bart van Heukelom
2
Nie można utworzyć instancji bez wywołania konstruktora w kodzie bajtowym Java. Weryfikator odrzuci każdy kod, który próbuje użyć niezainicjowanej instancji. Implementacja deserializacji obiektów używa pomocników kodu natywnego do tworzenia wystąpień bez wywoływania konstruktora.
Holger
W przypadku klasy Foo rozszerzającej Object nie można było utworzyć wystąpienia Foo przez wywołanie konstruktora zadeklarowanego w Object. Weryfikator odmówiłby tego. Mógłbyś stworzyć taki konstruktor za pomocą Java's ReflectionFactory, ale nie jest to funkcja kodu bajtowego, ale realizowana przez Jni. Twoja odpowiedź jest zła, a Holger ma rację.
Rafael Winterhalter
8
  • GOTOmoże być używany z etykietami do tworzenia własnych struktur kontrolnych (innych niż for whileitp.)
  • Możesz przesłonić thiszmienną lokalną wewnątrz metody
  • Łącząc oba możesz stworzyć kod bajtowy zoptymalizowany pod kątem wywołań końcowych (robię to w JCompilo )

Jako punkt pokrewny możesz pobrać nazwęparametru dla metod skompilowanych z debugowaniem ( Paranamer robi to czytając kod bajtowy

Daniel Worthington-Bodart
źródło
Jak oceniasz overridetę lokalną zmienną?
Michael
2
Zastąpienie @Michaela to zbyt mocne słowo. Na poziomie kodu bajtowego dostęp do wszystkich zmiennych lokalnych uzyskuje się za pomocą indeksu liczbowego i nie ma różnicy między zapisem do istniejącej zmiennej a inicjalizacją nowej zmiennej (z rozłącznym zakresem), w obu przypadkach jest to po prostu zapis do zmiennej lokalnej. thisZmienna posiada wskaźnik zera, ale oprócz tego, że wstępnie inicjalizowane thisodniesienia podczas wprowadzania sposobu przykład, tylko lokalna zmienna. Możesz więc zapisać do niego inną wartość, która może działać jak zakończenie thiszakresu lub zmiana thiszmiennej, w zależności od tego, jak jej używasz.
Holger,
Widzę! Więc naprawdę można to thiszmienić? Myślę, że to tylko słowo „override” sprawiło, że zacząłem się zastanawiać, co dokładnie oznacza.
Michael,
5

Może sekcja 7A w tym dokumencie jest interesująca, chociaż dotyczy raczej pułapek związanych z kodem bajtowym, a nie funkcjami kodu bajtowego .

eljenso
źródło
Ciekawa lektura, ale nie wygląda na to, żeby ktoś chciał (ab) użyć którejkolwiek z tych rzeczy.
Bart van Heukelom
4

W języku Java pierwsza instrukcja w konstruktorze musi być wywołaniem konstruktora superklasy. Kod bajtowy nie ma tego ograniczenia, zamiast tego obowiązuje zasada, że ​​konstruktor superklasy lub inny konstruktor w tej samej klasie musi zostać wywołany dla obiektu przed uzyskaniem dostępu do elementów członkowskich. Powinno to pozwolić na większą swobodę, na przykład:

  • Utwórz instancję innego obiektu, zapisz go w zmiennej lokalnej (lub stosie) i przekaż jako parametr do konstruktora superklasy, zachowując referencję w tej zmiennej do innego użytku.
  • Wywołaj różne inne konstruktory na podstawie warunku. To powinno być możliwe: Jak warunkowo wywołać inny konstruktor w Javie?

Nie testowałem ich, więc proszę, popraw mnie, jeśli się mylę.

msell
źródło
Możesz nawet ustawić członków instancji przed wywołaniem jej konstruktora nadklasy. Odczytywanie pól lub wywoływanie metod nie jest jednak wcześniej możliwe.
Rafael Winterhalter
3

Coś, co można zrobić z kodem bajtowym, a nie zwykłym kodem Java, to wygenerowanie kodu, który można załadować i uruchomić bez kompilatora. Wiele systemów ma JRE zamiast JDK i jeśli chcesz generować kod dynamicznie, może być lepiej, jeśli nie łatwiej, wygenerować kod bajtowy zamiast kodu Javy, zanim będzie można go użyć.

Peter Lawrey
źródło
6
Ale wtedy po prostu pomijasz kompilator, nie tworząc czegoś, czego nie można wyprodukować za pomocą kompilatora (jeśli był dostępny).
Bart van Heukelom
2

Napisałem optymalizator kodu bajtowego, gdy byłem I-Play (został zaprojektowany w celu zmniejszenia rozmiaru kodu dla aplikacji J2ME). Jedną z dodanych przeze mnie funkcji była możliwość użycia wbudowanego kodu bajtowego (podobnie do wbudowanego języka asemblera w C ++). Udało mi się zmniejszyć rozmiar funkcji, która była częścią metody bibliotecznej, używając instrukcji DUP, ponieważ potrzebuję wartości dwukrotnie. Miałem też instrukcje zerobajtowe (jeśli wywołujesz metodę, która pobiera char i chcesz przekazać int, o której wiesz, że nie musi być rzutowana, dodałem int2char (var), aby zastąpić char (var) i usunąłby instrukcja i2c, aby zmniejszyć rozmiar kodu. Zrobiłem również, że float a = 2,3; float b = 3,4; float c = a + b; i to zostanie przekonwertowane na stały punkt (szybciej, a niektóre J2ME nie obsługuje zmiennoprzecinkowe).

nharding
źródło
2

W Javie, jeśli spróbujesz zastąpić metodę publiczną metodą chronioną (lub jakimkolwiek innym ograniczeniem dostępu), pojawi się błąd: „próbuję przypisać słabsze uprawnienia dostępu”. Jeśli zrobisz to z kodem bajtowym JVM, weryfikator nie będzie z tym problemem i możesz wywołać te metody za pośrednictwem klasy nadrzędnej, tak jakby były publiczne.

Joseph Sible - przywróć Monikę
źródło