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.