Несколько месяцев назад мы выпустили наш новый сторонний проект с мини-сайтом под названием Java Deathmatch (мини-игра-головоломка для разработчиков), и с тех пор более 20 000 разработчиков попробовали его. На сайте представлены 20 вопросов с несколькими вариантами ответов по Java, и сегодня, после того, как мы собрали статистику по всем играм, в которые мы играли, мы будем рады поделиться некоторыми результатами и решениями с вами.
В целом мы собрали 61 872 ответа, что дает нам около 3094 ответов на каждый из 20 вопросов. Каждый сеанс Java deathmatch случайным образом выбирает 5 вопросов и дает вам 90 секунд на решение каждого из них. На каждый вопрос есть 4 возможных ответа. Нас критиковали за то, что вопросы слишком сложные, но, ну, это не называется смертельным матчем без причины! Используя эту статистику, мы смогли определить, какие вопросы были самыми сложными, а какие — самыми простыми. В этом посте мы хотели бы поделиться 5 сложнейшими вопросами из этого эксперимента и решить их вместе.
Новое сообщение: 4 из 5 разработчиков Java не смогли решить этот вопрос http://t.co/b9m6b9NfHM pic.twitter.com/2QmnHcQLRo
— Такипи (@takipid) 27 июля 2015 г.
В среднем 41% попыток ответов были правильными, что совсем неплохо. Живая статистика результатов и вопросы по индексу доступны прямо здесь . Статистика этого поста — снимок с 26 июля. Проверьте Java Deathmatch для полной викторины.
1. Самый сложный вопрос о Java Deathmatch
Давайте начнем с самого прочного ореха, который мы получили от Александра-Константина Бледеа из Бухареста. И это настоящая головоломка. Только 20% участников смогли решить эти вопросы. Это означает, что если бы вы выбрали случайный ответ — у вас, вероятно, был бы лучший шанс выбрать правильный ответ. У дженериков Java есть это качество.
Хорошо, так что у нас здесь? У нас есть дженерики с удалением типов и несколько исключений. Несколько вещей, чтобы помнить здесь:
1. RuntimeException и SQLException оба наследуются от Exception, в то время как RuntimeException не проверено, а SQLException является проверенным исключением.
2. Обобщения Java не реализованы, что означает, что во время компиляции информация об общем типе «теряется» и обрабатывается так, как если бы код заменялся связью типа или Object, если он не существует. Это то, что вы называете стиранием типа.
Наивно, мы ожидаем, что строка 7 вызовет ошибку компиляции, поскольку вы не можете привести SQLException к RuntimeException, но это не так. Что происходит, так это то, что T заменяется на Exception, поэтому мы имеем:
throw (Exception) t; // t is also an Exception
Так как pleaseThrow ожидает исключение , а T заменяется на исключение , приведение исключается, как если бы оно не было записано. Мы можем видеть это в байт-коде:
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
Ради интереса мы попытались увидеть, как будет выглядеть байт-код без использования обобщений, и приведение появилось прямо перед оператором ATHROW :
CHECKCAST java/lang/RuntimeException
Теперь, когда мы убеждены, что приведение
не выполняется, мы можем вычеркнуть два следующих ответа: «Сбой компиляции, потому что мы не можем привести SQLException к RuntimeException»
«Выдает ClassCastException, поскольку SQLException не является экземпляром RuntimeException»
В конце концов, мы бросаем SQLException, и вы ожидаете, что он будет захвачен блоком catch и получит трассировку его стека. Ну не совсем. Эта игра сфальсифицирована. Оказывается, что компилятор запутывается так же, как и мы, и код заставляет его думать, что блок catch недоступен. Для ничего не подозревающего свидетеля SQLException не существует . Правильный ответ заключается в том, что компиляция не удалась, потому что компилятор не ожидает, что SQLException будет выброшено из блока try.
Еще раз спасибо Александру за то, что поделились этим вопросом с нами! Еще один крутой способ увидеть, что именно здесь не так и как на самом деле генерируется SQLException, — заменить блок catch и заставить его ожидать вместо этого RuntimeException. Таким образом, вы увидите фактическую трассировку стека SQLException.
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.
Conclusion
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.