JavaRush /Blog Java /Random-MS /FindBugs membantu anda mempelajari Java dengan lebih baik...
articles
Tahap

FindBugs membantu anda mempelajari Java dengan lebih baik

Diterbitkan dalam kumpulan
Penganalisis kod statik popular kerana ia membantu mencari ralat yang dibuat akibat kecuaian. Tetapi apa yang lebih menarik ialah mereka membantu membetulkan kesilapan yang dibuat kerana kejahilan. Walaupun semuanya ditulis dalam dokumentasi rasmi untuk bahasa itu, ia bukanlah fakta bahawa semua pengaturcara telah membacanya dengan teliti. Dan pengaturcara boleh memahami: anda akan bosan membaca semua dokumentasi. Dalam hal ini, penganalisis statik adalah seperti rakan berpengalaman yang duduk di sebelah anda dan memerhatikan anda menulis kod. Dia bukan sahaja memberitahu anda: "Di sinilah anda membuat kesilapan apabila anda menyalin dan menampal," tetapi juga berkata: "Tidak, anda tidak boleh menulis seperti itu, lihat dokumentasi sendiri." Rakan seperti itu lebih berguna daripada dokumentasi itu sendiri, kerana dia hanya mencadangkan perkara-perkara yang sebenarnya anda hadapi dalam kerja anda, dan diam tentang perkara-perkara yang tidak akan berguna kepada anda. Dalam siaran ini, saya akan bercakap tentang beberapa kerumitan Java yang saya pelajari daripada menggunakan penganalisis statik FindBugs. Mungkin ada perkara yang tidak dijangka untuk anda juga. Adalah penting bahawa semua contoh bukan spekulatif, tetapi berdasarkan kod sebenar.

Operator ternary ?:

