Gunakan == (atau! =) Untuk Membandingkan Java Enums

Sebagian besar pengembang Java baru dengan cepat mengetahui bahwa mereka umumnya harus membandingkan String Java menggunakan String.equals (Object) daripada menggunakan ==. Hal ini ditekankan dan diperkuat kepada pengembang baru berulang kali karena mereka hampir selalu bermaksud membandingkan konten String (karakter sebenarnya yang membentuk String) daripada identitas String (alamatnya di memori). Saya berpendapat bahwa kita harus memperkuat gagasan yang ==dapat digunakan sebagai pengganti Enum.equals (Object). Saya memberikan alasan saya untuk pernyataan ini di sisa posting ini.

Ada empat alasan yang menurut saya menggunakan ==untuk membandingkan enum Java hampir selalu lebih disukai daripada menggunakan metode "sama dengan":

  1. The ==pada enums memberikan perbandingan diharapkan sama (konten) sebagaiequals
  2. The ==pada enum ini bisa dibilang lebih mudah dibaca (kurang verbose) dariequals
  3. The ==pada enum lebih null-aman daripadaequals
  4. The ==pada enum menyediakan waktu kompilasi (statis) memeriksa daripada runtime checking

Alasan kedua yang disebutkan di atas ("bisa dibilang lebih mudah dibaca") jelas merupakan masalah opini, tetapi bagian tentang "kurang bertele-tele" dapat disepakati. Alasan pertama saya biasanya lebih suka ==ketika membandingkan enum adalah konsekuensi dari bagaimana Spesifikasi Bahasa Java menjelaskan enum. Bagian 8.9 ("Enums") menyatakan:

Ini adalah kesalahan waktu kompilasi untuk mencoba secara eksplisit membuat instance jenis enum. Metode klon terakhir di Enum memastikan bahwa konstanta enum tidak akan pernah bisa dikloning, dan perlakuan khusus oleh mekanisme serialisasi memastikan bahwa instance duplikat tidak pernah dibuat sebagai hasil dari deserialisasi. Instansiasi reflektif dari jenis enum dilarang. Bersama-sama, keempat hal ini memastikan bahwa tidak ada instance tipe enum yang ada di luar yang ditentukan oleh konstanta enum.

Karena hanya ada satu contoh dari setiap konstanta enum, operator == diperbolehkan untuk menggantikan metode sama dengan saat membandingkan dua referensi objek jika diketahui bahwa setidaknya salah satunya merujuk ke konstanta enum. (Metode sama di Enum adalah metode terakhir yang hanya memanggil super.equals pada argumennya dan mengembalikan hasilnya, sehingga melakukan perbandingan identitas.)

Kutipan dari spesifikasi yang ditunjukkan di atas menyiratkan dan kemudian secara eksplisit menyatakan bahwa aman menggunakan ==operator untuk membandingkan dua enum karena tidak mungkin terdapat lebih dari satu contoh konstanta enum yang sama.

Keuntungan keempat ==dibandingkan .equalsjika membandingkan enum berkaitan dengan keamanan waktu kompilasi. Penggunaan ==kekuatan pemeriksaan waktu kompilasi yang lebih ketat daripada .equalskarena Object.equals (Object) harus, berdasarkan kontrak, mengambil keputusan sewenang-wenang Object. Saat menggunakan bahasa yang diketik secara statis seperti Java, saya percaya dalam memanfaatkan keuntungan dari pengetikan statis ini sebanyak mungkin. Jika tidak, saya akan menggunakan bahasa yang diketik secara dinamis. Saya percaya bahwa salah satu tema Java Efektif yang berulang hanya itu: lebih suka pemeriksaan tipe statis bila memungkinkan.

Misalnya, saya memiliki enum khusus yang dipanggil Fruitdan saya mencoba membandingkannya dengan kelas java.awt.Color. Menggunakan ==operator memungkinkan saya mendapatkan kesalahan waktu kompilasi (termasuk pemberitahuan sebelumnya di IDE Java favorit saya) tentang masalah tersebut. Berikut adalah daftar kode yang mencoba membandingkan enum kustom ke kelas JDK menggunakan ==operator:

