Setelah bekerja dengan kode byte Java cukup lama dan melakukan beberapa penelitian tambahan tentang masalah ini, berikut adalah ringkasan dari temuan saya:
Jalankan kode dalam konstruktor sebelum memanggil konstruktor super atau konstruktor tambahan
Dalam bahasa pemrograman Java (JPL), pernyataan pertama konstruktor harus berupa doa dari konstruktor super atau konstruktor lain dari kelas yang sama. Ini tidak benar untuk kode byte Java (JBC). Dalam kode byte, sangat sah untuk mengeksekusi kode apa pun sebelum konstruktor, selama:
- Konstruktor lain yang kompatibel dipanggil beberapa saat setelah blok kode ini.
- Panggilan ini tidak dalam pernyataan bersyarat.
- Sebelum panggilan konstruktor ini, tidak ada bidang instance yang dibangun dibaca dan tidak ada metode yang dipanggil. Ini menyiratkan item berikutnya.
Setel bidang contoh sebelum memanggil konstruktor super atau konstruktor bantu
Seperti disebutkan sebelumnya, sangat sah untuk menetapkan nilai bidang instance sebelum memanggil konstruktor lain. Bahkan ada hack warisan yang membuatnya dapat mengeksploitasi "fitur" ini dalam versi Java sebelum 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();
}
}
Dengan cara ini, bidang dapat diatur sebelum konstruktor super dipanggil yang bagaimanapun tidak mungkin lagi. Di JBC, perilaku ini masih bisa diimplementasikan.
Cabang panggilan konstruktor super
Di Jawa, tidak mungkin mendefinisikan seperti panggilan konstruktor
class Foo {
Foo() { }
Foo(Void v) { }
}
class Bar() {
if(System.currentTimeMillis() % 2 == 0) {
super();
} else {
super(null);
}
}
Hingga Java 7u23, verifier HotSpot VM tidak melewatkan pemeriksaan ini, itulah mengapa itu mungkin. Ini digunakan oleh beberapa alat pembuat kode sebagai semacam peretasan tetapi tidak lagi sah untuk mengimplementasikan kelas seperti ini.
Yang terakhir hanyalah bug dalam versi kompiler ini. Dalam versi kompiler yang lebih baru, ini dimungkinkan lagi.
Tentukan kelas tanpa konstruktor
Kompiler Java akan selalu menerapkan setidaknya satu konstruktor untuk kelas apa pun. Dalam kode byte Java, ini tidak diperlukan. Ini memungkinkan pembuatan kelas yang tidak dapat dibangun bahkan ketika menggunakan refleksi. Namun, menggunakan sun.misc.Unsafe
masih memungkinkan untuk pembuatan instance seperti itu.
Tetapkan metode dengan tanda tangan yang identik tetapi dengan tipe pengembalian yang berbeda
Dalam JPL, metode diidentifikasi sebagai unik dengan namanya dan tipe parameter mentahnya. Di JBC, jenis pengembalian mentah juga dipertimbangkan.
Tetapkan bidang yang tidak berbeda dengan nama tetapi hanya berdasarkan jenis
File kelas dapat berisi beberapa bidang dengan nama yang sama selama mereka menyatakan jenis bidang yang berbeda. JVM selalu merujuk ke bidang sebagai tupel nama dan tipe.
Lemparkan pengecualian yang tidak dideklarasikan tanpa menangkapnya
Java runtime dan kode byte Java tidak mengetahui konsep pengecualian yang diperiksa. Hanya kompiler Java yang memverifikasi bahwa pengecualian yang diperiksa selalu ditangkap atau dideklarasikan jika dilempar.
Gunakan doa metode dinamis di luar ekspresi lambda
Apa yang disebut pemanggilan metode dinamis dapat digunakan untuk apa saja, tidak hanya untuk ekspresi lambda Java. Menggunakan fitur ini memungkinkan misalnya untuk menonaktifkan logika eksekusi saat runtime. Banyak bahasa pemrograman dinamis yang bermuara pada JBC meningkatkan kinerja mereka dengan menggunakan instruksi ini. Dalam kode byte Java, Anda juga bisa meniru ekspresi lambda di Java 7 di mana kompiler belum memungkinkan untuk penggunaan doa metode dinamis sementara JVM sudah memahami instruksi.
Gunakan pengidentifikasi yang biasanya tidak dianggap sah
Pernah membayangkan menggunakan spasi dan pemisah garis dalam nama metode Anda? Buat JBC Anda sendiri dan semoga sukses untuk review kode. Satu-satunya karakter ilegal untuk pengidentifikasi adalah .
, ;
, [
dan /
. Selain itu, metode yang tidak disebutkan namanya <init>
atau <clinit>
tidak dapat berisi <
dan >
.
Tetapkan ulang final
parameter atau this
referensi
final
parameter tidak ada di JBC dan akibatnya dapat dipindahkan. Setiap parameter, termasuk this
referensi hanya disimpan dalam array sederhana dalam JVM yang memungkinkan untuk menetapkan kembali this
referensi pada indeks 0
dalam kerangka metode tunggal.
Tetapkan ulang final
bidang
Selama bidang terakhir diberikan dalam konstruktor, adalah sah untuk menetapkan kembali nilai ini atau bahkan tidak memberikan nilai sama sekali. Oleh karena itu, dua konstruktor berikut ini legal:
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
Untuk static final
bidang, bahkan diizinkan untuk menetapkan kembali bidang di luar penginisialisasi kelas.
Perlakukan konstruktor dan inisialisasi kelas seolah-olah mereka adalah metode
Ini lebih merupakan fitur konseptual tetapi konstruktor tidak diperlakukan secara berbeda dalam JBC daripada metode normal. Hanya verifikasi JVM yang memastikan bahwa konstruktor memanggil konstruktor hukum lain. Selain itu, itu hanya konvensi penamaan Java bahwa konstruktor harus dipanggil <init>
dan bahwa initializer kelas dipanggil <clinit>
. Selain perbedaan ini, representasi metode dan konstruktor identik. Seperti yang ditunjukkan Holger dalam komentar, Anda bahkan dapat mendefinisikan konstruktor dengan tipe kembali selain void
atau penginisialisasi kelas dengan argumen, meskipun tidak mungkin untuk memanggil metode ini.
Buat catatan asimetris * .
Saat membuat catatan
record Foo(Object bar) { }
javac akan menghasilkan file kelas dengan bidang tunggal bernama bar
, metode accessor bernama bar()
dan konstruktor mengambil satu Object
. Selain itu, atribut catatan untuk bar
ditambahkan. Dengan membuat catatan secara manual, dimungkinkan untuk membuat, bentuk konstruktor yang berbeda, untuk melewati bidang dan mengimplementasikan accessor secara berbeda. Pada saat yang sama, masih dimungkinkan untuk membuat API refleksi percaya bahwa kelas mewakili catatan aktual.
Panggil metode super apa pun (hingga Java 1.1)
Namun, ini hanya mungkin untuk Java versi 1 dan 1.1. Di JBC, metode selalu dikirim pada tipe target eksplisit. Ini berarti untuk
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"); }
}
itu mungkin untuk diterapkan Qux#baz
untuk memanggil Foo#baz
sambil melompati Bar#baz
. Meskipun masih mungkin untuk menetapkan permintaan eksplisit untuk memanggil implementasi metode super lain dari kelas super langsung, ini tidak lagi memiliki efek dalam versi Java setelah 1.1. Di Java 1.1, perilaku ini dikontrol dengan menetapkan ACC_SUPER
bendera yang akan memungkinkan perilaku yang sama yang hanya memanggil implementasi kelas super langsung.
Tentukan panggilan non-virtual dari metode yang dideklarasikan di kelas yang sama
Di Jawa, tidak mungkin untuk mendefinisikan kelas
class Foo {
void foo() {
bar();
}
void bar() { }
}
class Bar extends Foo {
@Override void bar() {
throw new RuntimeException();
}
}
Kode di atas akan selalu menghasilkan RuntimeException
saat foo
dipanggil pada instance dari Bar
. Tidak mungkin mendefinisikan Foo::foo
metode untuk memanggil metode sendiri bar
yang didefinisikan dalam Foo
. Seperti bar
metode contoh non-pribadi, panggilan selalu virtual. Namun, dengan kode byte, seseorang dapat menentukan permohonan untuk menggunakan INVOKESPECIAL
opcode yang secara langsung menautkan bar
metode panggilan Foo::foo
ke Foo
versi. Opcode ini biasanya digunakan untuk menerapkan pemanggilan metode super tetapi Anda dapat menggunakan kembali opcode untuk menerapkan perilaku yang dijelaskan.
Anotasi jenis butiran halus
Di Jawa, anotasi diterapkan sesuai dengan @Target
anotasi yang dideklarasikan. Menggunakan manipulasi kode byte, dimungkinkan untuk menentukan anotasi secara independen dari kontrol ini. Juga, misalnya dimungkinkan untuk membuat anotasi tipe parameter tanpa membuat anotasi parameter bahkan jika @Target
anotasi berlaku untuk kedua elemen.
Tetapkan atribut apa pun untuk suatu tipe atau anggotanya
Di dalam bahasa Java, hanya dimungkinkan untuk mendefinisikan anotasi untuk bidang, metode atau kelas. Di JBC, Anda pada dasarnya dapat menanamkan informasi apa pun ke dalam kelas Java. Untuk memanfaatkan informasi ini, Anda tidak dapat lagi mengandalkan mekanisme pemuatan kelas Java tetapi Anda perlu mengekstrak informasi meta sendiri.
Overflow dan secara implisit menetapkan byte
, short
, char
dan boolean
nilai-nilai
Tipe primitif terakhir biasanya tidak dikenal di JBC tetapi hanya didefinisikan untuk tipe array atau untuk deskriptor bidang dan metode. Dalam instruksi kode byte, semua jenis yang disebutkan mengambil ruang 32 bit yang memungkinkan untuk mewakili mereka sebagai int
. Secara resmi, hanya int
, float
, long
dan double
jenis ada dalam kode byte mana semua kebutuhan konversi eksplisit oleh aturan verifier JVM.
Tidak melepaskan monitor
Sebuah synchronized
blok sebenarnya terdiri dari dua pernyataan, satu untuk memperoleh dan satu untuk melepaskan monitor. Di JBC, Anda dapat memperoleh satu tanpa melepaskannya.
Catatan : Dalam implementasi terbaru dari HotSpot, ini malah mengarah IllegalMonitorStateException
pada akhir metode atau rilis implisit jika metode tersebut diakhiri oleh pengecualian itu sendiri.
Tambahkan lebih dari satu return
pernyataan ke inisialisasi tipe
Di Jawa, bahkan initializer jenis sepele seperti
class Foo {
static {
return;
}
}
itu ilegal. Dalam kode byte, jenis penginisialisasi diperlakukan sama seperti metode lain, yaitu pernyataan pengembalian dapat didefinisikan di mana saja.
Buat loop tereduksi
Kompiler Java mengubah loop ke pernyataan goto dalam kode byte Java. Pernyataan seperti itu dapat digunakan untuk membuat loop tereduksi, yang tidak pernah dilakukan oleh kompiler Java.
Tentukan blok tangkapan rekursif
Dalam kode byte Java, Anda dapat mendefinisikan sebuah blok:
try {
throw new Exception();
} catch (Exception e) {
<goto on exception>
throw Exception();
}
Pernyataan serupa dibuat secara implisit saat menggunakan synchronized
blok di Jawa di mana pengecualian saat melepaskan monitor kembali ke instruksi untuk melepaskan monitor ini. Biasanya, tidak ada pengecualian yang terjadi pada instruksi seperti itu tetapi jika itu akan (mis. Usang ThreadDeath
), monitor masih akan dirilis.
Panggil metode default apa saja
Kompiler Java memerlukan beberapa kondisi yang harus dipenuhi untuk memungkinkan pemanggilan metode default:
- Metode harus yang paling spesifik (tidak boleh diganti oleh sub antarmuka yang diimplementasikan oleh jenis apa pun , termasuk tipe super).
- Jenis antarmuka metode default harus diimplementasikan secara langsung oleh kelas yang memanggil metode default. Namun, jika antarmuka
B
memperluas antarmuka A
tetapi tidak mengesampingkan metode A
, metode tersebut masih dapat dipanggil.
Untuk kode byte Java, hanya kondisi kedua yang diperhitungkan. Namun yang pertama tidak relevan.
Meminta metode super pada contoh yang tidak this
Kompiler Java hanya memungkinkan untuk memanggil metode super (atau antarmuka standar) pada contoh this
. Dalam kode byte, bagaimanapun juga dimungkinkan untuk memanggil metode super pada instance dengan tipe yang sama seperti berikut ini:
class Foo {
void m(Foo f) {
f.super.toString(); // calls Object::toString
}
public String toString() {
return "foo";
}
}
Akses anggota sintetis
Dalam kode byte Java, dimungkinkan untuk mengakses anggota sintetis secara langsung. Misalnya, perhatikan bagaimana dalam contoh berikut ini instance luar dari Bar
instance lain diakses:
class Foo {
class Bar {
void bar(Bar bar) {
Foo foo = bar.Foo.this;
}
}
}
Ini umumnya berlaku untuk bidang, kelas, atau metode sintetis apa pun.
Tetapkan informasi jenis umum tidak sinkron
Sementara Java runtime tidak memproses tipe generik (setelah compiler Java menerapkan tipe erasure), informasi ini masih dikaitkan dengan kelas yang dikompilasi sebagai informasi meta dan dapat diakses melalui API refleksi.
Verifier tidak memeriksa konsistensi dari String
nilai-nilai meta data yang disandikan ini. Oleh karena itu dimungkinkan untuk mendefinisikan informasi tentang tipe generik yang tidak cocok dengan penghapusan. Sebagai rahasia, pernyataan berikut ini bisa benar:
Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
Juga, tanda tangan dapat didefinisikan sebagai tidak valid sehingga pengecualian runtime dilemparkan. Pengecualian ini dilemparkan ketika informasi diakses untuk pertama kalinya karena dievaluasi dengan malas. (Mirip dengan nilai anotasi dengan kesalahan.)
Tambahkan informasi meta parameter hanya untuk metode tertentu
Kompiler Java memungkinkan untuk menyematkan nama parameter dan informasi pengubah ketika mengkompilasi kelas dengan parameter
flag diaktifkan. Dalam format file kelas Java, informasi ini disimpan per-metode yang memungkinkan untuk hanya menyertakan informasi metode tersebut untuk metode tertentu.
Mengacaukan segalanya dan menghancurkan JVM Anda
Sebagai contoh, dalam kode byte Java, Anda dapat menentukan untuk memanggil metode apa pun pada jenis apa pun. Biasanya, pemverifikasi akan mengeluh jika suatu jenis tidak diketahui dari metode semacam itu. Namun, jika Anda memanggil metode yang tidak dikenal pada array, saya menemukan bug di beberapa versi JVM di mana verifier akan melewatkan ini dan JVM Anda akan selesai setelah instruksi dipanggil. Ini bukan fitur meskipun, tetapi secara teknis sesuatu yang tidak mungkin dengan javac dikompilasi Java. Java memiliki semacam validasi ganda. Validasi pertama diterapkan oleh kompiler Java, yang kedua oleh JVM ketika sebuah kelas dimuat. Dengan melewatkan kompiler, Anda mungkin menemukan titik lemah dalam validasi verifikasi. Ini lebih merupakan pernyataan umum daripada fitur.
Beri anotasi jenis penerima konstruktor ketika tidak ada kelas luar
Sejak Java 8, metode dan konstruktor non-statis kelas dalam dapat mendeklarasikan tipe penerima dan membubuhi keterangan jenis ini. Konstruktor dari kelas tingkat atas tidak dapat membubuhi keterangan jenis penerima mereka karena mereka paling tidak menyatakan satu.
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Sejak Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
Namun tidak mengembalikan AnnotatedType
mewakili Foo
, adalah mungkin untuk menyertakan anotasi tipe untuk Foo
's konstruktor langsung dalam file kelas di mana penjelasan ini kemudian dibaca oleh refleksi API.
Gunakan instruksi kode byte yang tidak digunakan / lawas
Karena orang lain menamainya, saya akan memasukkannya juga. Java sebelumnya menggunakan subrutin oleh JSR
dan RET
pernyataan. JBC bahkan tahu jenis alamat pengirimnya sendiri untuk tujuan ini. Namun, penggunaan subrutin melakukan overcomplicate analisis kode statis yang mengapa instruksi ini tidak lagi digunakan. Sebaliknya, kompiler Java akan menduplikasi kode yang dikompilasinya. Namun, ini pada dasarnya menciptakan logika yang identik itulah sebabnya saya tidak benar-benar menganggapnya untuk mencapai sesuatu yang berbeda. Demikian pula, misalnya Anda dapat menambahkanNOOP
instruksi kode byte yang tidak digunakan oleh kompiler Java juga tetapi ini tidak akan benar-benar memungkinkan Anda untuk mencapai sesuatu yang baru juga. Seperti yang ditunjukkan dalam konteks, "petunjuk fitur" yang disebutkan ini sekarang dihapus dari himpunan opcode legal yang membuatnya lebih sedikit fitur.