Программист
59,7
рейтинг
26 марта 2015 в 08:10

Разработка → 10 вещей, которых вы не знали о Java перевод

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.<RuntimeException> doThrow0(e);
    }
 
    @SuppressWarnings("unchecked")
    static <E extends Exception> 
    void doThrow0(Exception e) throws E {
        throw (E) e;
    }
}

Эта программа не только компилируется, но и на самом деле кидает SQLException. Вам даже не нужен @SneakyThrows из Lombok'а.

Более подробно об этом вы можете прочитать здесь, или здесь, на Stack Overflow.

2. Можно создать два метода, которые отличаются только возвращаемым типом


Такой код не скомпилируется, верно?

class Test {
    Object x() { return "abc"; }
    String x() { return "123"; }
}

Верно. Язык Java не позволяет в одном классе иметь два «эквивалентно перегруженных» метода, даже если они отличаются возвращаемым типом или объявленными исключениями.

Но погодите-ка. Давайте почитаем документацию к Class.getMethod(String, Class...). Там написано:
Обратите внимание, что класс может содержать несколько подходящих методов, потому что хотя язык Java и запрещает объявлять несколько методов с одинаковой сигнатурой, виртуальная машина Java всё же позволяет это, если отличается возвращаемый тип. Такая гибкость виртуальной машины может использоваться для реализации некоторых возможностей языка. Например, ковариантный возвращаемый тип может быть реализован с помощью бридж-метода, который отличается от реального перегруженного метода только возвращаемым типом.
О как! Да, звучит разумно. На самом деле так и произойдёт, если вы напишете:

abstract class Parent<T> {
    abstract T x();
}

class Child extends Parent<String> {
    @Override
    String x() { return "abc"; }
}

Вот такой байткод будет сгенерирован для класса Child:

// Method descriptor #15 ()Ljava/lang/String;
// Stack: 1, Locals: 1
java.lang.String x();
  0  ldc <String "abc"> [16]
  2  areturn
    Line numbers:
      [pc: 0, line: 7]
    Local variable table:
      [pc: 0, pc: 3] local: this index: 0 type: Child
  
// Method descriptor #18 ()Ljava/lang/Object;
// Stack: 1, Locals: 1
bridge synthetic java.lang.Object x();
  0  aload_0 [this]
  1  invokevirtual Child.x() : java.lang.String [19]
  4  areturn
    Line numbers:
      [pc: 0, line: 1]

Поятно, что генерик-тип T в байт-коде превращается просто в Object. Синтетический бридж-метод генерируется компилятором, потому что в некоторых местах, где метод вызывается, в качестве возвращаемого типа Parent.x() может ожидаться Object. Было бы трудно добавить генерик-типы без бридж-методов и обеспечить бинарную совместимость. Меньшим злом оказалось доработать JVM, чтобы она поддерживала такую возможность (а в качестве побочного эффекта появились ковариантные возвращаемые типы). Умно получилось, да?

Интересуетесь перегрузкой по возвращаемому типу? Почитайте это обсуждение на Stack Overflow.

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: <identifier> expected
    int goto = 1;
        ^

Всё потому, что goto — это неиспользуемое ключевое слово. На всякий случай, вдруг когда пригодится.

Но это ещё не самое интересное. Больше всего впечатляет, что вы на самом деле можете реализовать goto с помощью break, continue и блоков с метками:

Прыжок вперёд:

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<Person>;

Тип People создан таким образом, что может в любом месте использоваться вместо Set<Person>:

People?      p1 = null;
Set<Person>? p2 = p1;
People?      p3 = p2;

В Java мы не можем объявлять алиасы типов глобально. Но это возможно сделать в пределах класса или метода. Предположим, нам не нравятся длинные имена Integer, Long и т. д., мы хотим вместо них использовать короткие: I и L. Легко:

class Test<I extends Integer> {
    <L extends Long> void x(I i, L l) {
        System.out.println(
            i.intValue() + ", " + 
            l.longValue()
        );
    }
}

В этом коде I — это алиас для Integer в пределах класса Test, а L — алиас для Long в пределах метода x(). Мы можем спокойно вызывать этот метод:

