Вещи, которые вы [возможно] не знали о Java

    Приветствую, читатель!


    Эта статья разбавит мой поток сознания о производительности. Поговорим о забавных вещах в яве и околояве, о которых вы возможно не знали. О некоторых из перечисленных я сам узнал недавно, так что считаю, что большинство читателей найдёт для себя хотя бы пару-тройку любопытных моментов.


    assert может принимать 2 аргумента


    Обычно assert используется для проверки некоторого условия и бросает AssertionError если условие не удовлетворяется. Чаще всего проверка выглядит так:


    assert list.isEmpty();

    Однако, она может быть и такой:


    assert list.isEmpty() : list.toString();

    Сообразительный читатель уже догадался, что второе выражение (кстати, оно ленивое) возвращает значение типа Object, которое передаётся в AssertionError и несёт пользователю дополнительные сведения об ошибке. Более формальное описание см. в соответствующем разделе спецификации языка: https://docs.oracle.com/javase/specs/jls/se13/html/jls-14.html#jls-14.10


    За без малого 6 с половиной лет работы с явой расширенное использование ключевого слова assert я видел лишь однажды.


    strictfp


    Это не ругательство — это малоизвестное ключевое слово. Если верить документации, его использование включает строгую арифметику для чисел с плавающей запятой:


    public interface NonStrict {
      float sum(float a, float b);
    }

    можно лёгким движением руки превратить в


    public strictfp interface Strict {
      float sum(float a, float b);
    }

    Также это ключевое слово может применятся к отдельным методам:


    public interface Mixed {
      float sum(float a, float b);
    
      strictfp float strictSum(float a, float b);
    }

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


    continue может принимать аргумент


    Узнал об этом на прошлой неделе. Обычно мы пишем так:


    for (Item item : items) {
      if (item == null) {
        continue;
      }
      use(item);
    }

    Подобное использование неявно предполагает возвращение в начало цикла и следующий проход. Иными словами, код выше можно переписать как:


    loop: for (Item item : items) {
      if (item == null) {
        continue loop;
      }
      use(item);
    }

    Однако, вернуться из цикла можно и во внешний цикл, если таковой имеется:


    @Test
    void test() {
      outer: for (int i = 0; i < 20; i++) {
        for (int j = 10; j < 15; j++) {
          if (j == 13) {
            continue outer;
          }
        }
      }
    }

    Обратите внимание, счётчик i при возвращении в точку outer не сбрасывается, так что цикл является конечным.


    При вызове vararg-метода без аргументов всё равно создаётся пустой массив


    Когда мы смотрим на вызов такого метода извне, то кажется, что беспокоится не о чем:


    @Benchmark
    public Object invokeVararg() {
      return vararg();
    }

    Мы ведь ничего не передали в метод, не так ли? А вот если посмотреть изнутри, то всё не так радужно:


    public Object[] vararg(Object... args) {
      return args;
    }

    Опыт подтверждает опасения:


    Benchmark                                  Mode  Cnt     Score    Error   Units
    invokeVararg                               avgt   20     3,715 ±  0,092   ns/op
    invokeVararg:·gc.alloc.rate.norm           avgt   20    16,000 ±  0,001    B/op
    invokeVararg:·gc.count                     avgt   20   257,000           counts

    Избавится от ненужного массива при отсутствии аргументов можно передавая null:


    @Benchmark
    public Object invokeVarargWithNull() {
      return vararg(null);
    }

    Сборщику мусора действительно полегчает:


    invokeVarargWithNull                       avgt   20     2,415 ±  0,067   ns/op
    invokeVarargWithNull:·gc.alloc.rate.norm   avgt   20    ≈ 10⁻⁵             B/op
    invokeVarargWithNull:·gc.count             avgt   20       ≈ 0           counts

    Код с null выглядит очень некрасиво, компилятор (и "Идея") будет ругаться, так что используйте этот подход в действительно горячем коде и снабдив его комментарием.


    Выражение switch-case не поддерживает java.lang.Class


    Этот код просто не компилируется:


    String to(Class<?> clazz) {
      switch (clazz) {
        case String.class: return "str";
        case Integer.class: return "int";
        default: return "obj";
      }
    }

    Смиритесь с этим.


    Тонкости присваивания и Class.isAssignableFrom()


    Есть код:


    int a = 0;
    Integer b = 10;
    
    a = b; // присваивание вполне работоспособно

    А теперь подумайте, какое значение вернёт этот метод:


    boolean check(Integer b) {
      return int.class.isAssignableFrom(b.getClass());
    }

    Прочитав название метода Class.isAssignableFrom() создаётся обманчивое впечатление, что выражение int.class.isAssignableFrom(b.getClass()) вернёт true. Мы ведь можем присвоить переменной типа int значение переменной типа Integer, не так ли?


    Однако метод check() вернёт false, так как в документации чётко прописано, что:


    /**
     * Determines if the class or interface represented by this
     * {@code Class} object is either the same as, or is a superclass or
     * superinterface of, the class or interface represented by the specified
     * {@code Class} parameter. It returns {@code true} if so;
     * otherwise it returns {@code false}. If this {@code Class}              // <---- !!!
     * object represents a primitive type, this method returns
     * {@code true} if the specified {@code Class} parameter is
     * exactly this {@code Class} object; otherwise it returns
     * {@code false}.
     *
     */
    @HotSpotIntrinsicCandidate
    public native boolean isAssignableFrom(Class<?> cls);

    Хоть int и не является наследником Integer-а (и наоборот) возможное взаимное присваивание — это особенность языка, а чтобы не вводить пользователей в заблуждение в документации сделана особая оговорка.


    Мораль: когда кажется — креститься надо надо перечитывать документацию.


    Из этого примера проистекает ещё один неочевидный факт:


    assert int.class != Integer.class;

    Класс int.class — это на самом деле Integer.TYPE, и чтобы убедиться в этом, достаточно посмотреть, во что будет скомпилирован этот код:


    Class<?> toClass() {
      return int.class;
    }

    Вжух:


    toClass()Ljava/lang/Class;
       L0
        LINENUMBER 11 L0
        GETSTATIC java/lang/Integer.TYPE : Ljava/lang/Class;
        ARETURN

    Открыв исходники java.lang.Integer увидим там вот это:


    @SuppressWarnings("unchecked")
    public static final Class<Integer> TYPE = (Class<Integer>) Class.getPrimitiveClass("int");

    Глядя на вызов Class.getPrimitiveClass("int") может возникнуть соблазн выпилить его и заменить на:


    @SuppressWarnings("unchecked")
    public static final Class<Integer> TYPE = int.class;

    Самое удивительное, что JDK с подобными изменениями (для всех примитивов) соберётся, а виртуальная машина запустится. Правда проработает она недолго:


    java.lang.IllegalArgumentException: Component type is null
        at jdk.internal.misc.Unsafe.allocateUninitializedArray(java.base/Unsafe.java:1379)
        at java.lang.StringConcatHelper.newArray(java.base/StringConcatHelper.java:458)
        at java.lang.StringConcatHelper.simpleConcat(java.base/StringConcatHelper.java:423)
        at java.lang.String.concat(java.base/String.java:1968)
        at jdk.internal.util.SystemProps.fillI18nProps(java.base/SystemProps.java:165)
        at jdk.internal.util.SystemProps.initProperties(java.base/SystemProps.java:103)
        at java.lang.System.initPhase1(java.base/System.java:2002)

    Ошибка вылезает вот здесь :


    class java.lang.StringConcatHelper {
    
     @ForceInline
     static byte[] newArray(long indexCoder) {
      byte coder = (byte)(indexCoder >> 32);
      int index = (int)indexCoder;
      return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, index << coder); //<--
     }
    
    }

    С упомянутыми изменениями byte.class возвращает null и ломает ансейф.


    Spring Data JPA позволяет объявить частично работоспособный репозиторий


    Завершу статью курьёзной ошибкой, возникшей на стыке Спринг Даты и Хибернейта. Вспомним, как мы объявляем репозиторий, обслуживающий некую сущность:


    @Entity
    public class SimpleEntity {
      @Id
      private Integer id;
    
      @Column
      private String name;
    }
    
    public interface SimpleRepository extends JpaRepository<SimpleEntity, Integer> {
    }

    Опытные пользователи знают, что при поднятии контекста Спринг Дата проверяет все репозитории и сразу валит всё приложение при попытке описать, к примеру, кривой запрос:


    public interface SimpleRepository extends JpaRepository<SimpleEntity, Integer> {
      @Query("слышь, парень, мелочь есть?")
      Optional<SimpleEntity> findLesserOfTwoEvils(); 
    }

    Однако, ничто не мешает нам объявить репозиторий с левым типом ключа:


    public interface SimpleRepository extends JpaRepository<SimpleEntity, Long> {
    }

    Этот репозиторий не только поднимется, но и будет частично работоспособен, например, метод findAll() отработает "на ура". А вот методы, использующие ключ ожидаемо упадут с ошибкой:


    IllegalArgumentException: Provided id of the wrong type for class SimpleEntity. Expected: class java.lang.Integer, got class java.lang.Long

    Всё дело в том, что Спринг Дата не сравнивает классы ключа сущности и ключа привязанного к ней репозитория. Происходит это не от хорошей жизни, а из-за неспособности Хибернейта выдать правильный тип ключа в определённых случаях: https://hibernate.atlassian.net/browse/HHH-10690


    В жизни я встретил подобное только один раз: в тестах (трольфейс) самой Спринг Даты, например, используемый в тестах org.springframework.data.jpa.repository.query.PartTreeJpaQueryIntegrationTests$UserRepository типизирован Long-ом, а в сущности User используется Integer. И это работает!


    На этом всё, надеюсь, мой обзор был вам полезен и интересен.


    Поздравляю вас с наступившим Новым годом и желаю копать яву глубже и шире!

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

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

    Комментарии 38

      +1
      Спасибо за continue.
        0

        Обращайтесь, я поражен был, если честно, когда впервые увидел ))

          +3
          использование меток тянется еще со времен Си, причем не только в continue, но и break, можно «прыгать» как вперед, так и назад, ограничение одно: метка не должна выходить за пределы цикла.
          удобная штука, когда надо прервать из вложенного цикла во внешний.

          з.ы. но стоит отметить, что эта фича используется столь редко, что давно считается дурным тоном — т.е. если вам она понадобилась, то стоит присмотреться к коду, нельзя ли его написать иначе и лучше? (а в некоторых статических анализаторах, например, встроенном в Jetbrains Idea, есть даже предупреждения об использовании break,continue, меток и вложенных циклов)
            0

            Про брейк не знал, спасибо!

              0
              ограничение одно: метка не должна выходить за пределы цикла
              Непонятное ограничение, кстати. Программа на языке высокого уровня (т.е. reducible CFG) состоит из вложенных друг в друга statement'ов, каждый из которых имеет один вход и максимум один выход. 'break' позволяет переместится к выходу любого statement'а в иерархии. Было бы логично, если бы 'continue' позволяло то же самое для входа, но почему-то оно ограничено только входами в циклы.
                0
                break\continue с метками и так сильно критиковали, мол, это то же самое, что измененный goto label; очевидно, ограничение потому и наложено, чтобы не использовали управляющую конструкцию для того, для чего она не предназначена.

                для сравнения:
                в бейсиках 80х годов каждый оператор обязан был иметь числовую метку (не совсем так, можно было группировать операторы) и называлась метка номером строки, а условные операторы не могли быть сложными (не было понятия {блок}), и потому постоянно приходилось использовать goto: в какой-то момент ненависть к такому принуждению визуально прыгать по частям кода зашкалила и оператор выдрали с корнем из всех языков
                  0
                  Это ты сам придумал или где-то прочитал?

                  Критика goto

                  Похоже, что отлаживать спагетти-код никогда не приходилось.
                    0
                    > Это ты сам придумал или где-то прочитал?
                    вспомнил, что когда-то давно придумал читал

                    >Критика goto
                    вот именно

                    > Похоже, что отлаживать спагетти-код никогда не приходилось.
                    Хех, какой только говнокод не приходилось отлаживать… И не только отлаживать (отладчиками), но и, простигосподи, пользоваться print\write\alert
              0
              Я про break label узнал в 2016м, когда считал простые числа :).
              +1
              А ещё можно делать так:
              someLabel: {
              
                  if(conditionA) {
                      //...
                      break someLabel;
                  }
              
                  if(conditionB) {
                      //...
                      break someLabel;
                  }
              
                  if(conditionC) {
                      //...
                      break someLabel;
                  }
              }
              

              Если выполниться одно из условий, то оператор break someLabel выведет выполнение из блока
              +3
              > assert может принимать 2 аргумента
              при том, что assert может не работать совсем — все зависит от флагов запуска.

              > strictfp
              лучше не использовать float, а использовать double, а еще лучше — использовать StrictMath

              > При вызове vararg-метода без аргументов всё равно создаётся пустой массив
              что очень удобно в циклах, ну и логично, если помнить, что vararg — это всего лишь syntax sugar

              > Выражение switch-case не поддерживает java.lang.Class
              А для чего это может понадобиться? Нет ли специфичного запашка?
              image

              >
              Нет, остальное вообще, извините, детсад, комментарий аж застрял.
                +2
                Ну про switch это как бы странно относить к разряду вещей, которые мы не знали про Java: это скорее «а с чего бы вдруг оно так было?». Вот четко написано в доке (я смотрел в java 12): The type of the Expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type (§8.9), or a compile-time error occurs. Вот если бы наоборот, в этом списке был еще Class — можно было бы написать: «а знаете ли вы что вот так можно?».
                  +3
                  Так обычно и бывает: когда начинаешь изучать технологию не по верхам (лишь бы скорее начать строчить код), а основательно, вдумчиво, сверяясь с документацией, то всплывает много «интересных вещей, о которых вы могли не знать».
                    0
                    И которые с вероятностью 99.999% никогда не пригодятся и забудутся.
                      0
                      Извините, но наоборот — не зная которые, потратишь в 10 раз больше времени на ловлю багов.
                      Впрочем, зачастую помогает интуиция\здравый смысл.
                    0

                    А теперь скажите, кому это пригодилось в продакшене?

                      +1

                      Мне. На проблему со Спринг Датой я почти наступил.

                      +4
                      Двойственное ощущение от статьи. С одной стороны — просвещение, польза! Про «strictfp» не помню, чтобы слышал. С другой стороны — простите, детский сад какой-то.

                      Если использовать assert, хорошо бы знать, как он работает. Как только он появился, сразу прочитал про него всё, а не остановился на выжимке, что появилась мол, новая функциональность в языке.

                      Switch-case тоже, базовая конструкция языка, вроде. Либо используем и знаем, как она работает, либо не удивляемся, что она имеет ограничения. Раньше вообще можно было только числа использовать. (Да, тут я рассматриваю char как число). И, на всякий случай, в case можно писать только константу/константное выражение.

                      Break и continue — я бы удивился, если бы не было возможности указать метку. Как уже говорили выше — если пишешь на java, хорошо бы понимать, откуда растут ноги.

                      Вызов vararg-метода без аргументов. У меня единственный вопрос, а почему вообще ожидается, что массив, который содержит аргументы, внезапно может оказаться и не массивом вовсе, а null? Тут же, вроде, всё логично: длина массива равна количеству аргументов. Ноль аргументов — массив длины ноль. Или у меня логика ущербная? И предвосхищая комментарий: «ну, производительность-же!», опять же, сошлюсь на логику.

                      int.class.isAssignableFrom(Integer.class) — ну autoboxing-же. Если об этом не задумываться или не знать о его существовании, то как же жить-то дальше? int.class != Integer.class туда же. Это же базовые понятия языка — примитивные и непримитивные типы.

                      А по вопросу, помогли ли эти знания в production, скажу, что мат-часть хорошо бы знать. Иначе использовать её на полную не получится. А если не знать, то процесс уже напоминает раскладывание грабель. И очень повезёт, если как в случае с switch — case, код просто не скомпилируется. Так что то, что подобные вещи не знают программисты, которые работают над тем же проектом, это очень даже мешает в production. Поскольку обидно наступать на грабли, подложенные коллегой, который просто не знает базовых вещей.
                        0
                        Switch-case тоже, базовая конструкция языка, вроде. Либо используем и знаем, как она работает, либо не удивляемся, что она имеет ограничения. Раньше вообще можно было только числа использовать. (Да, тут я рассматриваю char как число). И, на всякий случай, в case можно писать только константу/константное выражение.

                        Вам осталось сделать крошечный шаг до осознания того, что String.class как раз и является постоянной. Я ведь недаром вписал пример про switch-case, эта конструкция не так проста, как кажется, но, похоже, почти никто не заметил скрытого смысла. Думаю, я сделаю про это отдельную запись.

                          +2
                          Я не очень хорошо помню, есть ли в спецификации jls/jvms понятие «постоянная». Но вынужден заметить, что ни константным выражением, ни константой String.class не является. Иными словами, на этапе компиляции значение данного выражения не может быть определено. Именно поэтому оно и не может быть использовано в case. Таблицы поиска для switch/case генерируются на этапе компиляции. Для String-значений switch компилируется в код, использующий две таблицы поиска. Первая из них содержит hashCode образцов. То, что вы предлагаете, вынуждает либо отказаться от эффективной реализации switch/case путём замены её на линейную цепочку проверок, либо утяжелить загрузку классов, переложив на неё коррекцию соответствующих таблиц поиска (для эффективного сравнения используется hashCode). Вот такой вот «крошечный шаг».
                            +2
                            Иными словами, на этапе компиляции значение данного выражения не может быть определено. Именно поэтому оно и не может быть использовано в case.

                            Class literal в case нельзя использовать потому, что в JLS11 §14.11 его поддержка не заявлена:


                            The type of the Expression must be char, byte, short, int, Character, Byte, Short, Integer, String, or an enum type (§8.9), or a compile-time error occurs

                            java.lang.Class в списке разрешённых типов отсутствует, точка.
                            Добавят в спецификацию разрешение использовать java.lang.Class — допилят и компилятор и VM.


                            В какой конкретно байткод это будет транслироваться — дело десятое.


                            P.S. Class literals, хоть тот же String.class, на уровне байткода вполне себе загружаются из пула констант.

                              0
                              Мы с вами говорим об одном и том же, только по разному. Вы отмечаете, что это не сделано потому, что было заявлено, что это сделано не будет. Я отмечаю, что реализовавать это было бы весьма проблематичто, поэтому, скорее всего, и было принято решение этого не делать. Им и со строками-то уже сильно выворачиваться пришлось. Ведь в конечном итоге, это не вызывает проблем с совместимостью, и, я думаю, много кто просил расширить конструкцию switch-case существенно больше, чем это было сделано.

                              Добавят в спецификацию разрешение использовать java.lang.Class — допилят и компилятор и VM.

                              В какой конкретно байткод это будет транслироваться — дело десятое.

                              По-моему, это утверждение слегка неосторожное. Складывается такое впечатление, что главное — это принять решение, а выполнимо ли оно с заданным качеством — «дело десятое»…
                                +1
                                Мы с вами говорим об одном и том же, только по разному.

                                Нет, ваша позиция — «Это очень сложно, потому и не сделали», моя — «Руки не дошли, вот и не сделали».


                                В пользу моей точки зрения говорит JDK-8213076 Pattern matching for switch, который как раз и посвящён добавлению этой возможности в язык, а вовсе не объяснению того, что это слишком сложно сделать.


                                Им и со строками-то уже сильно выворачиваться пришлось.

                                Сомневаюсь, что реализация вызвала сколь-нибудь серьёзные трудности. Принять решение — да, пришлось.


                                Складывается такое впечатление, что главное — это принять решение, а выполнимо ли оно с заданным качеством — «дело десятое»…

                                Не нужно передёргивать, я говорил про байткод. На уровне байткода этот switch вполне может выродиться в пару invokedynamic/tableswitch, которые JIT заменит на intrinsics.
                                Синтаксический сахар это совсем не космические технологии.

                                  0
                                  В пользу моей точки зрения говорит JDK-8213076 Pattern matching for switch...
                                  Уели. Согласен.
                                  Не нужно передёргивать, я говорил про байткод
                                  Я написал то, как можно легко понять ваше высказывание. Поэтому, собственно, и абзац начал с: «По-моему, это утверждение слегка неосторожное».
                            +2
                            Вам осталось сделать крошечный шаг до осознания того, что String.class как раз и является постоянной.

                            С чего бы? Я могу один и тот же класс грузить разными class loader'ми и это будут разные объекты.

                              +1

                              Я ведь не зря написал String.class и Integer.class: классы из java.lang, ЕМНИП, загружаются при запуске виртуальной машины и только один раз.

                          0

                          Про continue/break и varargs знал, а вот про assert — нет. Если честно, вообще не помню, когда я последний раз видел assert в коде

                            +2
                            А вот такую вещь вы знали? В Java есть ещё одна форма комментариев:
                            this_is_for_loop:
                            for (int i = 0; i < 10; i++) {
                                print_variable_i:
                                System.out.println(i);
                            }
                              0

                              Не знал )) Ставлю жирный плюс!

                                0

                                А есть ещё одна форма комментариев, уже "комбинированная" :)


                                trollface.jpg
                                public class C {
                                    public static void main(String[] args) {
                                        http://habr.com
                                    }
                                }
                                  0
                                  Не скомпилируется. Нужен statement.
                                    0

                                    Это был пример с подсветкой синтаксиса, а не рабочий вариант. :)

                                0
                                Это ж метка цикла.
                                  0

                                  Не знал, что System.out.println это цикл

                                +1

                                Кстати, IntelliJ IDEA умеет переписать за вас свитч по классам. Так что смело свитчуйтесь по любым объектам, IDEA всё поправит:



                                Результат:


                                if (String.class.equals(clazz)) {
                                    return "str";
                                } else if (Integer.class.equals(clazz)) {
                                    return "int";
                                }
                                return "obj";
                                  0

                                  10 минут назад обновился ))

                                    0

                                    Этой фиче сто лет в обед.

                                      0

                                      Я про неё узнал только что из твоего верхнего комментария )

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

                                Самое читаемое