Il contiene trap nelle raccolte Java

Una delle piccole e spiacevoli trappole in cui uno sviluppatore Java può incorrere si verifica quando Collection.contains (Object) non viene utilizzato con una comprensione adeguata. Dimostro questa potenziale trappola in questo post.

Collection.contains (Object) accetta un Object, il che significa che essenzialmente accetta un'istanza di qualsiasi classe Java. È qui che si trova la potenziale trappola. Se si passa in un'istanza di una classe diversa dal tipo di classi che possono essere archiviate in una particolare raccolta, è possibile che questo metodo restituisca semplicemente false. Questo non è davvero sbagliato perché un tipo diverso da quello della raccolta ovviamente non fa parte di quella raccolta. Tuttavia, può essere una trappola se lo sviluppatore si affida al valore restituito dalla containschiamata per implementare altra logica.

Ciò è dimostrato nel prossimo listato di codice.

 public void demonstrateIllConceivedContainsBasedCode() { final Set favoriteChildrensBooks = new HashSet(); favoriteChildrensBooks.add("Mrs. Frisby and the Rats of NIMH"); favoriteChildrensBooks.add("The Penguin that Hated the Cold"); favoriteChildrensBooks.add("The Bears' Vacation"); favoriteChildrensBooks.add("Green Eggs and Ham"); favoriteChildrensBooks.add("A Fish Out of Water"); favoriteChildrensBooks.add("The Lorax"); final Date date = new Date(); if (favoriteChildrensBooks.contains(date)) { out.println("That is a great book!"); } } 

Nell'elenco di codici sopra, la dichiarazione che stampa "Questo è un grande libro!" non verrà mai eseguito perché una Data non sarà mai contenuta in quel Set.

Non ci sono condizioni di avviso per questo, anche con le -Xlintopzioni di javac impostate. Tuttavia, NetBeans 6.8 fornisce un avviso per questo, come mostrato nell'istantanea della schermata successiva.

Come indica l'istantanea della schermata, NetBeans 6.8 fornisce il messaggio di avviso carino e abbastanza chiaro, "Chiamata sospetta a java.util.Collection.contains: l'oggetto specificato non può contenere istanze di Date (stringa prevista)". Questo è decisamente "sospetto" e non è quasi mai ciò che lo sviluppatore desiderava veramente.

Non è necessariamente sorprendente che il containsmetodo restituisca falsepiuttosto che un qualche tipo di messaggio di errore o eccezione perché è certamente vero che Setin questo esempio non conteneva il Dateper il quale era stata posta la domanda. Una tattica che può essere utilizzata per avere almeno un controllo in fase di esecuzione per la classe corretta in una chiamata a containsconsiste nell'usare un tipo di raccolta che implementa il lancio facoltativo di un ClassCastException quando appropriato.

La documentazione Javadoc per i rispettivi containsmetodi delle interfacce Collection, Set, List e Map afferma che essi generano ClassCastException"se il tipo dell'elemento specificato è incompatibile con questa raccolta (opzionale)" (Collection), "se il tipo di elemento è incompatibile con questo set (opzionale) "(Set)," se il tipo di elemento specificato è incompatibile con questo elenco (opzionale) "(List) e" se la chiave è di un tipo inappropriato per questa mappa (opzionale ) "(Map.containsKey). La cosa più importante da notare è che ognuno di questi dichiara il lancio di ClassCastExceptioncome opzionale .

Nel codice sopra, ho usato un HashSet, che non genera un ClassCastExceptionquando un tipo di oggetto incompatibile viene passato al suo containsmetodo. In effetti, la documentazione Javadoc per HashSet.contains (Object) non fa menzione del lancio di un file ClassCastException. Allo stesso modo, LinkedHashSet estende HashSeted eredita lo stesso containsdi HastSet. Il TreeSet, d'altra parte, ha commenti Javadoc che affermano che TreeSet.contains (Object) genera un ClassCastException"se l'oggetto specificato non può essere confrontato con gli elementi attualmente nel set". Quindi TreeSetgenera un'eccezione quando un oggetto incomparabile viene fornito al suo containsmetodo.

Dimostrerò ora le differenze in questi comportamenti con alcuni esempi di codice.

La prima classe da utilizzare qui è la classe Person.

Person.java

/* * //marxsoftware.blogspot.com/ */ package dustin.examples; import java.io.Serializable; public final class Person implements Comparable, Serializable { private final String lastName; private final String firstName; public Person(final String newLastName, final String newFirstName) { this.lastName = newLastName; this.firstName = newFirstName; } public String getLastName() { return this.lastName; } public String getFirstName() { return this.firstName; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Person other = (Person) obj; if (this.lastName == null ? other.lastName != null : !this.lastName.equals(other.lastName)) { return false; } if (this.firstName == null ? other.firstName != null : !this.firstName.equals(other.firstName)) { return false; } return true; } @Override public int hashCode() { int hash = 5; hash = 59 * hash + (this.lastName != null ? this.lastName.hashCode() : 0); hash = 59 * hash + (this.firstName != null ? this.firstName.hashCode() : 0); return hash; } public int compareTo(Object anotherPerson) throws ClassCastException { if (!(anotherPerson instanceof Person)) { throw new ClassCastException("A Person object expected."); } final Person theOtherPerson = (Person) anotherPerson; final int lastNameComparisonResult = this.lastName.compareTo(theOtherPerson.lastName); return lastNameComparisonResult != 0 ? lastNameComparisonResult : this.firstName.compareTo(theOtherPerson.firstName); } @Override public String toString() { return this.firstName + " " + this.lastName; } } 

