Results from the Java Deathmatch – A puzzle minigame for developers
A few months ago we released a new side project of ours with a minisite called Java Deathmatch, and since then over 20,000 developers have given it a try. The site features 20 multiple-choice Java questions and today after we’ve gathered stats from all the games that have been played we’re happy to share some of the results and solutions with you.
Overall we collected 61,872 answers, which gives us about 3,094 answers for each of the 20 questions. Each Java deathmatch session randomly chooses 5 questions and gives you 90 seconds to solve each one. Every question has 4 possible answers. We’ve been criticized that the questions are too hard but, well, it’s not called a deathmatch for no reason! Using those stats, we were able to determine which were the hardest questions, and which were the easiest. In this post we’d like to share the 5 toughest questions from this experiment and solve them together.
1. The toughest question of the Java deathmatch
Let’s start with the toughest nut to crack, a question we received from Alexandru-Constantin Bledea from Bucharest. And it’s a real brain teaser. Only 20% of the participants were able to solve this questions. This means if you would have chosen an answer at random – You’d probably have a better chance at hitting the right one. Java generics have this quality about them.
Alright, so what do we have here? We have generics with type erasure involved, and a couple of exceptions. A few things to remember here:
- RuntimeException and SQLException both inherit from Exception, while RuntimeException is unchecked and SQLException is a checked exception.
- Java generics are not reified, meaning that in compile time, the generic type information is “lost” and treated as if the code is replaced with the type’s bound or with Object if it doesn’t exist. This is what you call type erasure.
Naively we’d expect line 7 to cause a compilation error since you can’t cast SQLException to RuntimeException, but that’s not the case. What happens is that T is replaced with Exception so we have:
throw (Exception) t; // t is also an Exception
Since pleaseThrow expects an Exception, and T is replaced with Exception, the cast is eliminated as if it wasn’t written. We can see that in bytecode:
private pleaseThrow(Ljava/lang/Exception;)V throws java/lang/Exception L0 LINENUMBER 8 L0 ALOAD 1 ATHROW L1 LOCALVARIABLE this LTemp; L0 L1 0 // signature LTemp<TT;>; // declaration: Temp<T> LOCALVARIABLE t Ljava/lang/Exception; L0 L1 1 MAXSTACK = 1 MAXLOCALS = 2
Just for fun, we tried to see what the bytecode will look like without generics involved, and the cast appeared right before the ATHROW statement:
Now that we’re convinced there’s no casting involved, we can scratch off these two answers:
- “Compilation fails because we cannot cast SQLException to RuntimeException”
- “Throws ClassCastException because SQLException is not instanceof RuntimeException”
So we throw a SQLException after all, and you’d expect it to get caught by the catch block and get its stack trace. Well, not really. This game is rigged. Turns out the compiler gets confused just as we do, and the code makes it think that the catch block is unreachable. For the unsuspecting bystander, there is no SQLException. The correct answer is that compilation fails because the compiler doesn’t expect a SQLException to be thrown from the try block – When in fact it does get thrown!
Thanks again Alexandru for sharing this question with us!
2. toString(), or not toString(), that is the question
With only 24% of correct answers, the following question was the runner up on the tough scale.
This one is actually much more simple, just from looking at line 12 we can see that this code prints out m1 and m2, rather than, m1.name and m2.name. The tricky part here was remembering that when printing out a class, Java uses its toString method. The “name” field was artificially added. If you miss that and follow the rest of the code correctly, you might be tricked to choose m1 & new name.
This line sets both names to “m1”:
m1.name = m2.name = "m1";
Then callMe sets m2’s name to new name, and we’re done.
But this snippet will actually print out something like this, including the class name and hashcode:
MyClass@3d0bc85 & MyClass@7d08c1b7
And the correct answer would be “None of the above”.
3. Google Guava Sets
This question didn’t really require specific knowledge of Guava sets, but left most of the respondents confused. Only 25% answered it correctly, the same as choosing an answer at random.
So what are we seeing here? We have a method that returns a set containing a “clique” of a person’s best friends. We see that there’s a loop that checks if a person has a best friend, and adds them to the results set. If a person indeed has a best friend, it repeats the process for them, so we end up having a set of best friends until we reach a person who doesn’t have a best friend OR that its best friend is already in the set. That last part might be a bit tricky – we can’t add a person who is already in the set so there’s no potential for an infinite loop.
The problem here is that we’re risking an out of memory exception. There’s no bound on the set so we can keep adding and adding people until we run out of memory.
By the way, if you’re into Google Guava, check out this post we wrote about some of the lesser known yet useful features about it.
4. Double brace initialization, lol wut?!
This one was one of the shortest questions, but it was enough to get most of the developers confused. Only 26% got it right.
Not many developers are aware of this syntax that comes in handy when you need to initialize a constant collection, although some side-effects are included. Actually, this lack of popularity might be a good thing. So when the WAT?! effect wears off, you can see that we add an element to the list, and then try to print it out. Normally you’d expect it to print out [John] but double brace initialization has other plans in mind. What we see here is an anonymous class that is used to initialize the List. When it tries to print out NAMES, it actually comes out as null. Since the initializer wasn’t consumed yet and the list is empty.
You can read more about double brace initialization right here.
5. The curious case of the map at runtime
This one is another community-contributed question coming from Barak Yaish from Israel. Only 27% of the participants were able to solve this question.
Alright, compute looks up a value in the map. If it’s null, it adds it and returns its value. Since the list is empty, “foo” doesn’t exist, v is null, and we map “foo” to a new ArrayList<Object>(). The ArrayList is empty, so it prints out .
For the second line, “foo” does exist in the map so we evaluate the expression on the right. The ArrayList is cast to a List successfully, and “ber” is added to it. add returns true and that’s what it prints out.
The correct answer is  true. Thanks again Barak for sharing this question with us!
Bonus: And the easiest question is…
This time we have a question coming from Peter Lawrey of OpenHFT who also blogs on Vanilla Java. Peter is on the top 50 list of StackOverflow and this time he moved over to the other side and asked a question that 76% of you got right.
Answer C is simpler than A, B & D doesn’t compile.
From time to time we really like playing this kind of puzzles to sharpen our Java knowledge, but if you ever find yourself spending too much time on these puzzlers in your own codebase, it will probably be less than ideal. Especially if someone calls in the middle of the night to fix a critical production error. For this kind of situation, we’ve built Takipi for Java. Takipi is a Java agent that knows how to track uncaught exceptions, caught exceptions and log errors on servers in production. It lets you see the variable values that cause errors, all across the stack, and overlays them on your code.
|Reference:||4 out of 5 Java Developers Failed to Solve This Question from our JCG partner Alex Zhitnitsky at the Takipi blog.|