JavaRush /Java Blog /Random-ID /FindBugs membantu Anda mempelajari Java dengan lebih baik...
articles
Level 15

FindBugs membantu Anda mempelajari Java dengan lebih baik

Dipublikasikan di grup Random-ID
Penganalisis kode statis populer karena membantu menemukan kesalahan yang disebabkan oleh kecerobohan. Namun yang lebih menarik adalah mereka membantu memperbaiki kesalahan yang dibuat karena ketidaktahuan. Meskipun semuanya tertulis dalam dokumentasi resmi bahasa tersebut, bukan fakta bahwa semua programmer telah membacanya dengan cermat. Dan pemrogram dapat memahami: Anda akan bosan membaca semua dokumentasi. Dalam hal ini, penganalisa statis seperti seorang teman berpengalaman yang duduk di sebelah Anda dan melihat Anda menulis kode. Dia tidak hanya memberi tahu Anda: “Di sinilah Anda membuat kesalahan saat menyalin dan menempel,” tetapi juga mengatakan: “Tidak, Anda tidak bisa menulis seperti itu, lihat sendiri dokumentasinya.” Teman seperti itu lebih berguna daripada dokumentasi itu sendiri, karena dia hanya menyarankan hal-hal yang benar-benar Anda temui dalam pekerjaan Anda, dan diam tentang hal-hal yang tidak akan pernah berguna bagi Anda. Dalam postingan ini, saya akan membahas beberapa seluk-beluk Java yang saya pelajari dari penggunaan penganalisis statis FindBugs. Mungkin beberapa hal juga tidak terduga bagi Anda. Penting agar semua contoh tidak bersifat spekulatif, namun didasarkan pada kode nyata.

Operator terner?:

