Here’s the solution to the cookies puzzle! As a reminder, the Count class:
package count;
import monster.CookieMonster;
public class Count {
public static void main(String[] args) {
String noCookie = CookieMonster.eat("cookie");
if (noCookie.isEmpty() && CookieMonster.eat(noCookie).length() < noCookie.length()) {
// The goal is to reach this line
System.out.println("Minus one cookie!");
}
}
}
At first sight, it looks as if CookieMonster.eat needs to return a String with a negative length. That sounds like anti-matter. Looking closer, there are two separate requirements: the first time eat is called, it needs to return an empty String; and the second time, it needs to return something with a negative length().
The trick is, a generic method’s return type can be different depending on the type context in which it is used. To demonstrate that, let’s look at an example from java.util.Collections: <T> List<T> emptyList(). In that method signature, <T> says there is type variable T on the method, and List<T> is the return type. You can use this as List<String> strings = Collections.emptyList();. That works because type inference ensures that the T in the return type (List<T>) adjusts to the type of the variable it is assigned to (List<String>). When there is no context (when it is not assigned to anything) then the inferred type will be Object (the implicit upper bound of T). In Collections.emptyList().size() for example emptyList() returns a List<Object>.
Back to our CookieMonster, we can make eat return anything the context expects it to return like this: static <C> C eat(String cookie). That’s like a magic cookie. If you think ‘chocolate’ and put it in our mouth, it’s a chocolate cookie. In Count where it is assigned to a String, it turns into a String cookie (I don’t want to know what that tastes like). That’s a start, but in CookieMonster.eat(noCookie).length(), there is no context, so it turns into Object which doesn’t have a length() method, and that doesn’t compile.
We fix that by putting a bound on C. For both of the calls to eat to compile, the bound needs to be a supertype of String, and have a length() method. There is one such type: CharSequence. So we replace the type variable <C> by <C extends CharSequence>. In the first call eat("cookie") must return an empty String, and in the second call eat("") must return a CharSequence with negative length:
package monster;
public class CookieMonster {
public static <C extends CharSequence> C eat(String cookie) {
return (C) (cookie.length() > 0 ? "" : new CharSequence() {
public int length() {
return -1;
}
public char charAt(int index) {
throw new UnsupportedOperationException();
}
public CharSequence subSequence(int start, int end) {
throw new UnsupportedOperationException();
}
});
}
}
Congrats to the 9 Java monsters who found the solution; see the comments below.
