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


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


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


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


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


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




К сожалению, не доступен сервер mySQL