Tampaknya tidak ada yang lebih sederhana daripada operator ternary, tetapi ia memiliki kelemahan. Saya yakin tidak ada perbedaan mendasar antara desainnya Type var = condition ? valTrue : valFalse; dan Type var; if(condition) var = valTrue; else var = valFalse; ternyata ada kehalusan di sini. Karena operator ternary dapat menjadi bagian dari ekspresi kompleks, hasilnya harus berupa tipe konkrit yang ditentukan pada waktu kompilasi. Oleh karena itu, katakanlah, dengan kondisi benar dalam bentuk if, kompiler mengarahkan valTrue langsung ke tipe Tipe, dan dalam bentuk operator ternary, pertama-tama mengarah ke tipe umum valTrue dan valFalse (terlepas dari kenyataan bahwa valFalse tidak dievaluasi), dan kemudian hasilnya mengarah ke tipe Type. Aturan casting tidak sepenuhnya sepele jika ekspresi melibatkan tipe primitif dan pembungkusnya (Integer, Double, dll.) Semua aturan dijelaskan secara rinci di JLS 15.25. Mari kita lihat beberapa contoh. Number n = flag ? new Integer(1) : new Double(2.0); Apa yang akan terjadi pada n jika bendera disetel? Objek Ganda dengan nilai 1,0. Kompiler menganggap upaya kikuk kami untuk membuat objek lucu. Karena argumen kedua dan ketiga adalah pembungkus tipe primitif yang berbeda, kompilator membukanya dan menghasilkan tipe yang lebih tepat (dalam hal ini, ganda). Dan setelah mengeksekusi operator ternary untuk penugasan tersebut, tinju dilakukan lagi. Pada dasarnya kode tersebut setara dengan ini: Number n; if( flag ) n = Double.valueOf((double) ( new Integer(1).intValue() )); else n = Double.valueOf(new Double(2.0).doubleValue()); Dari sudut pandang kompiler, kode tersebut tidak mengandung masalah dan dapat dikompilasi dengan sempurna. Namun FindBugs memberikan peringatan:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: Nilai primitif tidak dimasukkan ke dalam kotak dan dipaksa untuk operator ternary di TestTernary.main(String[]) Nilai primitif yang dibungkus tidak dimasukkan ke dalam kotak dan dikonversi ke tipe primitif lain sebagai bagian dari evaluasi operator ternary bersyarat (operator b? e1: e2 ). Semantik Java mengamanatkan bahwa jika e1 dan e2 dibungkus dengan nilai numerik, nilai tersebut tidak dikotak-kotakkan dan dikonversi/dipaksa ke tipe umumnya (misalnya, jika e1 bertipe Integer dan e2 bertipe Float, maka e1 tidak dikotak, dikonversi ke nilai floating point, dan dikotak. Lihat JLS Bagian 15.25. Tentu saja, FindBugs juga memperingatkan bahwa Integer.valueOf(1) lebih efisien daripada Integer baru(1), tetapi semua orang sudah mengetahuinya.
Atau contoh ini: Integer n = flag ? 1 : null; Penulis ingin memasukkan null ke dalam n jika flag tidak disetel. Apakah menurut Anda ini akan berhasil? Ya. Namun mari kita perumitnya: Integer n = flag1 ? 1 : flag2 ? 2 : null; Tampaknya tidak banyak perbedaan. Namun, sekarang jika kedua tandanya jelas, baris ini akan memunculkan NullPointerException. Pilihan untuk operator ternary kanan adalah int dan null, sehingga tipe hasilnya adalah Integer. Pilihan di sebelah kiri adalah int dan Integer, jadi menurut aturan Java hasilnya adalah int. Untuk melakukan ini, Anda perlu melakukan unboxing dengan memanggil intValue, yang memunculkan pengecualian. Kodenya setara dengan ini: Integer n; if( flag1 ) n = Integer.valueOf(1); else { if( flag2 ) n = Integer.valueOf(Integer.valueOf(2).intValue()); else n = Integer.valueOf(((Integer)null).intValue()); } Di ​​sini FindBugs menghasilkan dua pesan, yang cukup untuk mencurigai adanya kesalahan:
BX_UNBOXING_IMMEDIATELY_REBOXED: Nilai dalam kotak tidak dikotakkan dan kemudian segera dikotak ulang di TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Kemungkinan dereferensi penunjuk nol dari null di TestTernary.main(String[]) Ada cabang pernyataan yang, jika dijalankan, menjamin bahwa a nilai null akan didereferensi, yang akan menghasilkan NullPointerException saat kode dijalankan.
Nah, satu contoh terakhir tentang topik ini: double[] vals = new double[] {1.0, 2.0, 3.0}; double getVal(int idx) { return (idx < 0 || idx >= vals.length) ? null : vals[idx]; } Tidak mengherankan jika kode ini tidak berfungsi: bagaimana suatu fungsi yang mengembalikan tipe primitif dapat mengembalikan null? Anehnya, ini dapat dikompilasi tanpa masalah. Nah, Anda sudah mengerti mengapa itu dikompilasi.

Format tanggal

Untuk memformat tanggal dan waktu di Java, disarankan untuk menggunakan kelas yang mengimplementasikan antarmuka DateFormat. Misalnya, tampilannya seperti ini: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Seringkali suatu kelas akan menggunakan format yang sama berulang kali. Banyak orang akan mendapatkan ide optimasi: mengapa membuat objek format setiap saat ketika Anda dapat menggunakan instance umum? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } Sangat indah dan keren, tapi sayangnya tidak berhasil. Lebih tepatnya berhasil, tapi terkadang rusak. Faktanya adalah dokumentasi untuk DateFormat mengatakan:
Format tanggal tidak disinkronkan. Disarankan untuk membuat contoh format terpisah untuk setiap thread. Jika beberapa thread mengakses suatu format secara bersamaan, format tersebut harus disinkronkan secara eksternal.
Dan ini benar jika Anda melihat implementasi internal SimpleDateFormat. Selama eksekusi metode format(), objek menulis ke bidang kelas, sehingga penggunaan SimpleDateFormat secara bersamaan dari dua utas akan menyebabkan hasil yang salah dengan beberapa kemungkinan. Inilah yang FindBugs tulis tentang ini:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Panggilan ke metode java.text.DateFormat statis di TestDate.getDate() Seperti yang dinyatakan JavaDoc, DateFormats pada dasarnya tidak aman untuk penggunaan multithread. Detektor telah menemukan panggilan ke instance DateFormat yang diperoleh melalui bidang statis. Ini terlihat mencurigakan. Untuk informasi lebih lanjut mengenai hal ini lihat Sun Bug #6231579 dan Sun Bug #6178997.

Jebakan BigDecimal

