Terjemahan artikel ini telah disiapkan khusus untuk siswa di kursus Java Developer.
Dalam artikel saya sebelumnya
Semuanya Tentang Metode Overloading vs Metode Overriding , kami melihat aturan dan perbedaan metode overloading dan overriding. Pada artikel ini, kita akan melihat bagaimana metode overloading dan overriding ditangani di dalam JVM.
Sebagai contoh, ambil kelas dari artikel sebelumnya: induk
Mammal
(mamalia) dan anak
Human
(manusia).
public class OverridingInternalExample { private static class Mammal { public void speak() { System.out.println("ohlllalalalalalaoaoaoa"); } } private static class Human extends Mammal { @Override public void speak() { System.out.println("Hello"); }
Kita dapat melihat pertanyaan polimorfisme dari dua sisi: dari "logis" dan "fisik". Pertama mari kita lihat sisi logis dari masalah ini.
Sudut pandang logis
Dari sudut pandang logis, pada tahap kompilasi, metode yang dipanggil dianggap terkait dengan jenis referensi. Tetapi pada saat dijalankan, metode objek yang dirujuk akan dipanggil.
Misalnya, di baris
humanMammal.speak();
kompiler berpikir bahwa
Mammal.speak()
akan dipanggil, karena
humanMammal
dinyatakan sebagai
Mammal
. Tetapi pada saat dijalankan, JVM akan tahu bahwa
humanMammal
berisi objek
Human
dan benar-benar akan memanggil metode
Human.speak()
.
Semuanya cukup sederhana selama kita tetap pada level konseptual. Tetapi bagaimana JVM menangani ini semua secara internal? Bagaimana JVM menghitung metode mana yang harus dipanggil?
Kita juga tahu bahwa metode kelebihan beban tidak disebut polimorfik dan diselesaikan pada waktu kompilasi. Meskipun kadang-kadang metode overloading disebut
kompilasi-waktu polimorfisme atau pengikatan awal / statis .
Metode yang diganti (override) diselesaikan pada saat runtime karena kompiler tidak tahu jika ada metode yang ditimpa dalam objek yang ditugaskan ke tautan.
Sudut pandang fisik
Pada bagian ini, kami akan mencoba menemukan bukti βfisikβ untuk semua pernyataan di atas. Untuk melakukan ini, lihat bytecode yang bisa kita dapatkan dengan menjalankan
javap -verbose OverridingInternalExample
. Parameter
-verbose
akan memungkinkan kita untuk mendapatkan bytecode yang lebih intuitif sesuai dengan program java kita.
Perintah di atas akan menampilkan dua bagian bytecode.
1. Kelompok konstanta . Ini berisi hampir semua yang diperlukan untuk menjalankan program. Misalnya, referensi metode (
#Methodref
), kelas (
#Class
), string literal (
#String
).
2. Bytecode program. Instruksi kode byte yang dapat dieksekusi.

Mengapa metode overloading disebut pengikatan statis
Dalam contoh di atas, kompiler berpikir bahwa metode
humanMammal.speak()
akan dipanggil dari kelas
Mammal
, meskipun pada saat run time akan dipanggil dari objek yang dirujuk dalam
humanMammal
- itu akan menjadi objek dari kelas
Human
.
Melihat kode kita dan hasil
javap
, kita melihat bahwa bytecode yang berbeda digunakan untuk memanggil metode
humanMammal.speak()
,
human.speak()
dan
human.speak("Hindi")
, karena kompiler dapat membedakannya berdasarkan referensi kelas .
Dengan demikian, dalam hal terjadi kelebihan metode, kompiler dapat mengidentifikasi instruksi bytecode dan alamat metode pada waktu kompilasi. Itulah sebabnya ini disebut
hubungan statis atau compile-time polymorphism.Mengapa metode overriding disebut pengikatan dinamis
Untuk memanggil metode
anyMammal.speak()
dan
humanMammal.speak()
, bytecode adalah sama, karena dari sudut pandang kompiler kedua metode dipanggil untuk kelas
Mammal
:
invokevirtual #4 // Method org/programming/mitra/exercises/OverridingInternalExample$Mammal.speak:()V
Jadi sekarang pertanyaannya adalah, jika kedua panggilan memiliki bytecode yang sama, bagaimana JVM tahu metode mana yang harus dihubungi?
Jawabannya tersembunyi di bytecode itu sendiri dan dalam instruksi
invokevirtual
. Menurut spesifikasi JVM
(catatan penerjemah: referensi ke JVM spec 2.11.8 ) :
Instruksi invokevirtual memanggil metode instance melalui pengiriman jenis objek (virtual). Ini adalah pengiriman metode normal dalam bahasa pemrograman Java.
JVM menggunakan
invokevirtual
untuk memanggil metode Java yang setara dengan metode virtual C ++. Di C ++, untuk mengganti metode di kelas lain, metode harus dinyatakan sebagai virtual. Tetapi di Java, secara default, semua metode adalah virtual (kecuali untuk metode final dan statis), jadi di kelas anak kita dapat mengganti metode apa pun.
Instruksi
invokevirtual
mengambil pointer ke metode yang akan dipanggil (# 4 adalah indeks dalam kumpulan konstan).
invokevirtual #4
Tetapi referensi # 4 lebih lanjut merujuk pada metode dan Kelas lain.
#4 = Methodref #2.#27
Semua tautan ini digunakan bersama untuk mendapatkan referensi ke metode dan kelas di mana metode yang diinginkan berada. Ini juga disebutkan dalam spesifikasi JVM (
catatan penerjemah: referensi ke JVM spec 2.7 ):
Java Virtual Machine tidak memerlukan struktur internal objek tertentu.
Dalam beberapa implementasi Java Virtual Machine oleh Oracle, referensi ke instance kelas adalah referensi ke handler, yang dengan sendirinya terdiri dari sepasang tautan: satu menunjuk ke tabel metode objek dan penunjuk ke objek Kelas yang mewakili jenis objek, dan yang lainnya ke area data pada heap yang berisi data objek.
Ini berarti bahwa setiap variabel referensi berisi dua pointer tersembunyi:
- Penunjuk ke tabel yang berisi metode objek dan penunjuk ke objek
Class
, misalnya, [speak(), speak(String) Class object]
- Pointer ke memori pada heap yang dialokasikan untuk data objek, seperti nilai-nilai bidang objek.
Tetapi sekali lagi muncul pertanyaan: bagaimana
invokevirtual
bekerja dengan ini? Sayangnya, tidak ada yang bisa menjawab pertanyaan ini, karena semuanya tergantung pada implementasi JVM dan bervariasi dari JVM ke JVM.
Dari alasan di atas, kita dapat menyimpulkan bahwa referensi ke objek secara tidak langsung berisi tautan / penunjuk ke tabel yang berisi semua referensi ke metode objek ini. Java meminjam konsep ini dari C ++. Tabel ini dikenal dengan berbagai nama, seperti
tabel metode virtual (VMT), tabel fungsi virtual (vftable), tabel virtual (vtable), tabel pengiriman .
Kami tidak dapat memastikan bagaimana vtable diimplementasikan di Java, karena itu tergantung pada JVM tertentu. Tetapi kita bisa berharap bahwa strategi akan hampir sama dengan di C ++, di mana vtable adalah struktur mirip array yang berisi nama metode dan referensi mereka. Setiap kali JVM mencoba mengeksekusi metode virtual, ia meminta alamatnya di vtable.
Untuk setiap kelas, hanya ada satu vtable, yang berarti bahwa tabel tersebut unik dan sama untuk semua objek kelas, mirip dengan objek Kelas. Objek kelas dibahas secara lebih rinci dalam artikel
Mengapa kelas Java luar tidak bisa statis dan
Mengapa Java adalah Bahasa Berorientasi Objek Murni Atau Mengapa Tidak .
Dengan demikian, hanya ada satu vtable untuk kelas
Object
, yang berisi semua 11 metode (jika
registerNatives
tidak diperhitungkan) dan tautan yang sesuai dengan implementasinya.

Ketika JVM memuat kelas Mammal ke dalam memori, ia membuat objek
Class
untuknya dan membuat vtable yang berisi semua metode dari vtable kelas
Object
dengan referensi yang sama (karena
Mammal
tidak menimpa metode dari
Object
) dan menambahkan entri baru untuk metode
speak()
.

Kemudian kelas kelas
Human
masuk, dan JVM menyalin semua entri dari tabel kelas
Mammal
ke tabel kelas
Human
dan menambahkan entri baru untuk versi
speak(String)
.
JVM tahu bahwa kelas
Human
telah menimpa dua metode:
toString()
dari
Object
dan
speak()
dari
Mammal
. Sekarang untuk metode ini, alih-alih membuat catatan baru dengan tautan yang diperbarui, JVM akan mengubah tautan ke metode yang ada dalam indeks yang sama dengan yang sebelumnya disajikan, dan mempertahankan nama metode yang sama.

Instruksi
invokevirtual
menyebabkan JVM untuk memproses nilai dalam referensi ke metode # 4 bukan sebagai alamat, tetapi sebagai nama metode yang dicari dalam tabel untuk objek saat ini.
Saya harap sekarang lebih jelas bagaimana JVM menggunakan pool konstan dan tabel metode virtual untuk menentukan metode mana yang akan dipanggil.
Anda dapat menemukan kode sampel di repositori
Github .