Приветствую, читатель!
Эта статья разбавит мой поток сознания о производительности. Поговорим о забавных вещах в яве и околояве, о которых вы возможно не знали. О некоторых из перечисленных я сам узнал недавно, так что считаю, что большинство читателей найдёт для себя хотя бы пару-тройку любопытных моментов.
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
. И это работает!
На этом всё, надеюсь, мой обзор был вам полезен и интересен.
Поздравляю вас с наступившим Новым годом и желаю копать яву глубже и шире!
ссылка на оригинал статьи https://habr.com/ru/post/482364/
Добавить комментарий