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


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


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. И это работает!


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


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

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


  1. Rumickon
    03.01.2020 16:08
    +1

    Спасибо за continue.


    1. tsypanov Автор
      03.01.2020 16:18

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


      1. Lure_of_Chaos
        03.01.2020 16:35
        +1

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

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


        1. tsypanov Автор
          03.01.2020 16:47

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


        1. Stiver
          04.01.2020 00:21

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


          1. Lure_of_Chaos
            04.01.2020 10:32

            break\continue с метками и так сильно критиковали, мол, это то же самое, что измененный goto label; очевидно, ограничение потому и наложено, чтобы не использовали управляющую конструкцию для того, для чего она не предназначена.

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


            1. BD9
              04.01.2020 15:52

              Это ты сам придумал или где-то прочитал?

              Критика goto

              Похоже, что отлаживать спагетти-код никогда не приходилось.


              1. Lure_of_Chaos
                04.01.2020 21:38

                > Это ты сам придумал или где-то прочитал?
                вспомнил, что когда-то давно придумал читал

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

                > Похоже, что отлаживать спагетти-код никогда не приходилось.
                Хех, какой только говнокод не приходилось отлаживать… И не только отлаживать (отладчиками), но и, простигосподи, пользоваться print\write\alert


      1. vlanko
        05.01.2020 16:50

        Я про break label узнал в 2016м, когда считал простые числа :).


    1. Shamanische
      04.01.2020 21:23

      А ещё можно делать так:

      someLabel: {
      
          if(conditionA) {
              //...
              break someLabel;
          }
      
          if(conditionB) {
              //...
              break someLabel;
          }
      
          if(conditionC) {
              //...
              break someLabel;
          }
      }
      

      Если выполниться одно из условий, то оператор break someLabel выведет выполнение из блока


  1. Lure_of_Chaos
    03.01.2020 16:43

    > assert может принимать 2 аргумента
    при том, что assert может не работать совсем — все зависит от флагов запуска.

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

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

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

    >
    Нет, остальное вообще, извините, детсад, комментарий аж застрял.


  1. alexzeed
    03.01.2020 17:52
    +1

    Ну про 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 — можно было бы написать: «а знаете ли вы что вот так можно?».


  1. AlteredARMOR
    03.01.2020 19:52

    Так обычно и бывает: когда начинаешь изучать технологию не по верхам (лишь бы скорее начать строчить код), а основательно, вдумчиво, сверяясь с документацией, то всплывает много «интересных вещей, о которых вы могли не знать».


    1. iLLuzor
      04.01.2020 10:59

      И которые с вероятностью 99.999% никогда не пригодятся и забудутся.


      1. Lure_of_Chaos
        04.01.2020 21:41

        Извините, но наоборот — не зная которые, потратишь в 10 раз больше времени на ловлю багов.
        Впрочем, зачастую помогает интуиция\здравый смысл.


  1. Firsto
    03.01.2020 19:58

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


    1. tsypanov Автор
      04.01.2020 21:52

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


  1. yarick123
    04.01.2020 23:39

    Двойственное ощущение от статьи. С одной стороны — просвещение, польза! Про «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. Поскольку обидно наступать на грабли, подложенные коллегой, который просто не знает базовых вещей.


    1. tsypanov Автор
      05.01.2020 22:55

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

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


      1. yarick123
        06.01.2020 03:21
        +1

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


        1. Maccimo
          06.01.2020 08:13
          +1

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


          1. yarick123
            06.01.2020 23:34

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

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

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

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


            1. Maccimo
              07.01.2020 04:00

              Мы с вами говорим об одном и том же, только по разному.

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


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


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

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


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

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


      1. mrsantak
        06.01.2020 05:42
        +1

        Вам осталось сделать крошечный шаг до осознания того, что String.class как раз и является постоянной.

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


        1. tsypanov Автор
          06.01.2020 17:40

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


  1. lanseg
    04.01.2020 12:42

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