samedi 24 septembre 2011

Java Puzzler - 57 is back !

Detecting a bug and solving it may be very frustrating, but it becomes enjoyable when you try to turn it into a challenge. Once again, I ran into a bug. Once again, I did not see it comming and once again, I will turn this into a puzzler.

No XML Serialization this time, but the same pattern : You will see the code and will be asked to guess what it produces.


Puzzler

The following program tries to compare the method equals and containsAll of java sets and lists. It creates one set and one list with a regex pattern that matches "Mickey" and "mouse". What does the program print ?

public static void main(String... args) {
    System.out.println(prepareList().equals(prepareSet()));
    System.out.println(prepareList().containsAll(prepareSet()));
}

private static List<Pattern> prepareList() {
    List<Pattern> result = new ArrayList<>();
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    return result;
}

private static Set<Pattern> prepareSet() {
    Set<Pattern> result = new HashSet<>();
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    return result;
}

What do we expect ?

This program seems pretty straightforward. It compares two collections containing the same Pattern. If we take a brief look at the javadoc of the List interface, we see that the equals method should return true "if and only if the specified object is also a list, both lists have the same size, and all corresponding pairs of elements in the two lists are equal". We expect the first statement to print false for a Set is not a List. The containsAll method takes a Collection, which is the superinterface of Set and checks if the list "contains all of the elements of the specified collection". Therefore the program should print :

false
true

What do we have ?

If you ran it, you saw a different thing :

false
false

Our first assumption seems correct, but not the second one. What can be wrong ?


Solution

Our program only uses base classes of the Java platform. Set and List classes are used in almost every Java application, so they are hardly arguable. Our only chance is to create a test that will first assert that two identical Pattern does equal.

public static void main(String... args) {
    Pattern original = Pattern.compile("[Mm](ickey|ouse)");
    Pattern copy = Pattern.compile("[Mm](ickey|ouse)");
    System.out.println(original.equals(copy));
}

If you ran this test, you saw that it print false, which means that two identical patterns does not equal.

Indeed, if you look closely to the source code of the Pattern class, you will see that it does not override the equals() method of java.lang.Object, nor does it override the hashCode() method. The following program will therefore print "3", because there is 3 "distinct" patterns in the set.

public static void main(String... args) {
    Set<Pattern> result = new HashSet<>();
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    result.add(Pattern.compile("[Mm](ickey|ouse)"));
    System.out.println(result.size());
}

We could override hashCode() and equals() with the following implementations to solve the problem. However, to make things worse, the Pattern class is final, so we must create a wrapper and all sort of delegates methods.

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    return this.toString().equals(o.toString());
}

@Override
public int hashCode() {
    return this.toString().hashCode();
}

The lesson of this puzzle is : when comparing patterns, compare the toString() results instead of the objects themselves.

As i said in my previous article, if you haven't read the book "Java Puzzlers" yet, I urge you to do so. If you have read it, you may have noticed that this puzzler is quite similar with Puzzler #57 - "What's in a Name ?", which also explains the title of this article.

Do know why this implementation choice was made ? Did you run into this bug and find another workaround ? Let's hear about it in the comments.

Aucun commentaire:

Enregistrer un commentaire