Setelah mengetahui bahwa kelas BigDecimal memungkinkan Anda menyimpan bilangan pecahan dengan presisi sewenang-wenang, dan melihat bahwa ia memiliki konstruktor untuk double, beberapa orang akan memutuskan bahwa semuanya sudah jelas dan Anda dapat melakukannya seperti ini: System.out.println(new BigDecimal( 1.1)); Tidak ada yang benar-benar melarang melakukan hal ini, tetapi hasilnya mungkin tampak tidak terduga: 1.100000000000000088817841970012523233890533447265625. Hal ini terjadi karena penggandaan primitif disimpan dalam format IEEE754, di mana tidak mungkin untuk merepresentasikan 1,1 secara akurat (dalam sistem bilangan biner, diperoleh pecahan periodik tak hingga). Oleh karena itu, nilai yang paling dekat dengan 1,1 disimpan di sana. Sebaliknya, konstruktor BigDecimal(double) bekerja dengan tepat: konstruktor ini dengan sempurna mengubah bilangan tertentu di IEEE754 menjadi bentuk desimal (pecahan biner akhir selalu dapat direpresentasikan sebagai desimal akhir). Jika Anda ingin merepresentasikan 1.1 sebagai BigDecimal, Anda dapat menulis New BigDecimal("1.1") atau BigDecimal.valueOf(1.1). Jika Anda tidak langsung menampilkan nomornya, tetapi melakukan beberapa operasi dengannya, Anda mungkin tidak mengerti dari mana kesalahan itu berasal. FindBugs mengeluarkan peringatan DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, yang memberikan saran yang sama. Berikut hal lainnya: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); Faktanya, d1 dan d2 mewakili angka yang sama, tetapi sama dengan mengembalikan false karena tidak hanya membandingkan nilai angka, tetapi juga urutan saat ini (jumlah tempat desimal). Ini tertulis dalam dokumentasi, tetapi hanya sedikit orang yang akan membaca dokumentasi untuk metode yang sudah dikenal seperti yang setara. Masalah seperti ini mungkin tidak serta merta muncul. Sayangnya, FindBugs sendiri tidak memperingatkan tentang hal ini, tetapi ada ekstensi populer untuk itu - fb-contrib, yang memperhitungkan bug ini:
MDM_BIGDECIMAL_EQUALS sama dengan() dipanggil untuk membandingkan dua angka java.math.BigDecimal. Hal ini biasanya merupakan kesalahan, karena dua objek BigDecimal hanya sama jika keduanya sama dalam nilai dan skala, sehingga 2,0 tidak sama dengan 2,00. Untuk membandingkan objek BigDecimal demi kesetaraan matematika, gunakan bandingkanTo() sebagai gantinya.

Jeda baris dan printf

Seringkali pemrogram yang beralih ke Java setelah C dengan senang hati menemukan PrintStream.printf (serta PrintWriter.printf , dll.). Bagus, saya tahu, sama seperti di C, Anda tidak perlu mempelajari sesuatu yang baru. Sebenarnya ada perbedaan. Salah satunya terletak pada terjemahan baris. Bahasa C memiliki pembagian menjadi teks dan aliran biner. Mengeluarkan karakter '\n' ke aliran teks dengan cara apa pun akan secara otomatis dikonversi ke baris baru yang bergantung pada sistem ("\r\n" di Windows). Tidak ada pemisahan seperti itu di Java: urutan karakter yang benar harus diteruskan ke aliran keluaran. Hal ini dilakukan secara otomatis, misalnya dengan metode keluarga PrintStream.println. Namun saat menggunakan printf, meneruskan '\n' dalam format string hanyalah '\n', bukan baris baru yang bergantung pada sistem. Sebagai contoh, mari kita tulis kode berikut: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Setelah mengarahkan hasilnya ke sebuah file, kita akan melihat: FindBugs membantu Anda mempelajari Java dengan lebih baik - 1 Jadi, Anda bisa mendapatkan kombinasi aneh dari jeda baris dalam satu thread, yang terlihat tidak rapi dan dapat mengejutkan beberapa parser. Kesalahan ini mungkin tidak diketahui untuk waktu yang lama, terutama jika Anda bekerja pada sistem Unix. Untuk menyisipkan baris baru yang valid menggunakan printf, karakter pemformatan khusus "%n" digunakan. Inilah yang FindBugs tulis tentang ini:
VA_FORMAT_STRING_USES_NEWLINE: Format string harus menggunakan %n daripada \n di TestNewline.main(String[]) Format string ini menyertakan karakter baris baru (\n). Dalam format string, umumnya lebih baik menggunakan %n, yang akan menghasilkan pemisah baris khusus platform.
Mungkin bagi sebagian pembaca, semua hal di atas sudah diketahui sejak lama. Tapi saya hampir yakin bagi mereka akan ada peringatan menarik dari penganalisa statis, yang akan mengungkapkan kepada mereka fitur-fitur baru dari bahasa pemrograman yang digunakan.
Komentar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION