Вещи, которые вы [возможно] не знали о 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 TYPE = (Class) Class.getPrimitiveClass("int");
Глядя на вызов Class.getPrimitiveClass("int")
может возникнуть соблазн выпилить его и заменить на:
@SuppressWarnings("unchecked")
public static final Class 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 {
}
Опытные пользователи знаю, что при поднятии контекста Спринг Дата проверяет все репозитории и сразу валит всё приложение при попытке описать, к примеру, кривой запрос:
public interface SimpleRepository extends JpaRepository {
@Query("слышь, парень, мелочь есть?")
Optional findLesserOfTwoEvils();
}
Однако, ничто не мешает нам объявить репозиторий с левым типом ключа:
public interface SimpleRepository extends JpaRepository {
}
Этот репозиторий не только поднимется, но и будет частично работоспособен, например, метод 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
. И это работает!
На этом всё, надеюсь, мой обзор был вам полезен и интересен.
Поздравляю вас с наступившим Новым годом и желаю копать яву глубже и шире!