2 «простых» вопроса по джаве


    Приветствую, уважаемый читатель.
    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 за пример.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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
                                Автор) Я тебе подкину тоже веселый пример.

                                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
                                      Мне кажется, в программировании отсутствует понятие «необходимости» как таковое. Всегда есть какое-то альтернативное решение. Всегда есть «архитектура лучше/выше/быстрее», «код понятнее/короче/круче». Потому-то оно и искусством считается=)

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

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

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

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