Приветствую, читатель!
Эта статья разбавит мой поток сознания о производительности. Поговорим о забавных вещах в яве и околояве, о которых вы возможно не знали. О некоторых из перечисленных я сам узнал недавно, так что считаю, что большинство читателей найдёт для себя хотя бы пару-тройку любопытных моментов.
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)
Lure_of_Chaos
03.01.2020 16:43> assert может принимать 2 аргумента
при том, что assert может не работать совсем — все зависит от флагов запуска.
> strictfp
лучше не использовать float, а использовать double, а еще лучше — использовать StrictMath
> При вызове vararg-метода без аргументов всё равно создаётся пустой массив
что очень удобно в циклах, ну и логично, если помнить, что vararg — это всего лишь syntax sugar
> Выражение switch-case не поддерживает java.lang.Class
А для чего это может понадобиться? Нет ли специфичного запашка?
>
Нет, остальное вообще, извините, детсад, комментарий аж застрял.
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 — можно было бы написать: «а знаете ли вы что вот так можно?».
AlteredARMOR
03.01.2020 19:52Так обычно и бывает: когда начинаешь изучать технологию не по верхам (лишь бы скорее начать строчить код), а основательно, вдумчиво, сверяясь с документацией, то всплывает много «интересных вещей, о которых вы могли не знать».
iLLuzor
04.01.2020 10:59И которые с вероятностью 99.999% никогда не пригодятся и забудутся.
Lure_of_Chaos
04.01.2020 21:41Извините, но наоборот — не зная которые, потратишь в 10 раз больше времени на ловлю багов.
Впрочем, зачастую помогает интуиция\здравый смысл.
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. Поскольку обидно наступать на грабли, подложенные коллегой, который просто не знает базовых вещей.tsypanov Автор
05.01.2020 22:55Switch-case тоже, базовая конструкция языка, вроде. Либо используем и знаем, как она работает, либо не удивляемся, что она имеет ограничения. Раньше вообще можно было только числа использовать. (Да, тут я рассматриваю char как число). И, на всякий случай, в case можно писать только константу/константное выражение.
Вам осталось сделать крошечный шаг до осознания того, что
String.class
как раз и является постоянной. Я ведь недаром вписал пример про switch-case, эта конструкция не так проста, как кажется, но, похоже, почти никто не заметил скрытого смысла. Думаю, я сделаю про это отдельную запись.yarick123
06.01.2020 03:21+1Я не очень хорошо помню, есть ли в спецификации jls/jvms понятие «постоянная». Но вынужден заметить, что ни константным выражением, ни константой String.class не является. Иными словами, на этапе компиляции значение данного выражения не может быть определено. Именно поэтому оно и не может быть использовано в case. Таблицы поиска для switch/case генерируются на этапе компиляции. Для String-значений switch компилируется в код, использующий две таблицы поиска. Первая из них содержит hashCode образцов. То, что вы предлагаете, вынуждает либо отказаться от эффективной реализации switch/case путём замены её на линейную цепочку проверок, либо утяжелить загрузку классов, переложив на неё коррекцию соответствующих таблиц поиска (для эффективного сравнения используется hashCode). Вот такой вот «крошечный шаг».
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
, на уровне байткода вполне себе загружаются из пула констант.yarick123
06.01.2020 23:34Мы с вами говорим об одном и том же, только по разному. Вы отмечаете, что это не сделано потому, что было заявлено, что это сделано не будет. Я отмечаю, что реализовавать это было бы весьма проблематичто, поэтому, скорее всего, и было принято решение этого не делать. Им и со строками-то уже сильно выворачиваться пришлось. Ведь в конечном итоге, это не вызывает проблем с совместимостью, и, я думаю, много кто просил расширить конструкцию switch-case существенно больше, чем это было сделано.
Добавят в спецификацию разрешение использовать java.lang.Class — допилят и компилятор и VM.
В какой конкретно байткод это будет транслироваться — дело десятое.
По-моему, это утверждение слегка неосторожное. Складывается такое впечатление, что главное — это принять решение, а выполнимо ли оно с заданным качеством — «дело десятое»…Maccimo
07.01.2020 04:00Мы с вами говорим об одном и том же, только по разному.
Нет, ваша позиция — «Это очень сложно, потому и не сделали», моя — «Руки не дошли, вот и не сделали».
В пользу моей точки зрения говорит JDK-8213076 Pattern matching for switch, который как раз и посвящён добавлению этой возможности в язык, а вовсе не объяснению того, что это слишком сложно сделать.
Им и со строками-то уже сильно выворачиваться пришлось.
Сомневаюсь, что реализация вызвала сколь-нибудь серьёзные трудности. Принять решение — да, пришлось.
Складывается такое впечатление, что главное — это принять решение, а выполнимо ли оно с заданным качеством — «дело десятое»…
Не нужно передёргивать, я говорил про байткод. На уровне байткода этот
switch
вполне может выродиться в паруinvokedynamic
/tableswitch
, которые JIT заменит на intrinsics.
Синтаксический сахар это совсем не космические технологии.
mrsantak
06.01.2020 05:42+1Вам осталось сделать крошечный шаг до осознания того, что String.class как раз и является постоянной.
С чего бы? Я могу один и тот же класс грузить разными class loader'ми и это будут разные объекты.
tsypanov Автор
06.01.2020 17:40Я ведь не зря написал String.class и Integer.class: классы из java.lang, ЕМНИП, загружаются при запуске виртуальной машины и только один раз.
lanseg
04.01.2020 12:42Про continue/break и varargs знал, а вот про assert — нет. Если честно, вообще не помню, когда я последний раз видел assert в коде
Rumickon
Спасибо за continue.
tsypanov Автор
Обращайтесь, я поражен был, если честно, когда впервые увидел ))
Lure_of_Chaos
использование меток тянется еще со времен Си, причем не только в continue, но и break, можно «прыгать» как вперед, так и назад, ограничение одно: метка не должна выходить за пределы цикла.
удобная штука, когда надо прервать из вложенного цикла во внешний.
з.ы. но стоит отметить, что эта фича используется столь редко, что давно считается дурным тоном — т.е. если вам она понадобилась, то стоит присмотреться к коду, нельзя ли его написать иначе и лучше? (а в некоторых статических анализаторах, например, встроенном в Jetbrains Idea, есть даже предупреждения об использовании break,continue, меток и вложенных циклов)
tsypanov Автор
Про брейк не знал, спасибо!
Stiver
Lure_of_Chaos
break\continue с метками и так сильно критиковали, мол, это то же самое, что измененный goto label; очевидно, ограничение потому и наложено, чтобы не использовали управляющую конструкцию для того, для чего она не предназначена.
для сравнения:
в бейсиках 80х годов каждый оператор обязан был иметь числовую метку (не совсем так, можно было группировать операторы) и называлась метка номером строки, а условные операторы не могли быть сложными (не было понятия {блок}), и потому постоянно приходилось использовать goto: в какой-то момент ненависть к такому принуждению визуально прыгать по частям кода зашкалила и оператор выдрали с корнем из всех языков
BD9
Это ты сам придумал или где-то прочитал?
Критика goto
Похоже, что отлаживать спагетти-код никогда не приходилось.
Lure_of_Chaos
> Это ты сам придумал или где-то прочитал?
вспомнил, что когда-то давно
придумалчитал>Критика goto
вот именно
> Похоже, что отлаживать спагетти-код никогда не приходилось.
Хех, какой только
говнокод не приходилось отлаживать… И не только отлаживать (отладчиками), но и, простигосподи, пользоваться print\write\alertvlanko
Я про break label узнал в 2016м, когда считал простые числа :).
Shamanische
А ещё можно делать так:
Если выполниться одно из условий, то оператор break someLabel выведет выполнение из блока