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


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


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                                         Score    Error   Units
invokeVararg                                      3,715 ±  0,092   ns/op
invokeVararg:·gc.alloc.rate.norm                 16,000 ±  0,001    B/op
invokeVararg:·gc.count                          257,000           counts

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


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

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


invokeVarargWithNull                              2,415 ±  0,067   ns/op
invokeVarargWithNull:·gc.alloc.rate.norm         ≈ 10⁻⁵             B/op
invokeVarargWithNull:·gc.count                      ≈ 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


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


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


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