Nampaknya tidak ada yang lebih mudah daripada pengendali ternary, tetapi ia mempunyai perangkapnya. Saya percaya bahawa tidak ada perbezaan asas antara reka bentuk Type var = condition ? valTrue : valFalse; dan Type var; if(condition) var = valTrue; else var = valFalse; ternyata terdapat kehalusan di sini. Memandangkan pengendali ternary boleh menjadi sebahagian daripada ungkapan yang kompleks, hasilnya mestilah jenis konkrit yang ditentukan pada masa penyusunan. Oleh itu, katakan, dengan keadaan benar dalam bentuk if, pengkompil membawa valTrue terus kepada jenis Jenis, dan dalam bentuk pengendali ternary, ia mula-mula membawa kepada jenis biasa valTrue dan valFalse (walaupun fakta bahawa valFalse bukan dinilai), dan kemudian hasilnya membawa kepada jenis Jenis. Peraturan pemutus tidak sepenuhnya remeh jika ungkapan melibatkan jenis primitif dan pembalut di atasnya (Integer, Double, dsb.) Semua peraturan diterangkan secara terperinci dalam JLS 15.25. Mari lihat beberapa contoh. Number n = flag ? new Integer(1) : new Double(2.0); Apakah yang akan berlaku kepada n jika bendera ditetapkan? Objek Berganda dengan nilai 1.0. Pengkompil mendapati percubaan kekok kami untuk mencipta objek lucu. Oleh kerana hujah kedua dan ketiga adalah pembalut pada jenis primitif yang berbeza, pengkompil membukanya dan menghasilkan jenis yang lebih tepat (dalam kes ini, berganda). Dan selepas melaksanakan pengendali ternary untuk tugasan, tinju dilakukan semula. Pada asasnya kod adalah bersamaan 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 pandangan pengkompil, kod tidak mengandungi masalah dan disusun dengan sempurna. Tetapi FindBugs memberikan amaran:
BX_UNBOXED_AND_COERCED_FOR_TERNARY_OPERATOR: Nilai primitif dinyahkotak dan dipaksa untuk operator ternary dalam TestTernary.main(String[]) Nilai primitif yang dibalut dinyahkotak dan ditukar kepada jenis primitif lain sebagai sebahagian daripada penilaian operator ternary bersyarat (operator e2 b? e1: e2 b? ). Semantik Java memberi mandat bahawa jika e1 dan e2 dibalut nilai berangka, nilai itu dinyahkotak dan ditukar/dipaksa kepada jenis biasa mereka (cth, jika e1 adalah jenis Integer dan e2 adalah jenis Terapung, maka e1 dinyahkotak, ditukar kepada nilai titik terapung, dan dikotak-kotak. Lihat JLS Bahagian 15.25. Sudah tentu, FindBugs juga memberi amaran bahawa Integer.valueOf(1) adalah lebih cekap daripada Integer(1) baharu, tetapi semua orang sudah mengetahuinya.
Atau contoh ini: Integer n = flag ? 1 : null; Pengarang ingin meletakkan null dalam n jika bendera tidak ditetapkan. Adakah anda fikir ia akan berkesan? ya. Tetapi mari kita merumitkan perkara: Integer n = flag1 ? 1 : flag2 ? 2 : null; Nampaknya tidak banyak perbezaan. Walau bagaimanapun, sekarang jika kedua-dua bendera jelas, baris ini melemparkan NullPointerException. Pilihan untuk operator ternary yang betul adalah int dan null, jadi jenis hasil ialah Integer. Pilihan untuk yang kiri adalah int dan Integer, jadi menurut peraturan Java hasilnya adalah int. Untuk melakukan ini, anda perlu melakukan unboxing dengan memanggil intValue, yang memberikan pengecualian. Kod ini bersamaan 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 mesej, yang cukup untuk mengesyaki ralat:
BX_UNBOXING_IMMEDIATELY_REBOXED: Nilai berkotak dinyahkotak dan kemudian dikotak semula dengan serta-merta dalam TestTernary.main(String[]) NP_NULL_ON_SOME_PATH: Kemungkinan dereference pointer null of null dalam TestTernary.main(String[]) Terdapat cabang pernyataan yang, jika dilaksanakan, menjamin bahawa jika dilaksanakan nilai null akan dinyahrujuk, yang akan menghasilkan NullPointerException apabila kod tersebut dilaksanakan.
Nah, satu contoh terakhir mengenai 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 menghairankan bahawa kod ini tidak berfungsi: bagaimanakah fungsi yang mengembalikan jenis primitif mengembalikan nol? Yang menghairankan, ia menyusun tanpa masalah. Nah, anda sudah faham mengapa ia disusun.

Format tarikh

Untuk memformat tarikh dan masa dalam Java, adalah disyorkan untuk menggunakan kelas yang melaksanakan antara muka DateFormat. Sebagai contoh, ia kelihatan seperti ini: public String getDate() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); } Selalunya kelas akan menggunakan format yang sama berulang kali. Ramai orang akan datang dengan idea pengoptimuman: mengapa membuat objek format setiap kali apabila anda boleh menggunakan contoh biasa? private static final DateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); public String getDate() { return format.format(new Date()); } Ia sangat cantik dan sejuk, tetapi malangnya ia tidak berfungsi. Lebih tepat lagi ia berfungsi, tetapi kadang-kadang pecah. Hakikatnya ialah dokumentasi untuk DateFormat berkata:
Format tarikh tidak disegerakkan. Adalah disyorkan untuk membuat contoh format berasingan untuk setiap urutan. Jika berbilang utas mengakses format secara serentak, ia mesti disegerakkan secara luaran.
Dan ini benar jika anda melihat pelaksanaan dalaman SimpleDateFormat. Semasa pelaksanaan kaedah format(), objek menulis ke medan kelas, jadi penggunaan serentak SimpleDateFormat daripada dua utas akan membawa kepada hasil yang salah dengan beberapa kebarangkalian. Inilah yang FindBugs tulis tentang ini:
STCAL_INVOKE_ON_STATIC_DATE_FORMAT_INSTANCE: Panggilan ke kaedah java.text.DateFormat statik dalam TestDate.getDate() Seperti yang dinyatakan oleh JavaDoc, DateFormats sememangnya tidak selamat untuk kegunaan berbilang benang. Pengesan telah menemui panggilan ke contoh DateFormat yang telah diperoleh melalui medan statik. Ini kelihatan mencurigakan. Untuk maklumat lanjut tentang ini, lihat Sun Bug #6231579 dan Sun Bug #6178997.

Perangkap BigDecimal

