Пользователь
0,0
рейтинг
9 октября 2013 в 13:27

Разработка → 2 «простых» вопроса по джаве

Java*

Приветствую, уважаемый читатель.
Java — интересный и красивый язык. Но иногда на нем можно написать такое, что лучше этого не видеть. Но все равно полезно знать, что происходит и в таких, кривых, случаях.
Любителей чистого кода прошу меня извинить.

Когда-то давно на хабре была пара статей как не надо писать на java с интересными задачками по java.
Часть 1, Часть 2
Они очень интересные, но, к сожалению, автор не стал продолжать.

Представляю вашему вниманию еще 2 задачки (на большее не хватило сил. Оказывается, писать статьи не так-то просто.)

В конце статьи, разумеется, будут ответы с разъяснениями, а также дополнительные задания для самых сильных.


Первая задача взята подчистую из замечательной книги Джошуа Блоха Java Puzzlers. Книга не научит вас, как писать код (скорее наоборот, как не надо и почему), но лично мне было дико интересно это читать. Просто для развлечения. Рекомендую.


Итак, приступим.

Условия


1. Казнить нельзя помиловать

public class A {
    public static class X {
        public static class Y {
            public static String Z = "life is good";
        }

        public static C Y;
    }

    public static class C {
        public static String Z = "life is pain";
    }

    public static void main(String[] args) {
        System.out.println(X.Y.Z);
    }
}

Что произойдет?
  1. Compile error
  2. Runtime error
  3. Выведет life is good
  4. Выведет life is pain
Кроме этого как, не меняя имен, [исправить ошибку, если ответ 1/2 и] вывести обе строчки?



2. Дженерики такие дженерики


   public class B {
    public static <T> T foo() {
        try {
            return (T) new Integer(42);
        } catch (ClassCastException e) {
            return (T) "habr";
        }
    }
    public static void main(String[] args) {
        System.out.println(B.<String>foo());
    }
}

Что произойдет?
  1. Compile error
  2. Runtime error
  3. Выведет 42
  4. Выведет habr




А теперь правильные ответы:



1. Казнить нельзя помиловать

public class A {
    public static class X {
        public static class Y {
            public static String Z = "life is good";
        }

        public static C Y;
    }

    public static class C {
        public static String Z = "life is pain";
    }

    public static void main(String[] args) {
        System.out.println(X.Y.Z);
    }
}
Выведет life is pain
Тут все дико, но просто. Да, jls такое позволяет. Приоритет всегда у поля.

Гораздо интереснее — как это обойти и вывести-таки life is good? Мне известны три решения:
  • reflection api
  • импорт X.Y
  • К статике можно обращаться через экземпляр: (new X.Y()).Z

Задание на пятерку

Последний способ хорош, но требует создания объекта, что плохо. Добавим приватный конструктор.А теперь слабо? (без рефлексии и статического импорта)
    public static class X {
        public static class Y {
            private Y() {}
            public static String Z = "life is good";
        }

        public static C Y;
    }
Ответ:
Скрытый текст
    ((X.Y)null).Z; // подберите свои челюсти, это Java.



2. Дженерики такие дженерики
   public class B {
    public static <T> T foo() {
        try {
            return (T) new Integer(42);
        } catch (ClassCastException e) {
            return (T) "habr";
        }
    }
    public static void main(String[] args) {
        System.out.println(B.<String>foo());
    }
}
Runtime error, а если точнее, то ClassCastException

Итак, все мы знаем, что в java дженерики — не более чем синтаксический сахар, ограничивающий наши возможности позволяющий компилятору выполнять дополнительные проверки типов. Но как только программа запущена — вся информация о классах-параметрах теряется. Увы, это не c++ и даже не c#.

Посмотрим внимательно на код:

System.out.println(B.<String>foo());

Компилятор понимает, что тип аргумента — String, а следовательно подставляет наиболее подходящий println:

public void println(String x)

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

Идем дальше.

return (T) new Integer(42);

