Pemrograman kinerja Java, Bagian 2: Biaya casting

Untuk artikel kedua dalam seri kami tentang kinerja Java, fokus bergeser ke casting - apa itu, berapa biayanya, dan bagaimana kami (terkadang) dapat menghindarinya. Bulan ini, kami memulai dengan tinjauan singkat tentang dasar-dasar kelas, objek, dan referensi, kemudian menindaklanjuti dengan melihat beberapa tokoh kinerja hardcore (di bilah sisi, agar tidak menyinggung perasaan mual!) Dan pedoman tentang jenis operasi yang paling mungkin menyebabkan gangguan pencernaan Java Virtual Machine (JVM). Terakhir, kami mengakhiri dengan pandangan mendalam tentang bagaimana kami dapat menghindari efek penataan kelas umum yang dapat menyebabkan casting.

Pemrograman kinerja Java: Baca seluruh seri!

  • Bagian 1. Pelajari cara mengurangi overhead program dan meningkatkan kinerja dengan mengontrol pembuatan objek dan pengumpulan sampah
  • Bagian 2. Mengurangi overhead dan kesalahan eksekusi melalui kode tipe-aman
  • Bagian 3. Lihat bagaimana koleksi alternatif mengukur kinerja, dan cari tahu bagaimana mendapatkan hasil maksimal dari setiap jenis

Jenis objek dan referensi di Java

Bulan lalu, kita membahas perbedaan dasar antara tipe primitif dan objek di Java. Baik jumlah tipe primitif dan hubungan di antara mereka (terutama konversi antar tipe) ditetapkan oleh definisi bahasa. Objek, di sisi lain, memiliki jenis yang tidak terbatas dan mungkin terkait dengan sejumlah jenis lainnya.

Setiap definisi kelas dalam program Java mendefinisikan tipe objek baru. Ini termasuk semua kelas dari perpustakaan Java, sehingga program apa pun yang diberikan mungkin menggunakan ratusan atau bahkan ribuan jenis objek yang berbeda. Beberapa tipe ini ditentukan oleh definisi bahasa Java sebagai memiliki penggunaan atau penanganan khusus tertentu (seperti penggunaan java.lang.StringBufferuntuk java.lang.Stringoperasi penggabungan). Selain beberapa pengecualian ini, semua tipe pada dasarnya diperlakukan sama oleh compiler Java dan JVM yang digunakan untuk menjalankan program.

Jika definisi kelas tidak menentukan (melalui extendsklausa di header definisi kelas) kelas lain sebagai induk atau superclass, itu secara implisit memperluas java.lang.Objectkelas. Ini berarti bahwa setiap kelas pada akhirnya meluas java.lang.Object, baik secara langsung atau melalui urutan satu atau lebih tingkat kelas induk.

Objek sendiri selalu contoh kelas, dan obyek tipe adalah kelas yang itu sebuah contoh. Di Java, kami tidak pernah berurusan langsung dengan objek; kami bekerja dengan referensi ke objek. Misalnya, baris:

 java.awt.Component myComponent; 

tidak membuat java.awt.Componentobjek; itu menciptakan jenis variabel referensi java.lang.Component. Meskipun referensi memiliki tipe seperti halnya objek, tidak ada kecocokan yang tepat antara referensi dan tipe objek - nilai referensi dapat berupa null, objek dengan tipe yang sama dengan referensi, atau objek dari subkelas apa pun (mis., Kelas yang diturunkan dari) jenis referensi. Dalam kasus khusus ini, java.awt.Componentadalah kelas abstrak, jadi kita tahu bahwa tidak akan pernah ada objek dengan tipe yang sama seperti referensi kita, tapi pasti ada objek subkelas dari tipe referensi itu.

Polimorfisme dan casting

