Thursday, March 03, 2005

When the Java three-part expression is not equivalent

What is wrong with this code?

return -1 == endIndex
    ? Collections.emptyMap()
    : makeMap(path.substring(endIndex + 1));

Here's a hint:

private static Map<String, String> makeMap(final String s) {
    final Map<String, String> map = new HashMap<String, String>();

    for (final String pair : split(s, ';')) {
        final int i = pair.indexOf('=');

        if (-1 == i)
            map.put(pair, null);
        else
            map.put(pair.substring(0, i), pair.substring(i + 1));
    }

    return map;
}

Ok, now do you see? What about now:

public Map<String, String> getServiceSpecifiers() {
    final String path = uri.getPath();
    // Ignore service specifier (e.g., ftp)
    final int endIndex = path.indexOf(';');

    return -1 == endIndex
        ? Collections.emptyMap()
        : makeMap(path.substring(endIndex + 1));
}

The return type is Map<String, String>, and Collections.emptyMap() is defined as:

public static final <K,V> Map<K,V> emptyMap() {
    return (Map<K,V>) EMPTY_MAP;
}

So what is really the matter? Last clue; this fixes the problem:

if (-1 == endIndex)
    return Collections.emptyMap();
else
    return makeMap(path.substring(endIndex + 1));

The problem is generics.

The compiler deduces the bounds correctly in return Collections.emptyMap() but not when in the three-part expression (X ? Y : Z). The compiler tries to harmonize the types when evaluating the expression, and this leads to picking the lowest bounds possible, Object rather than String. The expression is then returned, but now it no longer meets the bounds for the method and it is a syntax error.

The IF statement does not have the extra bounds deduction, so reduces the empty map to the bounds of the method exactly.

How annoying.

UPDATE: An anonymous commenter pointed out the correct Java syntax to pull this off:

return -1 == endIndex
    ? Collections.<String, String>emptyMap()
    : makeMap(path.substring(endIndex + 1));

My C++ blinders kept me from considering this possibility; I tried putting the generic specification in several other places in the statement instead, none of which worked. An interesting construction.

UPDATE: Paul Holser pointed out the right name for this construct: the ternary conditional operator.

2 comments:

Anonymous said...

This is annoying once and then you get used to it. The way to fix it? Instead of

return -1 == endIndex
? Collections.emptyMap()
: makeMap(path.substring(endIndex + 1));

do this:

return -1 == endIndex
? Collections.<String,String>emptyMap()
: makeMap(path.substring(endIndex + 1));

Brian Oxley said...

Hey, thanks! I tried putting the <String, String> in several places but not between the dot add emptyMap(). Cool.