Совсем недавно я сдал OCA Java SE 7 Programmer I. За время подготовки успел решить огромное количество задач и извлечь из них много тонких моментов языка. Самые интересные и ловкие — сохранял на будущее. И вот у меня накопилась небольшая личная коллекция, лучшую часть которой я здесь и опишу.
В замечательной статье Знаешь ли ты JAVA, %username% и её второй части автор тоже поделился опытом после подготовки. Однако я пришёл к выводу, что могу добавить что-то своё. Так и зародилась эта статья.
Задачи
Итак, начнём. Я разбил все хитрости на маленькие задачки, которые составил специально для вас. Тонкости языка выделяются в чистом виде — без лишних наворотов и заблуждений, как это было в тестах. Также я рекомендую вам сначала ответить на вопрос с точностью до символа и записать куда-нибудь, а потом уже смотреть правильный ответ. Интересно, сколько пользователей, решивших этот тест, ответит больше чем на половину? И не забывайте, что все эти примеры ориентированы на Java 7.
1)Скомпилируется ли данный код и если да — то каким будет вывод?
long year = 201l;
System.out.print(year);
В английском языке строчная буква l очень похожа на цифру 1. И этим примером я хочу вас предостеречь — никогда не используйте l маленькую для обозначения long-литералов, хотя Java это позволяет. И вообще не используете её там, где потенциально может быть единица. Просто возьмите за правило использовать прописную L, как это и делает большинство программистов.
Если посмотреть внимательно, то можно заметить, что единицы визуально чуточку отличаются. Совсем капельку. Но мы же с вами хотим продумать всё до мелочей, не так ли?
Вывод будет — 201, а не 2011, как может показаться на первый взгляд.
2)Скомпилируется ли данный код и если да — то каким будет вывод?
int[][] array = {{1, 2, 3}, {0, 0, 0,},};
System.out.println(Arrays.deepToString(array));
Когда я первый раз увидел такой код в одном из тестов, я твёрдо решил, что код не скомпилируется — и оказался не прав. Некоторые из вас могут предположить, что не указанные элементы будут заполнены значением по-умолчанию — и это будет тоже неверно.
А вывод будет простой — [[1, 2, 3], [0, 0, 0]]. Получается, что компилятор просто игнорирует одну лишнюю запятую в конце массива. Причём именно одну — две подряд уже вызовут ошибку компиляции.
Описание этой ситуации я без проблем нашёл в спецификации языка — A trailing comma may appear after the last expression in an array initializer and is ignored.
А на практике — сделано для удобства при ручном копировании из одного массива в другой. Если бы компилятор не позволял ставить лишнюю запятую — то в некоторых случаях приходилось бы добавлять запятую в конце, когда мы копируем и вставляем значения из одного массива в конец другого — или удалять лишнюю в конце.
3)Скомпилируется ли данный код и если да — то каким будет вывод?
double $ = 0XD_EP2F;
System.out.print($);
Когда я изучал Java, я сразу взял на заметку, что из подчёркиваний и экспоненциальной формы для HEX можно сделать что-то дикое. Этот пример, конечно же, не для использования в реальных проектах — а для тренировки ваших знаний. Я думаю, это самый сложный пример и изюминка статьи. Первые два были лёгкой разминкой. Кто-нибудь ответил правильно с первого раза?
В этом примере заключены два интересных момента. Первое — это название переменной. Называть переменную долларом весьма забавно. Но никогда, никогда так не делайте в реальных проектах. Хотя компилятор не запрещает такой подход — он не рекомендован, так как доллар используется в технических целях. Например, для наименования анонимных и вложенных классов. MyClass$MyInnerClass, или же MyClass$1. И можно без труда устроить коллапс имён — один класс назвать просто MyClass$MyInnerClass, а другой — MyClass с вложенным MyInnerClass — и они будут иметь одинаковое имя. Так что включать в свои переменные доллар — не рекомендуется.
Также вполне корректен такой код
double _ = 8.0;
А теперь давайте разберём второй момент — непосредственно сам литерал. Я наворотил его, как только мог. Для начала легко заметить, что это число записано в шестнадцатеричной форме. Но ведь она допускает только A,B,C,D,E в качестве букв — кажете вы. Откуда тогда P и F? И причём тут знак подчёркивания?
Обо всём по-порядку. F в конце означает, что этот литерал — типа float. И у нас он автоматически приводится к типу double. Далее очень интересный момент — P2. Если мы хотим записать шестнадцатеричное число в экспоненциальной форме — мы не сможем использовать E+N, потому что E у нас может использоваться в самом числе и может возникнуть неоднозначность. Зато никто не мешает нам использовать BinaryExponentIndicator — указываем p и степень. Число будет умножено на 2 в указанной степени. В данном случае — на 4.
А вот символ подчёркивания — очень удобное нововведение Java 7. Мы просто можем вставлять его в число и разделять им, например, разряды, или группировать цифры. Компилятор его просто вырежет — он нужен для более удобного чтения, не более.
Итак, чтобы точно ответить на вопрос, нам нужно вспомнить, что System.out.print выводит переменную типа double в обычной, десятичной форме. Следственно, нам нужно перевести из шестнадцатеричного вида в десятичный. Это уже совсем тривиальная задача. DE16 = 22210. Далее, умножаем 222 на 22 и получаем 888. Наконец не забываем, что для типа double при отсутствии дробной части дописывается точка и ноль. Вот и ответ — 888.0.
4)Скомпилируется ли данный код и если да — то каким будет вывод?
public class Main{;
public static void main(String[] args) {
System.out.println(new Main().$_$()[2]);
}
;short $_$()[] {{{
return new short[007];
}}};
};
Имя метода — вполне допустимый идентификатор. А вот возвращаемый тип здесь не short а массив из short. Компилятор разрешает 2 формы объявления массива — квадратные скобки до идентификатора и после. Причём первый случай предназначен как-раз для метода. А что если попробовать второй случай для метода? Он тоже корректен, но выглядит ужасно, поэтому никогда не используйте его в реальных проектах. Но такая возможность есть.
Далее — внутри метода просто два вложенных блока кода, которые можно безболезненно убрать. И в итоге — метод просто возвращает массив из short длинной в 7 элементов, который инициализируется нулями и элемент с индексом 2 равен 0. Ах да, 007 — это восьмеричный литерал. Но это тоже ни на что не влияет.
5)Скомпилируется ли данный код и если да — то каким будет вывод?
public class Main {
public static void main(String[] args) {
((Main) null).haбra();
}
static void haбra() {
System.out.println("Hello habrahabr!");
}
}
Важный момент, который я хотел донести до вас — можно случайно напечатать не тот символ из другого языка и потом долго и мучительно искать ошибку. Например, английская a и русская а визуально неотличимы (по крайней мере, в этом шрифте). Если единицу можно отличить от l хоть как-то, то тут всё значительно хуже. Представьте себе ситуацию — вы по каким-либо причинам случайно в конце в названии класса или метода набрали русскую букву вместо английской. Или что более вероятно — редактировали уже существующее латинское название с русской раскладкой. Похожих букв между этими языками довольно много, так что шанс ошибиться вполне есть. Автозаполнение будет вставлять интернациональный идентификатор и всё будет отлично работать. Но вот если вы попробуете вызвать метод или получить класс через рефлексию — получите ошибку, которую будет не так просто обнаружить. Скорее, вы будете искать её в другом, так как названия будут визуально совпадать — потеряете время и силы.
Я хотел добавить ещё один пример, в котором два на внешний вид одинаковых идентификатора будут содержать внешне идентичные буквы из разных алфавитов, но по коду разные и посему идентификаторы будут также разные. И заставить объяснить, почему код вызывает ошибку компиляции. Но это было бы слишком жестоко — на вид подвох никак не заметить.
Далее — мы вызываем статический метод довольно оригинально — приводим null к типу Main. И это не вызовет ошибки времени выполнения! Дело в том, что вызов статических методов разрешается на этапе компиляции и зависит только от типа объекта.
6)Скомпилируется ли данный код и если да — то каким будет вывод?
Byte[] Byte[] = {{0}};
System.out.println(Byte);
System.out.println(Byte.class);
System.out.println(Byte.length);
System.out.println(new Byte("8"));
class java.lang.Byte
1
8
Далее вас должно смутить имя массива, которое полностью совпадает с именем класса. Java позволяет называть локальные переменные классами стандартной библиотеки, при этом они будут перекрывать классы. Однако компилятор у Джавы умный. Очень умный. Он сообразит, что после оператора new может следовать только класс, да и перед .class — тоже. И в том контексте будет подразумеваться именно класс Byte, хотя имя экземпляра должно его перекрывать.
Итоги
Мне нравится Java за её жёсткую стандартизованность. Спецификация определяет практически все тонкости с очень большой тонкостью. А значит — изучив эти тонкости один раз — вы сможете работать с ними везде.
Хочу добавить, что скрин кода над катом взят из IDEA. Схему подсветки я сам разработал и использую её преимущества по максимуму. Если кому понравилась — могу скинуть.
Я готовился по нескольким разным системам тестирования. Для начала, хорошим сайтом будет Quizful. Тесты «Java-Основы» и «Java-средний уровень» содержат очень большое количество полезных задач. Далее — очень хороши тесты от ExamLab — гонял по ним SCJP 6. И наконец — для самого экзамена — Enthuware.
После прохождения многочисленных тестов и решения задач «в уме» я стал приятно удивлён, каким комфортным и эффективным стало написание кода. Тесты очень хорошо систематизируют и организуют знания. Я стал предвидеть в уме многие ошибки, а также выбирать лучшие решения. И не пожалел, что выучил некоторую часть API, хотя сначала считал что не следует помнить названия методов и классов наизусть. Я считаю Java одним из лучших языков в мире и рекомендую тесты для его углублённого изучения. А Java SE 7 Programmer I был совсем не сложным — без труда набрал 96 из 100 баллов. Кто собирается сдавать — могу дать пару советов — напишите в личку.
Автор: kciray