[Перевод] 10 вещей, которых вы не знали о Java
Итак, вы работаете на Java с самого её появления? Вы помните те дни, когда она называлась «Oak», когда про ООП говорили на каждом углу, когда сиплюсплюсники думали, что у Java нет шансов, а апплеты считались крутой штукой? Держу пари, что вы не знали как минимум половину из того, что я собираюсь вам рассказать. Давайте откроем для себя несколько удивительных фактов о внутренних особенностях Java.1. Проверяемых (checked) исключений не существуетДа-да! JVM ничего про них не знает, знает только Java.Сегодня уже любой согласится, что проверяемые исключения были плохой идеей. Как сказал Брюс Эккель в своей завершающей речи на GeeCON в Праге, ни один язык после Java не связывался с проверяемыми исключениями и даже в новом Streams API в Java 8 от них отказались (что может вызвать трудности, когда ваши лямбды используют ввод-вывод или базы данных).
Хотите убедиться, что JVM ничего про них не знает? Запустите этот код:
public class Test {
// Нету throws: исключения не объявлены
public static void main (String[] args) {
doThrow (new SQLException ());
}
static void doThrow (Exception e) {
Test.
2. Можно создать два метода, которые отличаются только возвращаемым типом Такой код не скомпилируется, верно? class Test { Object x () { return «abc»; } String x () { return »123»; } } Верно. Язык Java не позволяет в одном классе иметь два «эквивалентно перегруженных» метода, даже если они отличаются возвращаемым типом или объявленными исключениями.Но погодите-ка. Давайте почитаем документацию к Class.getMethod (String, Class…). Там написано:
Обратите внимание, что класс может содержать несколько подходящих методов, потому что хотя язык Java и запрещает объявлять несколько методов с одинаковой сигнатурой, виртуальная машина Java всё же позволяет это, если отличается возвращаемый тип. Такая гибкость виртуальной машины может использоваться для реализации некоторых возможностей языка. Например, ковариантный возвращаемый тип может быть реализован с помощью бридж-метода, который отличается от реального перегруженного метода только возвращаемым типом.
О как! Да, звучит разумно. На самом деле так и произойдёт, если вы напишете:
abstract class Parent
class Child extends Parent
3. Это всё двумерные массивы! class Test { int[][] a () { return new int[0][]; } int[] b () [] { return new int[0][]; } int c () [][] { return new int[0][]; } } Да, это правда. Возвращаемый тип этих методов одинаков, даже если парсер в вашей голове не сразу это понял! А вот похожий кусок кода: class Test { int[][] a = {{}}; int[] b[] = {{}}; int c[][] = {{}}; } Скажете, безумие? А если ещё добавить к этому аннотации типов Java 8? Количество вариантов возрастает в разы! @Target (ElementType.TYPE_USE) @interface Crazy {}
class Test { @Crazy int[][] a1 = {{}}; int @Crazy [][] a2 = {{}}; int[] @Crazy [] a3 = {{}};
@Crazy int[] b1[] = {{}}; int @Crazy [] b2[] = {{}}; int[] b3 @Crazy [] = {{}};
@Crazy int c1[][] = {{}}; int c2 @Crazy [][] = {{}}; int c3[] @Crazy [] = {{}}; } Аннотации типов. Загадочный и мощный механизм. Круче его загадочности разве что его мощь.
Или другими словами: Мой последний коммит перед месячным отпуском
Найти реальный сценарий использования этих конструкций я оставляю вам в качестве упражнения.4. Вы не понимаете условные конструкции Вам кажется, что вы знаете всё об условных выражениях? Я вас разочарую. Большинство программистов считают, что следующие фрагменты кода эквивалентны: Object o1 = true? new Integer (1) : new Double (2.0); Это ведь то же самое? Object o2; if (true) o2 = new Integer (1); else o2 = new Double (2.0); А вот и нет. Давайте проверим: System.out.println (o1); System.out.println (o2); Программа выдаст следующее: 1.0 1 Ага! Условный оператор выполняет приведение численных типов, когда «необходимо», причём «необходимо» в очень жирных кавычках. Ведь вы же не ожидаете, что эта программа кинет NullPointerException? Integer i = new Integer (1); if (i.equals (1)) i = null; Double d = new Double (2.0); Object o = true? i: d; // NullPointerException! System.out.println (o); Больше подробностей на эту тему здесь.5. Составной оператор присваивания вы тоже не понимаете Не верите? Рассмотрим две строчки кода: i += j; i = i + j; Интуитивно они должны быть эквивалентны, так? Сюрприз! Они отличаются. Как сказано в JLS: Составной оператор присваивания вида E1 op= E2 эквивалентен выражению E1 = (T)((E1) op (E2)), где T — это тип E1, за исключением того, что E1 вычисляется только один раз.
Это настолько прекрасно, что я хотел бы процитировать ответ Питера Лори на Stack Overflow: Пример такого приведения типов можно показать на *= или /=
byte b = 10;
b *= 5.7;
System.out.println (b); // выведет 57
или byte b = 100;
b /= 2.5;
System.out.println (b); // выведет 40
или char ch = '0';
ch *= 1.1;
System.out.println (ch); // выведет '4'
или char ch = 'A';
ch *= 1.5;
System.out.println (ch); // выведет 'a'
Видите, какая полезная фича? Теперь я буду умножать свои символы с автоматическим приведением типов. Потому что, знаете ли…6. Случайные целые числа
Это скорее загадка. Не подглядывайте в решение, попробуйте догадаться сами. Когда я запускаю такой код:
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
«в некоторых случаях» получаю такой результат:
92
221
45
48
236
183
39
193
33
84
Как это возможно??Разгадка
Ответ приводится здесь и заключается в перезаписи кэша целых чисел JDK с помощью reflection и в использовании автобоксинга. Не пытайтесь повторить это дома! Ну или вспомните картинку выше про последний коммит перед отпуском.
7. GOTO
А вот моё любимое. В Java есть GOTO! Попробуйте:
int goto = 1;
И вы получите:
Test.java:44: error:
Прыжок вперёд:
label: {
// … какой-то код…
if (check) break label;
// …ещё какой-то код…
}
В байт-коде:
2 iload_1 [check]
3 ifeq 6 // Прыжок вперёд
6 …
Прыжок назад
label: do {
// … какой-то код…
if (check) continue label;
// …ещё какой-то код…
break label;
} while (true);
В байт-коде:
2 iload_1 [check]
3 ifeq 9
6 goto 2 // Прыжок назад
9 …
8. В Java есть алиасы к типам
В других языках, например, в (Цейлоне), мы можем легко объявить алиас для типа:
interface People => Set
9. Некоторые отношения между типами невычислимы!
Окей, сейчас будет реально круто, так что налейте себе кофе и сконцентрируйтесь. Рассмотрим следующие типы:
// Вспомогательный тип. Можно использовать и просто List
interface Type
class C implements Type implements Type
public abstract class Enum // На самом деле сахар для этого
class MyEnum extends Enum Если с C мы просто зацикливаемся, то с D ещё веселее: Является ли D Попытайтесь скомпилировать это в Eclipse, и он упадёт с переполнением стека! (не беспокойтесь, я уже сообщил в багтрекер)Придётся смириться: Некоторые отношения между типами невычислимы!
Если вас интересуют проблемы, связанные с генерик-типами, почитайте статью Росса Тейта «Укрощение шаблонов в системе типов Java» (в соавторстве с Аланом Лёнгом и Сорином Лернером), или наши размышления на тему.10. Пересечение типов
В языке Java есть весьма своеобразная штука — пересечение типов. Вы можете объявить генерик-тип, который является пересечением двух типов. Например:
class Test // Работает
Test
Но даже если это соблюдается, лямбда не будет автоматически реализовывать маркерный интерфейс Serializable. Вам потребуется приведение типа. Но если вы приведёте только к Serializable:
execute ((Serializable) (() → {}));
то лямбда больше не будет Runnable.Эх… Остаётся… Привести к двум типам сразу:
execute ((Runnable & Serializable) (() → {}));
Заключение
Обычно я говорю это только про SQL, но пришло время завершить статью вот так:
Java — загадочный и мощный механизм. Круче его загадочности разве что его мощь.