Записи — это новая функция предварительного просмотра в Java 14 , обеспечивающая приятный компактный синтаксис для объявления классов, которые должны быть глупыми держателями данных. В этой статье мы рассмотрим, как выглядят записи под капотом. Так пристегнуться!
Представление класса
Давайте начнем с очень простого примера:
Как насчет компиляции этого кода с помощью javac
:
Затем можно взглянуть на сгенерированный байт-код, используя javap
:
Это напечатает следующее:
Интересно, что, подобно Enums , Records — это обычные Java-классы с несколькими фундаментальными свойствами:
- Они объявлены как
final
классы, поэтому мы не можем наследовать от них. - Они уже наследуются от другого класса с именем
java.lang.Record
. Поэтому Records не может расширять любой другой класс, поскольку Java не допускает множественное наследование. - Записи могут реализовывать другие интерфейсы.
- Для каждого компонента есть метод доступа, например,
max
иmin
. - Есть автоматически генерируемые реализации
toString
,equals
и наhashCode
основе всех компонентов. - Наконец, есть автоматически сгенерированный конструктор, который принимает все компоненты в качестве аргументов.
Кроме того, java.lang.Record
это просто абстрактный класс с защищенным конструктором без аргументов и несколькими другими основными абстрактными методами:
Ничего особенного в этом классе!
Вам также может понравиться: Представляем Java Record
Любопытный случай классов данных
Исходя из Kotlin или Scala, можно заметить некоторые сходства между записями в Java, классами данных в Kotlin и классами Case в Scala . На первый взгляд, все они имеют одну очень фундаментальную цель: облегчить написание держателей данных.
Несмотря на это фундаментальное сходство, на уровне байт-кода все сильно отличается.
Класс данных Котлина
Для сравнения давайте рассмотрим класс данных Kotlin, эквивалентный Range
:
Подобно Records, Котлин компилятор генерирует аксессоры методы, по умолчанию toString
, equals
и hashCode
реализации и несколько дополнительных функций , основанных на этом простом однострочнике.
Давайте посмотрим, как компилятор Kotlin генерирует код, скажем, для toString
:
Мы выпустили javap -c -v Range
для генерации этого вывода. Кроме того, здесь мы используем простые имена классов для краткости.
В любом случае, Kotlin использует StringBuilder
для генерации строковое представление вместо множественных конкатенаций строк (как любой хороший Java-разработчик!). Это:
- Сначала создается новый экземпляр
StringBuilder
(индекс 0, 3, 4). - Затем он добавляет буквальную
Range(min=
строку (индекс 7, 9). - Затем добавляется фактическое минимальное значение (индекс 12, 13, 16).
- Затем он добавляет литерал
, max=
(индекс 19, 21). - Затем добавляется фактическое максимальное значение (индекс 24, 25, 28).
- Затем он закрывает скобки, добавляя
)
литерал (индекс 31, 33). - Наконец, он создает
StringBuilder
экземпляр и возвращает его (индекс 36, 39).
По сути, чем больше у нас свойств в нашем классе данных, тем длиннее байт-код и, следовательно, тем больше время запуска.
Класс случая Скалы
Давайте напишем case class
эквивалент в Scala:
На первый взгляд кажется, что Scala генерирует гораздо более простую toString
реализацию:
Однако toString
вызывает scala.runtime.ScalaRunTime._toString
статический метод . Это, в свою очередь, вызывает productIterator
метод для перебора этого типа продукта. Этот итератор вызывает productElement
метод, который выглядит следующим образом:
Это в основном переключает все свойства case class
. Например, если productIterator
свойство хочет первое свойство, оно возвращает min
. Также, когда productIterator
требуется второй элемент, он вернет max
значение. В противном случае он сгенерирует экземпляр IndexOutOfBoundsException
сигнала о выходе запроса за пределы.
Опять же, чем больше у нас свойств в a case class
, тем больше будет этих переключателей. Следовательно, длина байт-кода пропорциональна количеству свойств. Другими словами, та же проблема, что и у Котлина data class
.
Invoke Dynamic
Давайте еще ближе рассмотрим байт-код, сгенерированный для записей Java:
Независимо от количества компонентов записи, это будет байт-код. Простое, полированное и элегантное решение. Но как это invokedynamic
работает?
Представляем Indy
Invoke Dynamic (также известный как Indy) был частью JSR 292, намереваясь улучшить поддержку JVM для динамических языков. После первого выпуска в Java 7 invokedynamic
код операции и его java.lang.invoke
багаж довольно широко используются динамическими языками на основе JVM, такими как JRuby.
Хотя Indy был специально разработан для улучшения поддержки динамического языка, он предлагает гораздо больше. Фактически, это подходит для использования там, где разработчику языка нужна любая форма динамичности, от акробатики динамического типа до динамических стратегий! Например, лямбда-выражения Java 8 фактически реализованы с использованием языка invokedynamic
Java, хотя это статически типизированный язык!
Определяемый пользователем байт-код
В течение достаточно долгого времени JVM поддерживала четыре типа invokestatic
вызова методов : вызывать статические методы, invokeinterface
вызывать методы интерфейса, invokespecial
вызывать конструкторы super()
или private
методы и invokevirtual
вызывать методы экземпляра.
Несмотря на различия, эти типы вызовов имеют одну общую черту: мы не можем обогатить их своей собственной логикой. Напротив, invokedynamic
позволяет нам загружать процесс вызова любым способом, каким мы захотим. Затем JVM позаботится о прямом вызове загрузочного метода.
Как работает инди?
В первый раз, когда JVM видит invokedynamic
инструкцию, она вызывает специальный статический метод Bootstrap Method . Метод начальной загрузки - это фрагмент кода Java, который мы написали для подготовки фактической логики, которая будет вызвана :
Затем метод начальной загрузки возвращает экземпляр java.invoke.CallSite
. Это CallSite
держит ссылку на фактический метод, то есть MethodHandle
. С этого момента каждый раз, когда JVM invokedynamic
снова видит эту инструкцию, она пропускает медленный путь и напрямую вызывает основной исполняемый файл . JVM продолжает пропускать медленный путь, если что-то не меняется.
Почему Инди?
В отличие от API Reflection, java.lang.invoke
API довольно эффективен, поскольку JVM может полностью видеть все вызовы. Поэтому JVM может применять все виды оптимизаций, пока мы максимально избегаем медленного пути!
В дополнение к аргументу эффективности, этот invokedynamic
подход является более надежным и менее хрупким из-за своей простоты.
Кроме того, сгенерированный байт-код для записей Java не зависит от количества свойств. Таким образом, меньше байт-кода и более быстрое время запуска.
Наконец, давайте предположим, что новая версия Java включает новую и более эффективную реализацию метода начальной загрузки. С invokedynamic
, наше приложение может воспользоваться этим усовершенствованием без перекомпиляции. Таким образом, у нас есть какая-то прямая двоичная совместимость . Кроме того, это динамическая стратегия, о которой мы говорили!
Методы объекта
Теперь, когда мы достаточно знакомы с Indy, давайте разберемся с invokedynamic
байт-кодом в записях:
Посмотрите, что я нашел в таблице методов Bootstrap :
Таким образом, вызывается метод начальной загрузки для Records, bootstrap
который находится в java.lang.runtime.ObjectMethods
классе. Как видите, этот метод начальной загрузки ожидает следующие параметры:
- Экземпляр
MethodHandles.Lookup
представления контекста поиска (Ljava/lang/invoke/MethodHandles$Lookup
часть). - Имя методы (то есть
toString
,equals
,hashCode
и т.д.) начальная загрузка будет ссылка. Например, когда значение равноtoString
, bootstrap вернетConstantCallSite
(a,CallSite
который никогда не изменяется), которое указывает на фактическуюtoString
реализацию для этой конкретной Записи. - Для
TypeDescriptor
метода (Ljava/lang/invoke/TypeDescriptor
часть). - Маркер типа, т. Е.
Class<?>
Представляющий тип класса Record. ЭтоClass<Range>
в этом случае. - Пол-двоеточия списка всех компонентов имен, то есть
min;max
. - Один
MethodHandle
на компонент. Таким образом, метод начальной загрузки может создать наMethodHandle
основе компонентов для этой конкретной реализации метода.
invokedynamic
Инструкция передает все эти аргументы метода начальной загрузки. Метод Bootstrap, в свою очередь, возвращает экземпляр ConstantCallSite
. Это ConstantCallSite
содержит ссылку на запрошенную реализацию метода, например toString
.
Размышляя о записях
java.lang.Class
API был модернизирован для поддержки записи. Например, учитывая Class<?>
, мы можем проверить, является ли это Запись или нет, используя новый isRecord
метод:
Это, очевидно, возвращает false
для не-типов записей:
Существует также getRecordComponents
метод, который возвращает массив RecordComponent
в том же порядке, который они определили в исходной записи. Каждый java.lang.reflect.RecordComponent
представляет компонент записи или переменную текущего типа записи. Например, RecordComponent.getName
возвращает имя компонента:
Точно так же getType
метод возвращает токен типа для каждого компонента:
Можно даже получить дескриптор методов доступа через getAccessor
:
Аннотирующие записи
Java позволяет аннотировать записи, если аннотация применима к записи или ее элементам. Кроме того, будет новая аннотация ElementType
называется RECORD_COMPONENT
. Аннотации с этой целью могут использоваться только для компонентов записи:
Сериализация
Любая новая функция Java без неприятных отношений с сериализацией была бы неполной. На этот раз, однако, отношения звучат не так непривлекательно, как мы привыкли.
Хотя записи по умолчанию не сериализуемы, сделать это можно, просто внедрив java.io.Serializable
интерфейс маркера.
Сериализуемые записи сериализуются и десериализуются иначе, чем обычные сериализуемые объекты. Обновленный Javadoc для ObjectInputStream
утверждает, что:
- Сериализованная форма объекта записи представляет собой последовательность значений, полученных из компонентов записи .
- Процесс, с помощью которого объекты записей сериализуются или экспортируются, не может быть настроен ; любой класс-специфический
writeObject
,readObject
,readObjectNoData
,writeExternal
, иreadExternal
методы , определенные классы записей игнорируются во время сериализации и десериализации. - Класс
serialVersionUID
записи0L
не указан явно.
Заключение
Записи Java собираются предоставить новый способ инкапсуляции держателей данных. Хотя в настоящее время они ограничены с точки зрения функциональности (по сравнению с тем, что предлагают Kotlin или Scala), реализация является надежной.
Первый предварительный просмотр Records будет доступен в марте 2020 года. В этой статье мы использовали openjdk 14-ea 2020-03-17
сборку, поскольку Java 14 еще не выпущена!
Дальнейшее чтение
Первый взгляд на записи в Java 14
Потенциальные ловушки в классах данных Kotlin