Usa == (o! =) Per confrontare Java Enums

La maggior parte dei nuovi sviluppatori Java apprende rapidamente che in genere dovrebbero confrontare le stringhe Java utilizzando String.equals (Object) anziché utilizzare ==. Questo viene sottolineato e rinforzato ripetutamente ai nuovi sviluppatori perché quasi sempre intendono confrontare il contenuto della stringa (i caratteri effettivi che formano la stringa) piuttosto che l'identità della stringa (il suo indirizzo in memoria). Sostengo che dovremmo rafforzare la nozione che ==può essere usata al posto di Enum.equals (Object). Fornisco il mio ragionamento per questa affermazione nel resto di questo post.

Ci sono quattro ragioni per le quali credo che l'utilizzo ==per confrontare le enumerazioni Java sia quasi sempre preferibile rispetto all'utilizzo del metodo "uguale":

  1. Le ==enumerazioni on forniscono lo stesso confronto previsto (contenuto) diequals
  2. Le ==enumerazioni on sono probabilmente più leggibili (meno prolisse) diequals
  3. Le ==enumerazioni on sono più null-safe rispetto aequals
  4. L' ==enumerazione on fornisce il controllo (statico) in fase di compilazione anziché il controllo in fase di esecuzione

Il secondo motivo sopra elencato ("discutibilmente più leggibile") è ovviamente una questione di opinione, ma quella parte su "meno prolisso" può essere concordata. La prima ragione che generalmente preferisco ==quando confronto le enumerazioni è una conseguenza di come la specifica del linguaggio Java descrive le enumerazioni. La sezione 8.9 ("Enum") afferma:

È un errore in fase di compilazione tentare di creare un'istanza esplicita di un tipo enum. Il metodo di clonazione finale in Enum garantisce che le costanti enum non possano mai essere clonate e il trattamento speciale da parte del meccanismo di serializzazione garantisce che le istanze duplicate non vengano mai create come risultato della deserializzazione. L'istanziazione riflessiva dei tipi enum è vietata. Insieme, queste quattro cose assicurano che non esistano istanze di un tipo enum oltre a quelle definite dalle costanti enum.

Poiché esiste una sola istanza di ciascuna costante enum, è consentito utilizzare l'operatore == al posto del metodo equals quando si confrontano due riferimenti a oggetti se è noto che almeno uno di essi si riferisce a una costante enum. (Il metodo equals in Enum è un metodo finale che richiama semplicemente super.equals sul proprio argomento e restituisce il risultato, eseguendo così un confronto di identità.)

L'estratto dalla specifica mostrata sopra implica e quindi afferma esplicitamente che è sicuro usare l' ==operatore per confrontare due enumerazioni perché non è possibile che ci possa essere più di un'istanza della stessa costante enum.

Il quarto vantaggio di ==over .equalsquando si confrontano le enumerazioni ha a che fare con la sicurezza in fase di compilazione. L'uso di ==forza un controllo del tempo di compilazione più rigoroso di quello .equalsperché Object.equals (Object) deve, per contratto, prendere un arbitrario Object. Quando si utilizza un linguaggio tipizzato staticamente come Java, credo che sia necessario trarre vantaggio dai vantaggi di questa digitazione statica il più possibile. Altrimenti, userei un linguaggio digitato dinamicamente. Credo che uno dei temi ricorrenti di Effective Java sia proprio questo: preferire il controllo del tipo statico quando possibile.

Ad esempio, supponiamo di avere un'enumerazione personalizzata chiamata Fruite di aver provato a confrontarla con la classe java.awt.Color. L'utilizzo ==dell'operatore mi consente di ottenere un errore in fase di compilazione (inclusa la notifica anticipata nel mio IDE Java preferito) del problema. Ecco un elenco di codice che cerca di confrontare un'enumerazione personalizzata con una classe JDK utilizzando l' ==operatore:

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

L'errore del compilatore viene mostrato nell'istantanea della schermata successiva.

Sebbene non sia un fan degli errori, preferisco che vengano rilevati staticamente in fase di compilazione piuttosto che a seconda della copertura di runtime. Se avessi utilizzato il equalsmetodo per questo confronto, il codice sarebbe stato compilato correttamente, ma il metodo restituirebbe sempre falsefalse perché non è possibile che un dustin.examples.Fruitenum sia uguale a una java.awt.Colorclasse. Non lo consiglio, ma ecco il metodo di confronto che utilizza .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); } 

La cosa "bella" di quanto sopra è la mancanza di errori in fase di compilazione. Si compila magnificamente. Sfortunatamente, questo viene pagato con un prezzo potenzialmente alto.

Il vantaggio finale che ho elencato di usare ==piuttosto che Enum.equalsquando si confrontano le enumerazioni è l'elusione della temuta NullPointerException. Come ho affermato in Effective Java NullPointerException Handling, generalmente mi piace evitare NullPointerExceptioni messaggi di posta elettronica imprevisti . Esiste una serie limitata di situazioni in cui desidero veramente che l'esistenza di un nullo sia trattata come un caso eccezionale, ma spesso preferisco una segnalazione più aggraziata di un problema. Un vantaggio del confronto di enumerazioni con ==è che un valore nullo può essere confrontato con un valore non nullo senza incontrare un NullPointerException(NPE). Il risultato di questo confronto, ovviamente, è false.

Un modo per evitare l'NPE quando si utilizza .equals(Object)è richiamare il equalsmetodo su una costante enum o un'enumerazione nota non nulla e quindi passare l'enumerazione potenziale di carattere discutibile (possibilmente null) come parametro al equalsmetodo. Questo è stato fatto spesso per anni in Java con stringhe per evitare l'NPE. Tuttavia, con l' ==operatore, l'ordine di confronto non ha importanza. Mi piace.

Ho fatto i miei argomenti e ora passo ad alcuni esempi di codice. L'elenco successivo è una realizzazione dell'ipotetico enumerazione Fruit menzionata in precedenza.

Fruit.java

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

L'elenco di codice successivo è una semplice classe Java che fornisce metodi per rilevare se un particolare oggetto o enum è un determinato frutto. Normalmente metterei controlli come questi nell'enumerazione stessa, ma funzionano meglio in una classe separata qui per i miei scopi illustrativi e dimostrativi. Questa classe comprende i due metodi indicati in precedenza per il confronto Fruita Colorsia con ==e equals. Ovviamente, il metodo utilizzato ==per confrontare un enum con una classe doveva avere quella parte commentata per essere compilato correttamente.

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

Ho deciso di avvicinarmi alla dimostrazione delle idee catturate nei metodi di cui sopra tramite test unitari. In particolare, utilizzo GroovyTestCase di Groovy. Quella classe per l'utilizzo di unit test basati su Groovy è nel prossimo listato di codice.

EnumComparisonTest.groovy