new Test().x(1, 2L);

Конечно, такую методику нельзя воспринимать всерьёз. В нашем случае типы Integer и Long объявлены final, и это означает, что генерик-типы I и L эффективно алиасы (ну почти: совместимость при присваивании работает только в одну сторону). Если бы мы использовали типы, не объявленные как final (например, Object), то это были бы обычные генерики.

Ну хватит уже дурацких фокусов. Пришло время чего-то посерьёзнее!

9. Некоторые отношения между типами невычислимы!


Окей, сейчас будет реально круто, так что налейте себе кофе и сконцентрируйтесь. Рассмотрим следующие типы:

// Вспомогательный тип. Можно использовать и просто List
interface Type<T> {}

class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}

Что же на самом деле означают типы C и D?

Они в каком-то смысле рекурсивны и чем-то похожи (хотя и не полностью) на объявление типа java.lang.Enum. Смотрите:

public abstract class Enum<E extends Enum<E>> { ... }

Фактически объявление enum — это синтаксический сахар:

// Это
enum MyEnum {}

// На самом деле сахар для этого
class MyEnum extends Enum<MyEnum> { ... }

Запомним это и вернёмся к нашим исходным типам. Скомпилируется ли такой код?

class Test {
    Type<? super C> c = new C();
    Type<? super D<Byte>> d = new D<Byte>();
}

Вопрос сложный и у Росса Тейта есть ответ. Определить это вообще-то нельзя:

Является ли C подтипом Type<? super C>?
Шаг 0) C является ли Type<? super C>
Шаг 1) Type<Type<? super C>> является ли Type<? super C> (наследование)
Шаг 2) C является ли Type<? super C> (проверяем маску? super C)
Шаг... (бесконечный цикл)

Если с C мы просто зацикливаемся, то с D ещё веселее:
Является ли D<Byte> подтипом Type<? super D<Byte>>?
Шаг 0) D<Byte> является ли Type<? super D<Byte>>
Шаг 1) Type<Type<? super D<D<Byte>>>> является ли Type<? super D<Byte>>
Шаг 2) D<Byte> является ли Type<? super D<D<Byte>>>
Шаг 3) Type<Type<? super D<D<Byte>>>> является ли Type<? super D<D<Byte>>>
Шаг 4) D<D<Byte>> является ли Type<? super D<D<Byte>>>
Шаг... (бесконечный рост)

Попытайтесь скомпилировать это в Eclipse, и он упадёт с переполнением стека! (не беспокойтесь, я уже сообщил в багтрекер)
Придётся смириться:
Некоторые отношения между типами невычислимы!

Если вас интересуют проблемы, связанные с генерик-типами, почитайте статью Росса Тейта «Укрощение шаблонов в системе типов Java» (в соавторстве с Аланом Лёнгом и Сорином Лернером), или наши размышления на тему.

10. Пересечение типов


В языке Java есть весьма своеобразная штука — пересечение типов. Вы можете объявить генерик-тип, который является пересечением двух типов. Например:

class Test<T extends Serializable & Cloneable> {
}

Тип, которому соответствует T в конкретных экземплярах класса Test, должен реализовывать оба интерфейса Serializable и Cloneable. К примеру, String не подойдёт, а вот Date — вполне:

// Ошибка компиляции
Test<String> s = null;

// Работает
Test<Date> d = null;

Эта возможность получила развитие в Java 8, где вы можете преобразовать тип к пересечению. Где это может пригодиться? Почти нигде, но если вам надо привести лямбда-выражение к такому типу, у вас нет других вариантов. Предположим, у вашего метода вот такое безумное ограничение на тип:

<T extends Runnable & Serializable> void execute(T t) {}

Вас устраивает только такое Runnable, которое ещё и Serializable на тот случай, если вы захотите передать его по сети и запустить где-то ещё. В принципе лямбды сериализовать можно:
Вы можете сериализовать лямбда-выражение, если его целевой тип и захваченные аргументы сериализуемы.
Но даже если это соблюдается, лямбда не будет автоматически реализовывать маркерный интерфейс Serializable. Вам потребуется приведение типа. Но если вы приведёте только к Serializable:

execute((Serializable) (() -> {}));

то лямбда больше не будет Runnable.

Эх…

Остаётся…

Привести к двум типам сразу:

execute((Runnable & Serializable) (() -> {}));

Заключение


Обычно я говорю это только про SQL, но пришло время завершить статью вот так:
Java — загадочный и мощный механизм. Круче его загадочности разве что его мощь.
Перевод: Lukas Eder
Тагир Валеев @lany
карма
470,2
рейтинг 59,7
Программист
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (55)

  • +11
    Любое условие, цикл или вызов функции — это тоже goto в машинных кодах.
    • 0
      Да, причем «эмуляция» goto приведённые — ничто. Это как раз причина по которой goto как таковой «запрещен» в высокоуровневых языках.
      «Настоящий» goto позволяет перейти в середину цикла или в середину ветки исполнения.
      • 0
        Насколько я помню мануал по GW-BASIC, который я штудировал в 8 лет, даже там нельзя было войти в середину цикла по GOTO.
        • +1
          Какой-то это слишком модный и новый Basic, что в нем нет порядочного goto.
  • +3
    Проверяемые исключения также можно выбросить используя: Unsafe.getUnsafe().throwException(new SQLException()) ну или с помощью deprecated-метода: Thread.currentThread().stop(new SQLException());
    • 0
      только дальше Unsafe.getUnsafe() вы не уйдете
    • +2
      Thread.currentThread().stop(new SQLException()); больше не работает в Java8:

      /**
       * Throws {@code UnsupportedOperationException}.
       *
       * @param obj ignored
       *
       * @deprecated This method was originally designed to force a thread to stop
       *        and throw a given {@code Throwable} as an exception. It was
       *        inherently unsafe (see {@link #stop()} for details), and furthermore
       *        could be used to generate exceptions that the target thread was
       *        not prepared to handle.
       *        For more information, see
       *        <a href="{@docRoot}/../technotes/guides/concurrency/threadPrimitiveDeprecation.html">Why
       *        are Thread.stop, Thread.suspend and Thread.resume Deprecated?</a>.
       */
      @Deprecated
      public final synchronized void stop(Throwable obj) {
          throw new UnsupportedOperationException();
      }
  • +10
    респект автору, я думал я знаю java :(
  • +10
    Есть ли в мире хоть один язык программирования, про который бы не появилось статей вроде этой? :)
    • +7
      brainfuk :D
    • 0
      Машина Тьюринга, подстановки Маркова.
      Любой ассемблер можно приводить в качестве примера «языка программирования без подвохов» (хотя в этом случае подвохи надо ожидать от производителей процессоров, выпускающих не полностью одинаковые процессоры)
      Чем больше в языке становится возможностей — тем больше извращений и недопониманий.

      • +5
        С ассемблером и процессорами можно таких хитростей на кучу статей набрать :-) В духе «в этом коде для таких-то процессоров стоит вставить пару раз NOP, чтобы предсказатель веток успел обновить свою таблицу, иначе у вас будет постоянный misprediction, и цикл будет выполняться гораздо медленнее».
        • +2
          Я же говорю, ассемблер в этом примере надо рассматривать в связке с процессором.
          Вся сущность подобных проблем с «непонятностью» и «непредсказуемым поведением» — это вывод из самих понятий «алгоритм» и «исполнитель».
          Алгоритм — это конечная запись некоторых команд, которые понимает и может интерпретировать исполнитель.
          Если исполнители разные — они один и тот же алгоритм трактуют по-разному.
          Добиться «абсолютно понятного и недвусмысленного поведения» можно как можно сильнее упростив систему команд и определив в тонкостях что та или иная команда значит.
          Но это перфекционизм, вряд ли кому-либо нужный.
          • +1
            Я же говорю, ассемблер в этом примере надо рассматривать в связке с процессором.

            Ну, в статье же рассматривается Java в связке с JVM :) Так почему бы не рассматривать и ассемблер в связке с процессором ;) Почти что одного поля ягоды ;)
            • –1
              Не почти что, а одного поля.
              Хотя, справедливости ради следовало бы даже разделить и рассматривать отдельно
              1) язык ассемблера и соответствуюший ему ассемблер-исполнитель который переводит текст программы в машинный код. Потом машкод как язык (вполне себе язычок кстати — я даже «исправлял» некоторые «программы» заменяя E6 61 на 90 90) и процессор как исполнитель
              2) Java как язык и javac-исполнитель этого языка. Следующий этап — байткод как язык и JVM как исполнителя
  • +9
    Открыл статью с мыслями «ну-ка посмотрим, чего это такого я не знал». Как оказалось, 10 с 10 таки не знал :(
    • –6
      я бы плюсанул
    • +1
      Я знал примерно половину, но всё равно счёл статью достойной для перевода :-)
  • 0
    Плюсую.
    Положил в закладки.
  • –4
    Баян. У Джошуа Блоха, автора stdlib, есть книжка Java Puzzlers: Traps, Pitfalls, and Corner Cases — там всё в том же самом стиле, но чуть повеселее. И примеров там не десять, а несколько дюжин.
    • +6
      Конечно, в этой книжке 2005-го года рассказано о новой фиче Java 8 — кастовании к пересечению типов?

      Книжка хорошая, не спорю. Но местами устарела.
      • –1
        Конечно, в Java 8 больше маразмов, чем в Java 1.4.
  • +1
    Когда-то тоже любил подобные тонкости языка, даже статью об этом написал. Может быть, вам будет интересно почитать.

    Хитрые задачи по Java
    • 0
      Читал когда-то тот ваш пост, любопытный. Хотя всевозможными сертификатами и тестами я не интересуюсь, а вот Java мне интересна.
  • 0
    На счёт generic в блоке throws странно. Почему вообще это разрашили делать? Ведь generic в Exception нельзя применять, а в throws почему то можно?!
    P.S. Нельзя, означает, что class MyException extends Exception{} не скомпилируется, а, например, class MyException extends Date{} скомпилируется.
    • +1
      Честно говоря, недопонял ваш вопрос. Или там где-то парсер съел угловые скобки?
      • 0
        Ой. Точно! :) Не стал использовать source теги, а оказалось, что зря. Там ещё были Generic:

         class MyException<E> extends Exception{}
        


         class MyException<E> extends Date{}
        
  • +1
    Сегодня уже любой согласится, что проверяемые исключения были плохой идеей.

    Ой, да ладно. Насколько я знаю, основная претензия к checked исключениям, состоит в том многие на них забивают просто оставляют пустой catch блок. При этом нормальных альтернатив не очень предлагается.

    К примеру:
    public FileInputStream(File file) throws FileNotFoundException {...} // FileNotFoundException - checked
    

    явно заставляет программиста при использовании этого конструктора, включать мозг и что-то делать с возможностью того что данный файл будет не найден. Если это будет пустой catch блок — это тоже решение, но это решение программиста который пишет код, которое он (я надеюсь) обдумал.

    Если бы в данном случае кидался unchecked то возможно было бы 2 печальных сценария:
    • не очень опытный программист — забыл бы обработать это сценарий и узнал бы о нем потом, когда было бы уже поздно
    • опытный программист — полез бы в документацию смотреть какое исключение там кидается в случае если что-то пошло не так

    Если вы не любите checked exception никто не мешает заворачивать их вызовы в try/catch и пробрасывать дальше как unchecked. Тем более что если у Вас решено их не использовать то их вызовы будут использоваться только при работе с внешним API.

    На StackOverflow есть хороший пост с обсуждением
    • +2
      Часто бывает надо написать код, в котором 100% никогда не будет исключения (а если будет, то это уже сигнал к тому, что программа в этом окружении вообще неработоспособна. Например, UnsupportedCharacterEncoding), но который требует обрамления в try-catch.

      Ещё бывает ситуация, в которой хочется на некоторый блок действий глобально повесить try-catch(Throwable) и если произойдет любое исключение, считать, что операция в целом зафейлилась. А не разбираться с каждым конкретным исключением, которое может случиться внутри этого блока.

      > Никто не мешает заворачивать их вызовы в try/catch и пробрасывать дальше как unchecked
      Только это и остаётся. Но это замусоривает код. Возвращаясь к примеру с UnsupportedCharacterEncoding, можно отметить, что если нужно написать метод, который занимает 5 строчек, а теперь приходится к нему добавить еще 5 строк try-catch-rethrow RuntimeException, то это же чистый мусор.

      Ну и конечно же, наличие checked исключений и легкость избавления от них пустым catch-блоком побуждает начинающих делать это в больших количествах.

      Про пункт с не очень опытным программистом. Эта проблема решаема статическим анализатором (и в IDE?), который бы говорил «чувак, возможно ты пропустил исключение здесь!».
      • +1
        Только это и остаётся. Но это замусоривает код. Возвращаясь к примеру с UnsupportedCharacterEncoding, можно отметить, что если нужно написать метод, который занимает 5 строчек, а теперь приходится к нему добавить еще 5 строк try-catch-rethrow RuntimeException, то это же чистый мусор.

        Согласен, но с восьмой версией и возможностью ловить несколько исключений проблема, на мой взгляд стала менее острой. Для меня плюсы от их использования, перевешивают необходимость написания 3-х строк кода. А вообще соглашусь с тем что UnsupportedChacacterEncoding — хороший пример плохого использования checked exceptions.

        Ну и конечно же, наличие checked исключений и легкость избавления от них пустым catch-блоком побуждает начинающих делать это в больших количествах.

        Бейте по рукам! :)
        Мне кажется, что по пустому catch блоку ошибку понять легче, нежели по не обернутому методу.

        Про пункт с не очень опытным программистом. Эта проблема решаема статическим анализатором (и в IDE?), который бы говорил «чувак, возможно ты пропустил исключение здесь!».

        Статический анализатор будет работать либо только со стандартной библиотекой, что не решает всех проблем, либо опираться на документацию конкретного функционала, что тоже не всегда надежно работает :(
    • +4
      Заметьте, что в Java 8 появилось UncheckedIOException и новые API-методы используют его. Ну и да, весь Stream API крайне плохо совместим с checked exceptions. Ясно, что сами разработчики Java сменили парадигму.

      В checked-исключениях есть плюсы, но красивая теория разбивается о суровую реальность, когда кто-нибудь неоправданно отнёс исключение не к той группе. Я сам бы не стал формулировать настолько самоуверенно как «Сегодня уже любой согласится» (Today, everyone agrees that checked exceptions were a mistake), но перевод есть перевод, надо мысль автора передавать, а не свою.
      • +1
        > В checked-исключениях есть плюсы, но красивая теория разбивается о суровую реальность, когда кто-нибудь неоправданно отнёс исключение не к той группе.

        Реальность для проверяемых исключений еще суровее: не существует ни одного типа исключения, с которым всегда и везде нужно работать как с проверяемым. Ну то есть всегда найдется случай, когда разработчик подумает, что данное исключение «неоправданно отнесли не к той группе».
  • +4
    6 пункт желтоватый получился. Я даже немного запаниковал после его прочтения.
    • 0
      Да, насколько я понял, это получилось так, потому что там вручную изменил кеш Integer.
  • 0
    del
  • +2
    Я бы еще добавил что в bytecode не множественных catch, а блок finally отсутствует вообще и дублируется между телом try и catch-блоками. В итоге правильно составленный исходник будет расти экспоненциально при компиляции в байткод.
    • +2
      Так я сам про это и писал недавно :-)
  • +1
    И все таки какое применение 3 пункта на практике? Думал instanceof по разному срабатывает, но нет. В чем подвох?
    • +1
      Если вы про аннотации типов, то вероятно это может принести пользу. Скажем,
      @Nonnull int[][] x;

      будет означать, что в x не может быть null. А, например,
      int @Nonnull [][] x;

      будет означать, что в x может быть null, но в x[0] не может быть. Если IDE поддерживает аннотации типов, вы увидите предупреждение при попытке присвоить null.

      А запись int[] x[], на мой взгляд, абсолютно бесполезна. Просто тяжёлое наследие, дурное влияние Си.
      • 0
        а какой профит в Java 7?
  • 0
    Меньшим злом оказалось доработать JVM, чтобы она поддерживала такую возможность


    Разве? На сколько я понимаю, эта доработка исключительно на уровне компилятора. Как раз если бы дорабатывали jvm, то научили бы ее нормально обрабатывать ковариантные возвращаемые типы, и бридж методы вообще бы не появились.
    • 0
      Если я правильно понял, до JVM 1.5 виртуальная машина не пропустила бы класс, где есть два метода, отличающиеся только возвращаемым типом. В этом смысле доработали JVM. К сожалению, не проверял, так ли это. Сейчас попробуй найди спецификацию JVM 1.4.
  • +8
    6 пункт и «разгадка» заставили меня негодовать. С помощью рефлексии, класслоадинга и проч. можно подменить вообще все что угодно, заставить любой код действовать как угодно. Я «угадал» что там подменили System.setOut().
    • 0
      И то верно. Я даже не подумал о таком варианте :-)
  • 0
    Все примеры хороши, но вот пункт 5-й — все интуитивно понятно. Не то, чтобы он тут лишний, но…
    • 0
      У автора много статей в стиле «Top 10». Не исключено, что он придумал сколько-то крутых примеров, а потом пришлось набивать до круглого числа :-) А вообще конкретно для меня этот пункт был неожиданный. Если б меня спросили, что будет, если char умножить на полтора, я бы предположил, что случится ошибка компиляции.
      • 0
        Прикол в том, что там тип не char а byte, поэтому я даже сразу предположил верный ответ ;)
  • 0
    Вот ещё интересный пример:
    System.out.println(Arrays.asList(new Integer[] {1, 2, 3}));
    System.out.println(Arrays.asList(new int[] {1, 2, 3}));

    Что выведется в первом случае, и что во втором?
    • 0
      Это легко :-)
      Скрытый текст
      В первом [1, 2, 3], а во втором что-то типа [[I@12345678]. Со Stream.of, кстати, та же ситуёвина.
      • 0
        Легко-то легко, а вот если человек с таким не сталкивался, то и по граблям пробежаться легко :-) И не только Stream.of, а вообще любое место, где есть сочетание variable arguments с автобоксингом.
        • 0
          Ну так FindBugs используйте, сколько раз говорить :D
          • 0
            Никто не спорит, что FindBugs, PMD и CheckStyle (особенно в Sonar-е) — это нужно, полезно и удобно, но, к сожалению, не всегда возможно. Если вам дают legacy-проект с десятками-сотнями мегабайт кода, тысячами-десятками тысяч классов, и при этом он написан, скажем политкорректно, не очень качественно, то навесить на него любой статический анализатор — деяние, сравнимое с подвигом.
            • 0
              Глаза боятся, руки делают. Если вы можете скомпилировать проект, то и запустить FindBugs несложно. Даже если вы не укажете ему все зависимости, он работать будет (возможно, чуть хуже, но всё-таки). В мире Java прикручивать статический анализатор к мутному проекту на порядок проще, чем в мире C++, например. Я не так давно к очень мутному проекту прикручивал, где сборка идёт через стокилобайтные ant-скрипты, много раз запаковывается и снова распаковывается war, сорцы и зависимости раскиданы по сотне мест. Не больше пары часов заняло, теперь в Хадсоне каждую ночь выполняется. Именно для legacy-кода статический анализ нужен как воздух. FindBugs ловит много багов, которые люди делали 10 лет назад, а сейчас делают редко.

              Конечно, одно дело — прикрутить, а другое — проанализировать результаты, понять, что серьёзное, а что — не очень, и пофиксать все баги =)

              Если реально стоит такой вопрос — пиши в личку, помогу интегрировать FindBugs забесплатно по старой дружбе ;-)
          • 0
            Кстати, у меня на этом коде FindBugs не нашёл ошибку. В консоль выводилось, что Вы указали (т.е.фактически ошибка была), но FindBugs не показал её.
            P.S. Проверял в IDEA с плагином FindBugs. При проверке другого кода ошибки находятся, т.е. FindBugs работает.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.