Un'altra classe usata nei miei esempi è la classe InanimateObject.

InanimateObject.java non comparabile

/* * //marxsoftware.blogspot.com/ */ package dustin.examples; public class InanimateObject { private final String name; private final String secondaryName; private final int yearOfOrigin; public InanimateObject( final String newName, final String newSecondaryName, final int newYear) { this.name = newName; this.secondaryName = newSecondaryName; this.yearOfOrigin = newYear; } public String getName() { return this.name; } public String getSecondaryName() { return this.secondaryName; } public int getYearOfOrigin() { return this.yearOfOrigin; } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final InanimateObject other = (InanimateObject) obj; if (this.name == null ? other.name != null : !this.name.equals(other.name)) { return false; } if (this.yearOfOrigin != other.yearOfOrigin) { return false; } return true; } @Override public int hashCode() { int hash = 3; hash = 23 * hash + (this.name != null ? this.name.hashCode() : 0); hash = 23 * hash + this.yearOfOrigin; return hash; } @Override public String toString() { return this.name + " (" + this.secondaryName + "), created in " + this.yearOfOrigin; } } 

La principale classe eseguibile per testare queste cose è SetContainsExample.

SetContainsExample.java

/* * //marxsoftware.blogspot.com/ */ package dustin.examples; import static java.lang.System.out; import java.util.Arrays; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; public class SetContainsExample { final Person davidLightman = new Person("Lightman", "David"); final Person willFarmer = new Person("Farmer", "Will"); final Person daveBowman = new Person("Bowman", "Dave"); final Person jerryShaw = new Person("Shaw", "Jerry"); final Person delSpooner = new Person("Spooner", "Del"); final InanimateObject wopr = new InanimateObject("War Operation Plan Response", "WOPR", 1983); final InanimateObject ripley = new InanimateObject("R.I.P.L.E.Y", "R.I.P.L.E.Y", 2008); final InanimateObject hal = new InanimateObject("Heuristically programmed ALgorithmic Computer", "HAL9000", 1997); final InanimateObject ariia = new InanimateObject("Autonomous Reconnaissance Intelligence Integration Analyst", "ARIIA", 2009); final InanimateObject viki = new InanimateObject("Virtual Interactive Kinetic Intelligence", "VIKI", 2035); public Set createPeople(final Class setType) { Set people = new HashSet(); if (validateSetImplementation(setType)) { if (HashSet.class.equals(setType)) { people = new HashSet(); } else if (LinkedHashSet.class.equals(setType)) { people = new LinkedHashSet(); } else if (TreeSet.class.equals(setType)) { people = new TreeSet(); } else if (EnumSet.class.equals(setType)) { out.println("ERROR: EnumSet is inappropriate type of Set here."); } else { out.println("WARNING: " + setType.getName() + " is an unexpected Set implementation."); } } else { out.println("WARNING: " + setType.getName() + " is not a Set implementation."); people = new HashSet(); } people.add(davidLightman); people.add(willFarmer); people.add(daveBowman); people.add(jerryShaw); people.add(delSpooner); return people; } private boolean validateSetImplementation(final Class candidateSetImpl) { if (candidateSetImpl.isInterface()) { throw new IllegalArgumentException( "Provided setType needs to be an implementation, but an interface [" + candidateSetImpl.getName() + "] was provided." ); } final Class[] implementedInterfaces = candidateSetImpl.getInterfaces(); final List implementedIFs = Arrays.asList(implementedInterfaces); return implementedIFs.contains(java.util.Set.class) || implementedIFs.contains(java.util.NavigableSet.class) || implementedIFs.contains(java.util.SortedSet.class); } public void testSetContains(final Set set, final String title) { printHeader(title); out.println("Chosen Set Implementation: " + set.getClass().getName()); final Person person = davidLightman; out.println( set.contains(person) ? person + " is one of my people." : person + " is NOT one of my people."); final Person luke = new Person("Skywalker", "Luke"); out.println( set.contains(luke) ? luke + " is one of my people." : luke + " is NOT one of my people."); out.println( set.contains(wopr) ? wopr + " is one of my people." : wopr + " is NOT one of my people."); } private void printHeader(final String headerText) { out.println(); out.println( "=================================================================="); out.println("== " + headerText); out.println( "=================================================================="); } public static void main(final String[] arguments) { final SetContainsExample me = new SetContainsExample(); final Set peopleHash = me.createPeople(HashSet.class); me.testSetContains(peopleHash, "HashSet"); final Set peopleLinkedHash = me.createPeople(LinkedHashSet.class); me.testSetContains(peopleLinkedHash, "LinkedHashSet"); final Set peopleTree = me.createPeople(TreeSet.class); me.testSetContains(peopleTree, "TreeSet"); } } 

Quando il codice precedente viene eseguito così com'è (senza InanimateObjectessere Comparable), l'output viene visualizzato come mostrato nell'istantanea della schermata successiva.

Il ClassCastExceptionci dice, "dustin.examples.InanimateObject non può essere gettato a java.lang.Comparable." Questo ha senso perché quella classe non implementa Comparable, che è necessario per l'uso con il TreeMap.contains(Object)metodo. Una volta creata Comparable, la classe ha un aspetto simile a quello mostrato di seguito.

Comparable InanimateObject.java