Java Puzzle 7: Cookies - Solution
Aug 12, 2012Here'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.
9 comments
My Cookie Monster:
That was tough, thanks for the challenge!
As I am pretty sure you cannot get a genuine java.lang.String with negative length, let’s use some unchecked generics magic to have the method return a String in the first (to avoid ClassCastExceptions) and another CharSequence with negative length in the second case.
Using a StringBuilder instead of the Segment would also work (use two threads to get the count negative), or just implement your own implementation of CharSequence. But I think this is the shortest solution :-)
Nice one, but not too tricky. The key term here is “type inference”. We make the monster return a String for each “real” cookie and only a (“negative”) CharSequence when given no cookie - that one can be customized to have negative length. Since the “noCookie” variable is declared to be String, the compiler will do the casting.
Both casts give unchecked casts warnings:
Another great puzzle. Thanks, keep them coming.