Setelah mengetahui bahawa kelas BigDecimal membolehkan anda menyimpan nombor pecahan ketepatan sewenang-wenangnya, dan melihat bahawa ia mempunyai pembina untuk dua kali ganda, sesetengah akan memutuskan bahawa semuanya jelas dan anda boleh melakukannya seperti ini: System.out.println(new BigDecimal( 1.1)); Tiada siapa yang benar-benar melarang melakukan ini, tetapi hasilnya mungkin kelihatan tidak dijangka: 1.100000000000000088817841970012523233890533447265625. Ini berlaku kerana gandaan primitif disimpan dalam format IEEE754, di mana adalah mustahil untuk mewakili 1.1 dengan tepat dengan sempurna (dalam sistem nombor binari, pecahan berkala tak terhingga diperolehi). Oleh itu, nilai yang paling hampir dengan 1.1 disimpan di sana. Sebaliknya, pembina BigDecimal(double) berfungsi dengan tepat: ia menukar nombor tertentu dalam IEEE754 kepada bentuk perpuluhan dengan sempurna (pecahan binari akhir sentiasa boleh diwakili sebagai perpuluhan akhir). Jika anda ingin mewakili tepat 1.1 sebagai BigDecimal, maka anda boleh menulis sama ada BigDecimal("1.1") atau BigDecimal.valueOf(1.1) baharu. Jika anda tidak memaparkan nombor itu serta-merta, tetapi melakukan beberapa operasi dengannya, anda mungkin tidak faham dari mana ralat itu datang. FindBugs mengeluarkan amaran DMI_BIGDECIMAL_CONSTRUCTED_FROM_DOUBLE, yang memberikan nasihat yang sama. Ini satu lagi perkara: BigDecimal d1 = new BigDecimal("1.1"); BigDecimal d2 = new BigDecimal("1.10"); System.out.println(d1.equals(d2)); Sebenarnya, d1 dan d2 mewakili nombor yang sama, tetapi sama dengan pulangan palsu kerana ia membandingkan bukan sahaja nilai nombor, tetapi juga susunan semasa (bilangan tempat perpuluhan). Ini ditulis dalam dokumentasi, tetapi beberapa orang akan membaca dokumentasi untuk kaedah biasa seperti yang sama. Masalah sedemikian mungkin tidak timbul serta-merta. FindBugs sendiri, malangnya, tidak memberi amaran tentang perkara ini, tetapi terdapat sambungan popular untuknya - fb-contrib, yang mengambil kira pepijat ini:
MDM_BIGDECIMAL_EQUALS sama dengan() dipanggil untuk membandingkan dua nombor java.math.BigDecimal. Ini biasanya satu kesilapan, kerana dua objek BigDecimal hanya sama jika ia sama dalam kedua-dua nilai dan skala, supaya 2.0 tidak sama dengan 2.00. Untuk membandingkan objek BigDecimal untuk kesamaan matematik, gunakan compareTo() sebaliknya.

Garis putus dan cetakf

Selalunya pengaturcara yang beralih ke Java selepas C gembira untuk menemui PrintStream.printf (serta PrintWriter.printf , dsb.). Suka, bagus, saya tahu bahawa, sama seperti dalam C, anda tidak perlu mempelajari sesuatu yang baharu. Sebenarnya ada perbezaan. Salah satunya terletak pada terjemahan baris. Bahasa C mempunyai pembahagian kepada aliran teks dan binari. Mengeluarkan aksara '\n' kepada strim teks dengan sebarang cara akan ditukar secara automatik kepada baris baharu yang bergantung kepada sistem ("\r\n" pada Windows). Tiada pemisahan sedemikian dalam Java: urutan aksara yang betul mesti dihantar ke aliran keluaran. Ini dilakukan secara automatik, contohnya, melalui kaedah keluarga PrintStream.println. Tetapi apabila menggunakan printf, menghantar '\n' dalam rentetan format hanyalah '\n', bukan baris baharu yang bergantung kepada sistem. Sebagai contoh, mari tulis kod berikut: System.out.printf("%s\n", "str#1"); System.out.println("str#2"); Setelah mengalihkan keputusan ke fail, kita akan melihat: FindBugs membantu anda mempelajari Java dengan lebih baik - 1 Oleh itu, anda boleh mendapatkan gabungan pemecah baris dalam satu utas, yang kelihatan ceroboh dan boleh meniup fikiran sesetengah penghurai. Ralat mungkin tidak disedari untuk masa yang lama, terutamanya jika anda menggunakan sistem Unix terutamanya. Untuk memasukkan baris baharu yang sah menggunakan printf, aksara pemformatan khas "%n" digunakan. Inilah yang FindBugs tulis tentang ini:
VA_FORMAT_STRING_USES_NEWLINE: Format rentetan hendaklah menggunakan %n dan bukannya \n dalam TestNewline.main(String[]) Rentetan format ini termasuk aksara baris baharu (\n). Dalam rentetan format, biasanya lebih baik menggunakan %n, yang akan menghasilkan pemisah talian khusus platform.
Mungkin, bagi sesetengah pembaca, semua perkara di atas diketahui sejak sekian lama. Tetapi saya hampir pasti bahawa bagi mereka akan ada amaran menarik dari penganalisis statik, yang akan mendedahkan kepada mereka ciri-ciri baru bahasa pengaturcaraan yang digunakan.
Komen
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION