Użyj == (lub! =), Aby porównać wyliczenia Java

Większość nowych programistów Java szybko dowiaduje się, że powinni raczej porównywać ciągi znaków Java za pomocą String.equals (Object), a nie używać ==. Jest to wielokrotnie podkreślane i wzmacniane przez nowych programistów, ponieważ prawie zawsze mają na celu porównanie zawartości String (rzeczywistych znaków tworzących String), a nie tożsamości String (jego adres w pamięci). Twierdzę, że powinniśmy wzmocnić pojęcie, które ==może być użyte zamiast Enum.equals (Object). Przedstawiam uzasadnienie tego stwierdzenia w pozostałej części tego postu.

Są cztery powody, dla których uważam, że ==porównywanie wyliczeń Java jest prawie zawsze lepsze niż metoda „równa się”:

  1. ==Na teksty stałe zapewnia taką samą oczekiwaną porównania (treść) jakoequals
  2. ==Na teksty stałe jest zapewne bardziej czytelne (mniej gadatliwy) niżequals
  3. ==Na teksty stałe jest bardziej bezpieczny niż nullequals
  4. ==Na teksty stałe zapewnia kompilacji (statyczny) sprawdzanie zamiast sprawdzania wykonawczego

Drugi powód wymieniony powyżej („prawdopodobnie bardziej czytelny”) jest oczywiście kwestią opinii, ale można uzgodnić część dotyczącą „mniej gadatliwości”. Pierwszym powodem, dla którego generalnie preferuję ==porównywanie wyliczeń, jest konsekwencja tego, jak specyfikacja języka Java opisuje wyliczenia. Sekcja 8.9 („Wyliczenia”) mówi:

Próba jawnego utworzenia wystąpienia typu wyliczeniowego jest błędem czasu kompilacji. Ostateczna metoda klonowania w Enum zapewnia, że ​​stałe wyliczenia nie mogą być klonowane, a specjalne traktowanie przez mechanizm serializacji gwarantuje, że zduplikowane wystąpienia nigdy nie są tworzone w wyniku deserializacji. Odblaskowe tworzenie instancji typów wyliczeniowych jest zabronione. Razem te cztery rzeczy zapewniają, że żadne wystąpienia typu wyliczenia nie istnieją poza tymi zdefiniowanymi przez stałe wyliczenia.

Ponieważ istnieje tylko jedno wystąpienie każdej stałej wyliczeniowej, dopuszczalne jest użycie operatora == zamiast metody equals podczas porównywania dwóch odwołań do obiektów, jeśli wiadomo, że co najmniej jedno z nich odwołuje się do stałej wyliczenia. (Metoda equals w Enum jest ostatnią metodą, która po prostu wywołuje super.equals na swoim argumencie i zwraca wynik, wykonując w ten sposób porównanie tożsamości).

Fragment specyfikacji pokazanej powyżej sugeruje, a następnie wyraźnie stwierdza, że ​​użycie ==operatora do porównania dwóch wyliczeń jest bezpieczne, ponieważ nie ma możliwości, aby istniało więcej niż jedno wystąpienie tej samej stałej wyliczenia.

Czwarta zaleta ==nad .equalsporównując teksty stałe ma do czynienia z bezpieczeństwem kompilacji. Użycie ==wymusza bardziej rygorystyczne sprawdzenie czasu kompilacji niż w przypadku, .equalsponieważ Object.equals (Object) musi, zgodnie z umową, przyjąć dowolność Object. Używając języka statycznego, takiego jak Java, wierzę w maksymalne wykorzystanie zalet tego statycznego typowania. W przeciwnym razie użyłbym dynamicznie wpisywanego języka. Uważam, że jednym z powtarzających się tematów Effective Java jest właśnie to: preferuj statyczne sprawdzanie typów, gdy tylko jest to możliwe.

Na przykład załóżmy, że mam wywoływane niestandardowe wyliczenie Fruiti próbuję porównać je z klasą java.awt.Color. Użycie ==operatora pozwala mi uzyskać błąd kompilacji (w tym wcześniejsze powiadomienie w moim ulubionym środowisku Java IDE) o problemie. Oto lista kodu, która próbuje porównać niestandardowe wyliczenie z klasą JDK przy użyciu ==operatora:

/** * 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; } 

Błąd kompilatora jest wyświetlany w migawce ekranu, która jest następna.

Chociaż nie jestem fanem błędów, wolę, aby były one wykrywane statycznie w czasie kompilacji, a nie w zależności od pokrycia w czasie wykonywania. Gdybym użył equalsmetody do tego porównania, kod skompilowałby się dobrze, ale metoda zawsze zwróciłaby falsefałsz, ponieważ nie ma możliwości, aby dustin.examples.Fruitwyliczenie było równe java.awt.Colorklasie. Nie polecam, ale oto metoda porównawcza wykorzystująca .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); } 

„Miłą” rzeczą w powyższym jest brak błędów kompilacji. Pięknie się komponuje. Niestety jest to opłacane potencjalnie wysoką ceną.

Ostatnią zaletą używania ==zamiast Enum.equalsporównywania wyliczeń, którą wymieniłem, jest uniknięcie przerażającego NullPointerException. Jak już wspomniałem w Effective Java NullPointerException Handling, generalnie lubię unikać nieoczekiwanych plików NullPointerExceptions. Istnieje ograniczony zestaw sytuacji, w których naprawdę chcę, aby istnienie wartości zerowej było traktowane jako wyjątkowy przypadek, ale często wolę bardziej wdzięczne zgłaszanie problemu. Zaletą porównywania wyliczeń z ==jest to, że wartość null można porównać z wyliczeniem innym niż null bez napotkania NullPointerException(NPE). Wynik tego porównania jest oczywiście false.

Jednym ze sposobów uniknięcia NPE podczas używania .equals(Object)jest wywołanie equalsmetody względem stałej wyliczenia lub znanego wyliczenia innego niż null, a następnie przekazanie potencjalnego wyliczenia wątpliwego znaku (prawdopodobnie null) jako parametru do equalsmetody. Często robiono to przez lata w Javie za pomocą ciągów znaków, aby uniknąć NPE. Jednak w przypadku ==operatora kolejność porównania nie ma znaczenia. Lubię to.

Przedstawiłem swoje argumenty, a teraz przechodzę do kilku przykładów kodu. Następna lista jest realizacją wspomnianego wcześniej hipotetycznego wyliczenia owoców.

Fruit.java

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

Następna lista kodu to prosta klasa Javy, która udostępnia metody do wykrywania, czy określone wyliczenie lub obiekt jest określonym owocem. Zwykle takie kontrole umieszczałbym w samym wyliczeniu, ale działają one lepiej w osobnej klasie tutaj dla moich celów ilustracyjnych i demonstracyjnych. Klasa ta obejmuje dwie metody pokazane wcześniej do porównywania Fruitsię Colorz zarówno ==i equals. Oczywiście metoda używająca ==do porównania wyliczenia z klasą musiała mieć tę część zakomentowaną, aby poprawnie skompilować.

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); } } 

Postanowiłem podejść do demonstracji pomysłów uchwyconych w powyższych metodach za pomocą testów jednostkowych. W szczególności korzystam z GroovyTestCase Groovy. Ta klasa do korzystania z testów jednostkowych opartych na Groovy znajduje się na następnej liście kodu.

EnumComparisonTest.groovy