Cast происходит в рантайме, в тот момент, когда дженериков уже «нет».
Что произойдет?
Ничего. Просто вернется Integer.

Ну и приехали — вызвали printf, принимающий String, а передали ему Integer.

Уроки на будущее
  • Не надо так делать
  • В мире есть не только println, принимающий Object
  • Каст к типу дженерика — это костыль

Задание на пятерку

Реализуйте метод, бросающий произвольный Throwable (в том числе, checked exception), не требующий ни thows, ни try-catch:

public static void throwWithoutCheck(Throwable t) {
    // Никаких проверок, только хардкор. Хочу throwWithoutCheck(new Exception()) - и никаких throws!
}
Ответ:
Скрытый текст
    private static <T extends Throwable> void castAndThrow(Throwable t) throws T {
        throw (T) t;
    }
    public static void throwWithoutCheck(Throwable t) {
        B.<RuntimeException>castAndThrow(t);
    }


UPD:
Есть еще способы кинуть эксепшн без проверок. Они не подразумевались в качестве ответа, но, скажем так, более «честные»:

Thread.currentThread().stop(new IOException());

Обратите внимание, что stop — @deprecated. Концепция работы с потоками претерпела значительные изменения, трава стала зеленее, а воздух чище.
В качестве аргумента Stop можно передать «причину» остановки. Понятное дело, что на джаве в принципе нельзя заключить это в try catch блок, надеюсь, тут все ясно.
Спасибо orionll за пример.


Или:
Unsafe.getUnsafe().throwException(new IOException());