Jenis referensi menentukan bagaimana objek yang direferensikan - yaitu, objek yang merupakan nilai referensi - dapat digunakan. Misalnya, dalam contoh di atas, penggunaan kode myComponentdapat memanggil salah satu metode yang ditentukan oleh kelas java.awt.Component, atau salah satu kelas supernya, pada objek yang direferensikan.

Namun, metode yang sebenarnya dijalankan oleh panggilan ditentukan bukan oleh jenis referensi itu sendiri, melainkan oleh jenis objek yang dirujuk. Ini adalah prinsip dasar polimorfisme - subkelas dapat mengganti metode yang ditentukan di kelas induk untuk mengimplementasikan perilaku yang berbeda. Dalam kasus variabel contoh kami, jika objek yang direferensikan sebenarnya adalah turunan dari java.awt.Button, perubahan status yang dihasilkan dari setLabel("Push Me")panggilan akan berbeda dari yang dihasilkan jika objek yang direferensikan adalah turunan dari java.awt.Label.

Selain definisi kelas, program Java juga menggunakan definisi antarmuka. Perbedaan antara antarmuka dan kelas adalah bahwa antarmuka hanya menentukan sekumpulan perilaku (dan, dalam beberapa kasus, konstanta), sedangkan kelas mendefinisikan implementasi. Karena antarmuka tidak mendefinisikan implementasi, objek tidak pernah bisa menjadi contoh antarmuka. Namun, mereka bisa menjadi contoh kelas yang mengimplementasikan antarmuka. Referensi dapat berupa tipe antarmuka, dalam hal ini objek yang direferensikan dapat berupa instance dari setiap kelas yang mengimplementasikan antarmuka (baik secara langsung atau melalui beberapa kelas leluhur).

Pengecoran digunakan untuk mengonversi antar jenis - antara jenis referensi khususnya, untuk jenis operasi pengecoran yang kami minati di sini. Operasi upcast (juga disebut konversi pelebaran dalam Spesifikasi Bahasa Java) mengonversi referensi subkelas menjadi referensi kelas leluhur. Operasi pengecoran ini biasanya otomatis, karena selalu aman dan dapat diterapkan secara langsung oleh penyusun.

Operasi downcast (juga disebut konversi penyempitan dalam Spesifikasi Bahasa Java) mengonversi referensi kelas leluhur menjadi referensi subkelas. Operasi casting ini menciptakan overhead eksekusi, karena Java mengharuskan cast diperiksa pada waktu proses untuk memastikannya valid. Jika objek yang direferensikan bukan instance dari salah satu jenis target untuk cast atau subclass dari jenis tersebut, percobaan cast tidak diizinkan dan harus melempar java.lang.ClassCastException.

The instanceofOperator di Jawa memungkinkan Anda untuk menentukan apakah atau tidak operasi pengecoran tertentu diperbolehkan tanpa benar-benar mencoba operasi. Karena biaya kinerja pemeriksaan jauh lebih kecil daripada pengecualian yang dihasilkan oleh percobaan pemeran yang tidak diizinkan, secara umum bijaksana untuk menggunakan instanceofpengujian kapan pun Anda tidak yakin bahwa jenis referensi adalah yang Anda inginkan. . Namun, sebelum melakukannya, Anda harus memastikan bahwa Anda memiliki cara yang masuk akal untuk menangani referensi dari jenis yang tidak diinginkan - jika tidak, Anda sebaiknya membiarkan pengecualian itu dilemparkan dan menanganinya di tingkat yang lebih tinggi dalam kode Anda.

Waspadai angin

Casting memungkinkan penggunaan pemrograman generik di Java, di mana kode ditulis untuk bekerja dengan semua objek kelas yang diturunkan dari beberapa kelas dasar (seringkali java.lang.Object, untuk kelas utilitas). Namun, penggunaan casting menyebabkan serangkaian masalah yang unik. Di bagian selanjutnya kita akan melihat dampaknya pada kinerja, tetapi pertama-tama mari kita pertimbangkan efeknya pada kode itu sendiri. Berikut adalah contoh menggunakan java.lang.Vectorkelas collection generik :

