Die Enthält eine Falle in Java-Sammlungen

Eine der bösen kleinen Fallen, in die ein Java-Entwickler geraten kann, tritt auf, wenn Collection.contains (Object) nicht mit entsprechendem Verständnis verwendet wird. Ich demonstriere diese potenzielle Falle in diesem Beitrag.

Collection.contains (Object) akzeptiert ein Objekt, dh es akzeptiert im Wesentlichen eine Instanz einer Java-Klasse. Hier liegt die potenzielle Falle. Wenn eine Instanz einer anderen Klasse als der Klassentyp übergeben wird, der in einer bestimmten Sammlung gespeichert werden kann, kann es sein, dass diese Methode einfach zurückgegeben wird false. Dies ist nicht wirklich falsch, da ein anderer Typ als die Sammlung offensichtlich nicht Teil dieser Sammlung ist. Es kann jedoch eine Falle sein, wenn sich der Entwickler auf den vom containsAufruf zurückgegebenen Wert verlässt , um eine andere Logik zu implementieren.

Dies wird in der nächsten Codeliste demonstriert.

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

In der obigen Codeliste steht die Aussage "Das ist ein großartiges Buch!" wird niemals ausgeführt, da ein Datum niemals in diesem Set enthalten sein wird.

Es gibt keine Warnbedingung dafür, selbst wenn die -XlintOption von javac festgelegt ist. NetBeans 6.8 bietet hierfür jedoch eine Warnung, wie im nächsten Screenshot gezeigt.

Wie aus dem Screenshot hervorgeht, enthält NetBeans 6.8 die nette und ziemlich klare Warnmeldung "Verdächtiger Aufruf von java.util.Collection.contains: Das angegebene Objekt darf keine Instanzen von Date (erwartete Zeichenfolge) enthalten." Dies ist definitiv "verdächtig" und fast nie das, was der Entwickler wirklich wollte.

Es ist nicht unbedingt überraschend, dass die containsMethode falseeher eine Art Fehlermeldung oder Ausnahme zurückgibt, da es sicher stimmt, dass die Setin diesem Beispiel nicht die enthielt, Datefür die die Frage gestellt wurde. Eine Taktik, mit der mindestens eine Laufzeitprüfung für die richtige Klasse in einem Aufruf von durchgeführt werden kann, containsbesteht darin, einen Auflistungstyp zu verwenden, der gegebenenfalls das optionale Auslösen einer ClassCastException implementiert.

In der Javadoc-Dokumentation für die jeweiligen containsMethoden der Schnittstellen Collection, Set, List und Map wird angegeben , dass sie ClassCastException"wenn der Typ des angegebenen Elements mit dieser Sammlung nicht kompatibel ist (optional)" (Collection) "auslösen, wenn der Typ der angegebenen Element ist mit dieser Menge nicht kompatibel (optional) "(Menge)", wenn der Typ des angegebenen Elements mit dieser Liste nicht kompatibel ist (optional) "(Liste) und" wenn der Schlüssel von einem für diese Zuordnung ungeeigneten Typ ist (optional) ) "(Map.containsKey). Das Wichtigste ist, dass jeder von ihnen das Werfen ClassCastExceptionals optional deklariert .

Im obigen Code habe ich ein HashSet verwendet, das kein a auslöst, ClassCastExceptionwenn ein inkompatibler Objekttyp an seine containsMethode übergeben wird. In der Javadoc-Dokumentation für HashSet.contains (Object) wird das Auslösen von a nicht erwähnt ClassCastException. In ähnlicher Weise erweitert HashSetund erbt LinkedHashSet dasselbe containswie HastSet. Das TreeSet hingegen enthält Javadoc-Kommentare, die besagen, dass TreeSet.contains (Object) ein ClassCastException"Wenn das angegebene Objekt nicht mit den aktuell im Set enthaltenen Elementen verglichen werden kann" auslöst . Das TreeSetlöst also eine Ausnahme aus, wenn ein unvergleichliches Objekt für seine containsMethode bereitgestellt wird.

Ich werde nun die Unterschiede in diesen Verhaltensweisen anhand einiger Codebeispiele demonstrieren.

Die erste Klasse, die hier verwendet wird, ist die Personenklasse.

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

Eine andere in meinen Beispielen verwendete Klasse ist die InanimateObject-Klasse.

Nicht vergleichbare InanimateObject.java

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

Die ausführbare Hauptklasse zum Testen dieser Dinge ist 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"); } } 

Wenn der obige Code unverändert ausgeführt wird (ohne zu InanimateObjectsein Comparable), wird die Ausgabe wie im nächsten Screenshot gezeigt angezeigt.

Das ClassCastExceptionsagt uns: "Dustin.examples.InanimateObject kann nicht in java.lang.Comparable umgewandelt werden." Dies ist sinnvoll, da diese Klasse Comparable nicht implementiert, was für die Verwendung mit der TreeMap.contains(Object)Methode erforderlich ist . Wenn Comparabledie Klasse erstellt ist, sieht sie ungefähr so ​​aus, wie sie als nächstes gezeigt wird.

Vergleichbare InanimateObject.java