Тут все тоже понятно, просто так с Unsafe не поработать и, в данном случае, все совершенно оправданно.
Спасибо mishadoff за пример.
Алексей Фомин @Fomka
карма
15,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

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

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

  • +7
    Заголовок статьи и картинка вводят в заблуждение.
    • +1
      Поменял. Надеюсь, так лучше.
      • +1
        да, действительно лучше!
      • +3
        Теперь мне интересно какая там была раньше картинка
        • +1
          раньше была эта картинка
        • +1
          Опережая вопрос — а заголовок был: «Java, ну зачем?»
          ;)
          • +2
            Моя первая мысль после просмотра статьи.
  • 0
    «I was programming in ML in the 1990s. Imagine my dismay when Java came out in 1995, one of the biggest step backwards in history» (автор)
  • +5
    Да, Java позволяет отстрелить себе ногу (это вы еще git не видели :))
    С дженериками лучше, чем без них, удобнее: часть труда берет на себя компилятор. Конечно, его можно обмануть или ввести в заблуждение, только зачем?
    Вообще, пост напоминает историю, как израильтяне хаммер перевернули.
    • 0
      Почему не видел?=)

      А я и не говорю, что с дженериками плохо. Мне просто совсем непонятно, почему нельзя было хранить ссылку на класс-параметр в объекте дженерика. В этом случае дженерики оставались бы дженериками и в рантайме.
      • +1
        Обратная совместимость, не?
        • 0
          Повторюсь, почему нельзя было сразу так сделать? Очевидное же решение. Подозреваю, что это связано со старыми коллекциями, но не владею точной информацией.
          • +1
            Вначале джененриков даже в мыслях не было. А потом пришлось делать костылями ради обратной совместимости.
            • +1
              Так именно в этом и вопрос — ради какой совместимости? Коллекции? Это-то и интересно…
            • +3
              За этот костыль сейчас благодарят авторы Scala. Её продвинутую систему типов удалось реализовать лишь благодаря type erasure. А вот с .NET-овской версией Scala всё загнулось, как раз из-за type reification.
      • +1
        Я не разработчик языка. И потому судить о причинах и рассуждать «почему бы не хранить ссылку» — не могу. Авторам таки виднее, чем мне. Думаю, Выступления двух наших известных соотечественников, которые «пилят» производительность лямбды, показывают сколько «стекла» может крыться за с виду простым шагом. Поэтому… есть особенность, о ней надо знать. Можно свои идеи изложить в виде багрепорта. Можно просто смириться.
        • +1
          alhimik45 уже ответил — обратная совместимость. Это и хлеб и бич Java. Хлеб, потому что Java используют многие enterprise-приложения, которые не любят изменений внутри своего кода. А следовательно «write once run everywhere»-принцип должен соблюдаться и в части старого наследия.
          • 0
            На моей памяти эта обратная совместимость именно в энтерпрайз приложениях и чудила: проект с WebService, собранный JDK7, не запускается на JDK6. Что-то ему не ладно показалось в javax.ws.* Обратная совместимость — это не только формат классов. Это дохрена библиотек. Которые совершенствуются и включаются в JDK.
            В общем, не верю я в сферическую совместимость в вакууме. Для какого-то конкретного приложения — может быть. Если авторы приложат руки и специально проверят.
            • 0
              проект с WebService, собранный JDK7, не запускается на JDK6

              Не наоборот?
              • 0
                С последними версиями вполне может быть (натыкался, например, на баги типа 1.6_22 — работает, 1.6_25 работает, в 1.6_24 — сломалось).
              • 0
                Не, именно так. В разработке я на 7 перешел. А на боевой площадке еще 6я стояла. В процессе багфикса поймал :)
                • +3
                  Так это не обратная совместимость. Если вы собрали под 7, то как оно под 6 запустится? Это forward-совместимость, а не backward.
                  • 0
                    Тогда я не прав. Тут, тогда, про обратную совместимость.
            • +1
              Так ведь обратная совместимость языка, а не библиотек!
              Смотрите, дженерики были введены в версии 1.5, до неё все делали все преобразования вручную. Проблема в том, что часть кода работала отличным от дженериков образом и внесение их в ВМ привело бы к краху многие приложения. А ведь до сих пор есть код, написанный под 1.4 (а крутится порой и на 1.7).
              • 0
                В моем случае были не библиотеки а классы из состава самого JDK, в чем вся прелесть. Кроме того, после смены собственника старый софт часто намекает на левую java-машину и отказывается работать. OpenProj к примеру.
                • 0
                  Ещё раз: совместимость языка. Классы JDK теоретически можно невозбранно заменять, как там с практикой — не знаю. Но даже в этом случае JDK стараются наращивать, а не менять (добавить nio, а не заменять io, добавить Collections, ...) Но есть случаи, когда меняют и JDK, обычно об этом заранее предупреждает свойство
                  @deprecated
  • 0
    Можно еще сделать простой импорт Y класса:

    import ru.open.haven.client.gui.Test.X.Y;
    
    public class Test {
        public static class X {
            public static class Y {
                public static String Z = "life is good";
            }
    
            public static C Y;
        }
    
        public static class C {
            public static String Z = "life is pain";
        }
    
        public static void main(final String[] args) {
            System.out.println(Y.Z);
        }
    }
    


    Выведет «life is good»
    • 0
      Действительно, намудрил со статическим. Поправил в посте, спасибо
  • 0
    Дженерики в Java тот ещё выкидыш. И пока они обратную совместимость не поломают, ничего в лучшую сторону не изменится.
  • +1
    > Каст к типу дженерика — это костыль
    Единственный ценный месседж: каст к типу дженерика пропадает в рантайме.
    Кстати, это действительно так? Я не проверял, но что-то есть сомнения.

    Забавно, но информация о дженериках полностью не стирается в рантайме.
    См. docs.oracle.com/javase/6/docs/api/java/lang/reflect/Type.html и все что с ним связано, вроде:
    docs.oracle.com/javase/6/docs/api/java/lang/reflect/Method.html#getGenericReturnType()
  • 0
    Или:

    short a = 1;
    short b = 2;
    short z = a + b;
    System.out.println(z);

    Что выведет?
    • +1
      Пишите ещё через неделю )
  • +1
    > Реализуйте метод, бросающий произвольный Throwable

    Unsafe.getUnsafe().throwException(new IOException());
    • +1
      Или так:

      Thread.currentThread().stop(new IOException());
      • 0
        Да, все верно, хоть и использовать deprecated методы и не комильфо;)
        Все-таки подход к управлению потоками кардинально поменялся и это как раз тот случай, когда deprecated == нельзя, имхо.

        Я не писал этот способ, потому что задание, все-таки, было «дополнительным» к дженерикам. Грубо говоря, на «усвоенный материал». На мой взгляд весьма забавное (как и каст нулла)

        В любом случае, добавлю в пост, спасибо.
    • 0
      Ну, на то он и Unsafe. Так не интересно;) Просто так не напишете. В любом случае, добавлю, спасибо
  • 0
    А в груви life is good :)
    • 0
      Нет
      • 0
        Хотите сказать, что у вас другой результат, или что жизнь программиста на груви не хороша?
        • 0
          Груви — это велосипед, от которого отказался автор языка. О чем тут еще можно говорить?
          • 0
            Не слышал про такое. Можете дать ссыль на инфу?
            Вообще, из всех велосипедов, которые мне пришлось гнуть, этот — самый вменяемый…
          • 0
            Ну, этот отказался, другие подобрали. Что это доказывает? Как из этого можно ввести, что это плохой язык?
            • +1
              Он не плохой, он просто не нужен. Ну скажем, есть в Groovy хотя бы pattern matching? А персистентные коллекции? Какую задачу вообще Groovy решает кроме того, что Java-код можно переписать чуть короче?
  • 0
    Автор) Я тебе подкину тоже веселый пример.

    public class GenericTest
    {
    	public static void main(String[] args)
    	{
    		Map<Integer, Long> v = new HashMap<Integer, Long>();
    		v.put(1, 0L);
    
    		Long val = v.isEmpty() ? 0 : v.get(2);
    
    		System.out.println(val);
    	}
    }
    


    Могу сказать код скомпилится на все JDK, и не выполнится на любой(тестил 6-7-8, мб и где то пофиксили но я не видел)
    • 0
      Конечно, у вас вот тут:

      Long val = v.isEmpty()? 0: v.get(2);

      NPE, надо вот так:

      Long val = v.isEmpty()? 0: v.get(1);
      • +1
        Нет, v.get(2) возвращает null, который легко печатается System.out.println(). Если пример выше переписать через if:
        import java.util.*;
        
        public class GenericTest {
            public static void main(String[] args) {
                Map<Integer, Long> v = new HashMap<Integer, Long>();
                v.put(1, 0L);
                if(v.isEmpty()) {
                    System.out.println(0);
                } else {
                    System.out.println(v.get(2));
                }   
            }   
        }
        

        То тоже будет работать. Как и другой код, что я уже написал. Так что вся хитрочть в операторе "?"
    • +3
      Это особенность не дженерика, а оператора "?". В Java он не умеет выводить тип. Вот так работает:

      import java.util.*;
      
      public class GenericTest {
          public static void main(String[] args) {
              Map<Integer, Long> v = new HashMap<Integer, Long>();
              v.put(1, 0L);
              Long val = v.isEmpty() ? Long.valueOf(0) : v.get(2);
              System.out.println(val);
          }   
      }
      
      
  • 0
    Вопрос на засыпку — придумать пример из жизни, когда использование таких конструкций реально _необходимо_ ;-)
    • +1
      Мне кажется, в программировании отсутствует понятие «необходимости» как таковое. Всегда есть какое-то альтернативное решение. Всегда есть «архитектура лучше/выше/быстрее», «код понятнее/короче/круче». Потому-то оно и искусством считается=)

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

      Эти задачки — это не паттерны, это синтетические примеры, в которых неочевидное поведение языка (например) приводит к неожиданным результатам. И если встретить подобный пример в живом проекте вряд ли удастся (и слава богу) — понимание этой неочевидности краевых случаев _необходимо_ (противоречу сам себе с необходимостью;)).

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

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