Vektor pribadi someNumbers; ... public void doSomething () {... int n = ... Integer number = (Integer) someNumbers.elementAt (n); ...}

Kode ini menyajikan masalah potensial dalam hal kejelasan dan pemeliharaan. Jika seseorang selain pengembang asli memodifikasi kode pada suatu saat, dia mungkin berpikir bahwa dia dapat menambahkan a java.lang.Doubleke someNumberskoleksi, karena ini adalah subkelas dari java.lang.Number. Semuanya akan terkompilasi dengan baik jika dia mencoba ini, tetapi pada beberapa titik yang tidak pasti dalam eksekusi, dia kemungkinan akan mendapatkan java.lang.ClassCastExceptionpelemparan ketika percobaan melemparkan ke java.lang.Integerdieksekusi untuk nilai tambahnya.

Masalahnya di sini adalah bahwa penggunaan casting melewati pemeriksaan keamanan yang dibangun ke dalam compiler Java; programmer akhirnya mencari kesalahan selama eksekusi, karena kompiler tidak akan menangkapnya. Ini tidak menimbulkan bencana, tetapi jenis kesalahan penggunaan ini sering kali tersembunyi dengan cukup cerdik saat Anda menguji kode, hanya untuk mengungkapkan dirinya sendiri saat program dimasukkan ke dalam produksi.

Tidak mengherankan, dukungan untuk teknik yang akan memungkinkan kompilator mendeteksi jenis kesalahan penggunaan ini adalah salah satu peningkatan yang paling banyak diminta untuk Java. Ada sebuah proyek yang sekarang sedang berlangsung dalam Proses Komunitas Java yang sedang menyelidiki menambahkan hanya dukungan ini: nomor proyek JSR-000014, Tambahkan Jenis Umum ke Bahasa Pemrograman Java (lihat bagian Sumber Daya di bawah untuk lebih jelasnya.) Dalam kelanjutan artikel ini, yang akan datang bulan depan, kita akan melihat proyek ini secara lebih rinci dan mendiskusikan bagaimana itu mungkin membantu dan di mana kemungkinan akan membuat kita menginginkan lebih.

Masalah kinerja

Sudah lama diketahui bahwa casting dapat merusak performa di Java, dan Anda dapat meningkatkan performa dengan meminimalkan casting dalam kode yang banyak digunakan. Panggilan metode, terutama panggilan melalui antarmuka, juga sering disebut sebagai potensi hambatan kinerja. Generasi JVM saat ini telah berkembang jauh dari pendahulunya, dan perlu dicek untuk melihat seberapa baik prinsip-prinsip ini bertahan hingga saat ini.

Untuk artikel ini, saya mengembangkan serangkaian tes untuk melihat seberapa penting faktor-faktor ini terhadap kinerja dengan JVM saat ini. Hasil pengujian dirangkum menjadi dua tabel di sidebar, Tabel 1 menunjukkan overhead metode panggilan dan overhead casting Tabel 2. Kode sumber lengkap untuk program pengujian juga tersedia secara online (lihat bagian Sumberdaya di bawah untuk lebih jelasnya).

Untuk meringkas kesimpulan ini bagi pembaca yang tidak ingin mempelajari detail dalam tabel, jenis pemanggilan dan pemeran metode tertentu masih cukup mahal, dalam beberapa kasus memakan waktu hampir sepanjang alokasi objek sederhana. Jika memungkinkan, jenis operasi ini harus dihindari dalam kode yang perlu dioptimalkan untuk performa.