/** * Indicate if provided Color is a watermelon. * * This method's implementation is commented out to avoid a compiler error * that legitimately disallows == to compare two objects that are not and * cannot be the same thing ever. * * @param candidateColor Color that will never be a watermelon. * @return Should never be true. */ public boolean isColorWatermelon(java.awt.Color candidateColor) { // This comparison of Fruit to Color will lead to compiler error: // error: incomparable types: Fruit and Color return Fruit.WATERMELON == candidateColor; } 

Kesalahan kompilator ditampilkan di snapshot layar yang muncul berikutnya.

Meskipun saya bukan penggemar error, saya lebih suka error tersebut ditangkap secara statis pada waktu kompilasi daripada bergantung pada cakupan runtime. Seandainya saya menggunakan equalsmetode untuk perbandingan ini, kode akan dikompilasi dengan baik, tetapi metode akan selalu mengembalikan falsefalse karena tidak mungkin dustin.examples.Fruitenum akan sama dengan java.awt.Colorkelas. Saya tidak merekomendasikannya, tetapi berikut adalah metode perbandingan menggunakan .equals:

/** * Indicate whether provided Color is a Raspberry. This is utter nonsense * because a Color can never be equal to a Fruit, but the compiler allows this * check and only a runtime determination can indicate that they are not * equal even though they can never be equal. This is how NOT to do things. * * @param candidateColor Color that will never be a raspberry. * @return {@code false}. Always. */ public boolean isColorRaspberry(java.awt.Color candidateColor) { // // DON'T DO THIS: Waste of effort and misleading code!!!!!!!! // return Fruit.RASPBERRY.equals(candidateColor); } 

Hal yang "bagus" tentang hal di atas adalah kurangnya kesalahan waktu kompilasi. Ini terkompilasi dengan indah. Sayangnya, ini dibayar dengan harga yang berpotensi tinggi.

Keuntungan terakhir yang saya sebutkan menggunakan ==daripada Enum.equalssaat membandingkan enum adalah menghindari NullPointerException yang ditakuti. Seperti yang saya nyatakan dalam Penanganan NullPointerException Java Efektif, saya biasanya suka menghindari NullPointerExceptions. Ada serangkaian situasi terbatas di mana saya benar-benar ingin keberadaan null diperlakukan sebagai kasus luar biasa, tetapi sering kali saya lebih suka pelaporan masalah yang lebih anggun. Keuntungan membandingkan enum dengan ==adalah bahwa null dapat dibandingkan dengan enum bukan null tanpa menemui NullPointerException(NPE). Hasil dari perbandingan ini, jelas, adalah false.

Salah satu cara untuk menghindari NPE saat menggunakan .equals(Object)adalah dengan memanggil equalsmetode terhadap konstanta enum atau enum non-null yang diketahui dan kemudian meneruskan enum potensial dari karakter yang dipertanyakan (mungkin null) sebagai parameter ke equalsmetode. Hal ini sering dilakukan selama bertahun-tahun di Jawa dengan Strings untuk menghindari NPE. Namun, dengan ==operator, urutan perbandingan tidak menjadi masalah. Aku suka itu.

Saya telah membuat argumen saya dan sekarang saya beralih ke beberapa contoh kode. Daftar berikutnya adalah realisasi dari hipotesis enum Buah yang disebutkan sebelumnya.

Fruit.java

package dustin.examples; public enum Fruit { APPLE, BANANA, BLACKBERRY, BLUEBERRY, CHERRY, GRAPE, KIWI, MANGO, ORANGE, RASPBERRY, STRAWBERRY, TOMATO, WATERMELON } 

Daftar kode berikutnya adalah kelas Java sederhana yang menyediakan metode untuk mendeteksi jika enum atau objek tertentu adalah buah tertentu. Saya biasanya meletakkan cek seperti ini di enum itu sendiri, tetapi mereka bekerja lebih baik di kelas terpisah di sini untuk tujuan ilustrasi dan demonstratif saya. Kelas ini meliputi dua metode ditampilkan sebelumnya untuk membandingkan Fruituntuk Colordengan kedua ==dan equals. Tentu saja, metode yang digunakan ==untuk membandingkan enum dengan kelas harus memiliki bagian yang diberi komentar agar dapat dikompilasi dengan benar.

EnumComparisonMain.java

package dustin.examples; public class EnumComparisonMain { /** * Indicate whether provided fruit is a watermelon ({@code true} or not * ({@code false}). * * @param candidateFruit Fruit that may or may not be a watermelon; null is * perfectly acceptable (bring it on!). * @return {@code true} if provided fruit is watermelon; {@code false} if * provided fruit is NOT a watermelon. */ public boolean isFruitWatermelon(Fruit candidateFruit) { return candidateFruit == Fruit.WATERMELON; } /** * Indicate whether provided object is a Fruit.WATERMELON ({@code true}) or * not ({@code false}). * * @param candidateObject Object that may or may not be a watermelon and may * not even be a Fruit! * @return {@code true} if provided object is a Fruit.WATERMELON; * {@code false} if provided object is not Fruit.WATERMELON. */ public boolean isObjectWatermelon(Object candidateObject) { return candidateObject == Fruit.WATERMELON; } /** * Indicate if provided Color is a watermelon. * * This method's implementation is commented out to avoid a compiler error * that legitimately disallows == to compare two objects that are not and * cannot be the same thing ever. * * @param candidateColor Color that will never be a watermelon. * @return Should never be true. */ public boolean isColorWatermelon(java.awt.Color candidateColor) { // Had to comment out comparison of Fruit to Color to avoid compiler error: // error: incomparable types: Fruit and Color return /*Fruit.WATERMELON == candidateColor*/ false; } /** * Indicate whether provided fruit is a strawberry ({@code true}) or not * ({@code false}). * * @param candidateFruit Fruit that may or may not be a strawberry; null is * perfectly acceptable (bring it on!). * @return {@code true} if provided fruit is strawberry; {@code false} if * provided fruit is NOT strawberry. */ public boolean isFruitStrawberry(Fruit candidateFruit) { return Fruit.STRAWBERRY == candidateFruit; } /** * Indicate whether provided fruit is a raspberry ({@code true}) or not * ({@code false}). * * @param candidateFruit Fruit that may or may not be a raspberry; null is * completely and entirely unacceptable; please don't pass null, please, * please, please. * @return {@code true} if provided fruit is raspberry; {@code false} if * provided fruit is NOT raspberry. */ public boolean isFruitRaspberry(Fruit candidateFruit) { return candidateFruit.equals(Fruit.RASPBERRY); } /** * Indicate whether provided Object is a Fruit.RASPBERRY ({@code true}) or * not ({@code false}). * * @param candidateObject Object that may or may not be a Raspberry and may * or may not even be a Fruit! * @return {@code true} if provided Object is a Fruit.RASPBERRY; {@code false} * if it is not a Fruit or not a raspberry. */ public boolean isObjectRaspberry(Object candidateObject) { return candidateObject.equals(Fruit.RASPBERRY); } /** * Indicate whether provided Color is a Raspberry. This is utter nonsense * because a Color can never be equal to a Fruit, but the compiler allows this * check and only a runtime determination can indicate that they are not * equal even though they can never be equal. This is how NOT to do things. * * @param candidateColor Color that will never be a raspberry. * @return {@code false}. Always. */ public boolean isColorRaspberry(java.awt.Color candidateColor) { // // DON'T DO THIS: Waste of effort and misleading code!!!!!!!! // return Fruit.RASPBERRY.equals(candidateColor); } /** * Indicate whether provided fruit is a grape ({@code true}) or not * ({@code false}). * * @param candidateFruit Fruit that may or may not be a grape; null is * perfectly acceptable (bring it on!). * @return {@code true} if provided fruit is a grape; {@code false} if * provided fruit is NOT a grape. */ public boolean isFruitGrape(Fruit candidateFruit) { return Fruit.GRAPE.equals(candidateFruit); } } 

Saya memutuskan untuk mendekati demonstrasi ide yang ditangkap dalam metode di atas melalui tes unit. Secara khusus, saya menggunakan Groovy's GroovyTestCase. Kelas untuk menggunakan pengujian unit yang didukung Groovy ada di daftar kode berikutnya.

EnumComparisonTest.groovy