Utilisez == (ou! =) Pour comparer les énumérations Java

La plupart des nouveaux développeurs Java apprennent rapidement qu'ils doivent généralement comparer les chaînes Java en utilisant String.equals (Object) plutôt qu'en utilisant ==. Ceci est souligné et renforcé à plusieurs reprises pour les nouveaux développeurs car ils veulent presque toujours comparer le contenu de la chaîne (les caractères réels formant la chaîne) plutôt que l'identité de la chaîne (son adresse en mémoire). Je soutiens que nous devrions renforcer la notion qui ==peut être utilisée à la place de Enum.equals (Object). Je donne mon raisonnement pour cette affirmation dans le reste de ce post.

Il y a quatre raisons pour lesquelles je pense que l'utilisation ==de la comparaison des énumérations Java est presque toujours préférable à l'utilisation de la méthode "equals":

  1. L' ==énumération on fournit la même comparaison attendue (contenu) queequals
  2. L' ==énumération on est sans doute plus lisible (moins verbeuse) queequals
  3. L' ==énumération on est plus sûre pour les valeurs nulles queequals
  4. Les ==enums on fournit une vérification à la compilation (statique) plutôt qu'une vérification à l'exécution

La deuxième raison énumérée ci-dessus ("sans doute plus lisible") est évidemment une question d'opinion, mais cette partie concernant "moins verbeux" peut être acceptée. La première raison que je préfère généralement ==lors de la comparaison des énumérations est une conséquence de la façon dont la spécification du langage Java décrit les énumérations. La section 8.9 («Enums») stipule:

Tenter d'instancier explicitement un type enum est une erreur de compilation. La méthode de clonage finale dans Enum garantit que les constantes d'énumération ne peuvent jamais être clonées, et le traitement spécial par le mécanisme de sérialisation garantit que les instances dupliquées ne sont jamais créées à la suite de la désérialisation. L'instanciation réfléchissante des types enum est interdite. Ensemble, ces quatre éléments garantissent qu'aucune instance d'un type enum n'existe au-delà de celles définies par les constantes enum.

Comme il n'y a qu'une seule instance de chaque constante d'énumération, il est permis d'utiliser l'opérateur == à la place de la méthode equals lors de la comparaison de deux références d'objet si l'on sait qu'au moins l'une d'entre elles fait référence à une constante d'énumération. (La méthode equals dans Enum est une méthode finale qui appelle simplement super.equals sur son argument et renvoie le résultat, effectuant ainsi une comparaison d'identité.)

L'extrait de la spécification ci-dessus implique et déclare ensuite explicitement qu'il est sûr d'utiliser l' ==opérateur pour comparer deux énumérations car il n'y a aucun moyen qu'il puisse y avoir plus d'une instance de la même constante enum.

Le quatrième avantage à ==sur .equalslors de la comparaison des énumérations a à voir avec la sécurité au moment de la compilation. L'utilisation de ==force un contrôle de temps de compilation plus strict que celui de .equalsparce que Object.equals (Object) doit, par contrat, prendre une valeur arbitraire Object. Lors de l'utilisation d'un langage à typage statique tel que Java, je crois qu'il est important de profiter au maximum des avantages de ce typage statique. Sinon, j'utiliserais un langage typé dynamiquement. Je crois que l'un des thèmes récurrents de Effective Java est juste cela: préférez la vérification de type statique chaque fois que possible.

Par exemple, supposons que j'aie appelé une énumération personnalisée Fruitet que j'essaie de la comparer à la classe java.awt.Color. L'utilisation de l' ==opérateur me permet d'obtenir une erreur de compilation (y compris un préavis dans mon IDE Java préféré) du problème. Voici une liste de codes qui tente de comparer une énumération personnalisée à une classe JDK à l'aide de l' ==opérateur:

/** * 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'erreur du compilateur est affichée dans la capture d'écran suivante.

Bien que je ne sois pas fan des erreurs, je préfère qu'elles soient capturées de manière statique au moment de la compilation plutôt que de dépendre de la couverture d'exécution. Si j'avais utilisé la equalsméthode pour cette comparaison, le code se serait bien compilé, mais la méthode retournerait toujours falsefalse car il n'y a aucun moyen pour qu'une dustin.examples.Fruiténumération soit égale à une java.awt.Colorclasse. Je ne le recommande pas, mais voici la méthode de comparaison utilisant .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 "belle" chose à propos de ce qui précède est le manque d'erreurs de compilation. Il compile magnifiquement. Malheureusement, cela est payé avec un prix potentiellement élevé.

Le dernier avantage que j'ai énuméré en utilisant ==plutôt que Enum.equalslors de la comparaison des énumérations est d'éviter la redoutable NullPointerException. Comme je l'ai indiqué dans la gestion efficace des exceptions Java NullPointerException, j'aime généralement éviter les NullPointerExceptions inattendus . Il existe un ensemble limité de situations dans lesquelles je veux vraiment que l'existence d'un nul soit traitée comme un cas exceptionnel, mais souvent je préfère un rapport plus gracieux d'un problème. Un avantage de la comparaison des énumérations avec ==est qu'un null peut être comparé à un enum non nul sans rencontrer de NullPointerException(NPE). Le résultat de cette comparaison est évidemment false.

Une façon d'éviter le NPE lors de l'utilisation .equals(Object)consiste à appeler la equalsméthode par rapport à une constante d'énumération ou à une énumération non nulle connue, puis à transmettre l'énumération potentielle du caractère douteux (éventuellement nul) en tant que paramètre à la equalsméthode. Cela a souvent été fait pendant des années en Java avec des chaînes pour éviter le NPE. Cependant, avec l' ==opérateur, l'ordre de comparaison n'a pas d'importance. J'aime ça.

J'ai fait mes arguments et maintenant je passe à quelques exemples de code. La liste suivante est une réalisation de l'énumération hypothétique de Fruit mentionnée précédemment.

Fruit.java

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

La liste de code suivante est une classe Java simple qui fournit des méthodes pour détecter si une énumération ou un objet particulier est un certain fruit. Je mettrais normalement des chèques comme ceux-ci dans l'énumération elle-même, mais ils fonctionnent mieux dans une classe séparée ici à des fins d'illustration et de démonstration. Cette classe comprend les deux méthodes présentées précédemment pour comparer Fruità la Colorfois avec ==et equals. Bien sûr, la méthode utilisant ==pour comparer une énumération à une classe devait avoir cette partie commentée pour compiler correctement.

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

J'ai décidé d'aborder la démonstration des idées capturées dans les méthodes ci-dessus via des tests unitaires. En particulier, j'utilise GroovyTestCase de Groovy. Cette classe d'utilisation des tests unitaires alimentés par Groovy se trouve dans la liste de code suivante.

EnumComparisonTest.groovy