Secara khusus, panggilan ke metode yang diganti (metode yang diganti dalam kelas apa pun yang dimuat, bukan hanya kelas objek yang sebenarnya) dan panggilan melalui antarmuka jauh lebih mahal daripada panggilan metode sederhana. Server HotSpot JVM 2.0 beta yang digunakan dalam pengujian bahkan akan mengonversi banyak panggilan metode sederhana menjadi kode sebaris, menghindari overhead apa pun untuk operasi semacam itu. Namun, HotSpot menunjukkan kinerja terburuk di antara JVM yang diuji untuk metode yang diganti dan panggilan melalui antarmuka.

Untuk casting (downcasting, tentu saja), JVM yang diuji biasanya menjaga kinerja mencapai tingkat yang wajar. HotSpot melakukan pekerjaan luar biasa dengan ini di sebagian besar pengujian benchmark, dan, seperti halnya panggilan metode, dalam banyak kasus sederhana mampu hampir sepenuhnya menghilangkan overhead casting. Untuk situasi yang lebih rumit, seperti cast yang diikuti dengan panggilan ke metode yang diganti, semua JVM yang diuji menunjukkan penurunan performa yang nyata.

Versi HotSpot yang diuji juga menunjukkan kinerja yang sangat buruk saat objek dilemparkan ke jenis referensi yang berbeda secara berurutan (alih-alih selalu dilemparkan ke jenis target yang sama). Situasi ini secara teratur muncul di pustaka seperti Swing yang menggunakan hierarki kelas yang dalam.

Dalam kebanyakan kasus, overhead untuk transmisi dan pemanggilan metode kecil dibandingkan dengan waktu alokasi objek yang dilihat di artikel bulan lalu. Namun, operasi ini akan sering digunakan jauh lebih sering daripada alokasi objek, jadi operasi ini masih dapat menjadi sumber masalah kinerja yang signifikan.

Di sisa artikel ini, kita akan membahas beberapa teknik khusus untuk mengurangi kebutuhan casting dalam kode Anda. Secara khusus, kita akan melihat seberapa sering casting muncul dari cara subclass berinteraksi dengan kelas dasar, dan menjelajahi beberapa teknik untuk menghilangkan jenis casting ini. Bulan depan, di bagian kedua tampilan casting ini, kami akan mempertimbangkan penyebab umum casting lainnya, penggunaan koleksi generik.

Kelas dasar dan casting

Ada beberapa kegunaan umum casting dalam program Java. Misalnya, casting sering digunakan untuk penanganan umum beberapa fungsionalitas di kelas dasar yang dapat diperluas oleh sejumlah subkelas. Kode berikut menunjukkan ilustrasi yang dibuat-buat dari penggunaan ini:

// kelas dasar sederhana dengan subkelas kelas abstrak publik BaseWidget {...} kelas publik SubWidget extends BaseWidget {... public void doSubWidgetSomething () {...}} ... // kelas dasar dengan subkelas, menggunakan set sebelumnya kelas public abstract class BaseGorph {// Widget yang terkait dengan private BaseWidget myWidget Gorph; ... // mengatur Widget yang terkait dengan Gorph ini (hanya diperbolehkan untuk subkelas) yang dilindungi void setWidget (Widget BaseWidget) {myWidget = widget; } // dapatkan Widget yang terkait dengan Gorph ini publik BaseWidget getWidget () {return myWidget; } ... // kembalikan Gorph dengan beberapa relasi ke Gorph ini // ini akan selalu sama dengan yang dipanggil, tapi kita hanya bisa // mengembalikan turunan dari kelas dasar public abstract BaseGorph otherGorph () {. ..}} // Subkelas Gorph menggunakan subkelas Widget kelas publik SubGorph extends BaseGorph {// kembalikan Gorph dengan beberapa hubungan ke Gorph publik BaseGorph otherGorph () ini {...} ... public void anyMethod () {... / / setel Widget yang kita gunakan SubWidget widget = ... setWidget (widget); ... // gunakan Widget kami ((SubWidget) getWidget ()). doSubWidgetSomething (); ... // gunakan OtherGorph SubGorph kami other = (SubGorph) otherGorph (); ...}}