1. Обзор
Java 16 , выпущенный 16 марта 2021 года, является последним краткосрочным добавочным выпуском, основанным на Java 15 . В этом выпуске есть несколько интересных функций, таких как записи и запечатанные классы.
В этой статье мы рассмотрим некоторые из этих новых функций.
2. Вызов методов по умолчанию из экземпляров прокси (JDK-8159746)
В качестве улучшения метода по умолчанию в интерфейсах с выпуском Java 16 была добавлена поддержка java.lang.reflect.InvocationHandler для
вызова методов интерфейса по умолчанию через динамический прокси с использованием отражения.
Чтобы проиллюстрировать это, давайте рассмотрим пример простого метода по умолчанию:
interface HelloWorld {
default String hello() {
return "world";
}
}
Благодаря этому усовершенствованию мы можем вызывать метод по умолчанию на прокси этого интерфейса, используя отражение:
Object proxy = Proxy.newProxyInstance(getSystemClassLoader(), new Class<?>[] { HelloWorld.class },
(prox, method, args) -> {
if (method.isDefault()) {
return InvocationHandler.invokeDefault(prox, method, args);
}
// ...
}
);
Method method = proxy.getClass().getMethod("hello");
assertThat(method.invoke(proxy)).isEqualTo("world");
3. Поддержка дневного периода (JDK-8247781)
Новым дополнением к DateTimeFormatter является символ периода дня « B
», который представляет собой альтернативу формату am/pm:
LocalTime date = LocalTime.parse("15:25:08.690791");
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("h B");
assertThat(date.format(formatter)).isEqualTo("3 in the afternoon");
Вместо чего-то вроде « 3 часа дня
» мы получаем вывод « 3 часа дня
». Мы также можем использовать шаблон DateTimeFormatter
« B
», « BBBB
» или « BBBBB
» для коротких, полных и узких стилей соответственно. ``
4. Добавьте метод Stream.toList
(JDK-8180352)
Цель состоит в том, чтобы уменьшить шаблон с помощью некоторых часто используемых сборщиков Stream
, таких как Collectors.toList
и Collectors.toSet
:
List<String> integersAsString = Arrays.asList("1", "2", "3");
List<Integer> ints = integersAsString.stream().map(Integer::parseInt).collect(Collectors.toList());
List<Integer> intsEquivalent = integersAsString.stream().map(Integer::parseInt).toList();
Наш пример с ints
работает по-старому, но intsEquivalent
дает тот же результат и более лаконичен.
5. Инкубатор Vector API (JEP-338)
Vector API находится на начальной стадии инкубации для Java 16. Идея этого API состоит в том, чтобы предоставить средства векторных вычислений, которые в конечном итоге смогут работать более оптимально (при поддержке архитектур ЦП), чем традиционный скалярный метод вычислений.
Давайте посмотрим, как мы могли бы традиционно перемножить два массива:
int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};
var c = new int[a.length];
for (int i = 0; i < a.length; i++) {
c[i] = a[i] * b[i];
}
Этот пример скалярного вычисления для массива длины 4 будет выполняться за 4 цикла. Теперь давайте посмотрим на эквивалентное векторное вычисление:
int[] a = {1, 2, 3, 4};
int[] b = {5, 6, 7, 8};
var vectorA = IntVector.fromArray(IntVector.SPECIES_128, a, 0);
var vectorB = IntVector.fromArray(IntVector.SPECIES_128, b, 0);
var vectorC = vectorA.mul(vectorB);
vectorC.intoArray(c, 0);
Первое, что мы делаем в векторном коде, — это создаем два IntVectors
из наших входных массивов, используя статический фабричный метод этого класса fromArray.
Первый параметр — это размер вектора, за которым следует массив и смещение (здесь установлено значение 0). Самое главное здесь — это размер вектора, который мы получаем до 128 бит. В Java для хранения каждого int
требуется 4 байта.
Поскольку у нас есть входной массив из 4 целых чисел,
для его хранения требуется 128 бит. Наш единственный вектор
может хранить весь массив.
На определенных архитектурах компилятор сможет оптимизировать байтовый код, чтобы сократить количество вычислений с 4 до 1 цикла. Эти оптимизации приносят пользу таким областям, как машинное обучение и криптография.
Следует отметить, что нахождение на стадии инкубации означает, что этот Vector API может быть изменен в новых версиях.
6. Записи (JEP-395)
Записи были представлены в Java 14. В Java 16 внесены некоторые дополнительные изменения .
Записи похожи на enum
тем, что они являются ограниченной формой класса. Определение записи
— это краткий способ определения неизменяемого объекта хранения данных.
6.1. Пример без записей
Во-первых, давайте определим класс Book :
public final class Book {
private final String title;
private final String author;
private final String isbn;
public Book(String title, String author, String isbn) {
this.title = title;
this.author = author;
this.isbn = isbn;
}
public String getTitle() {
return title;
}
public String getAuthor() {
return author;
}
public String getIsbn() {
return isbn;
}
@Override
public boolean equals(Object o) {
// ...
}
@Override
public int hashCode() {
return Objects.hash(title, author, isbn);
}
}
Для создания простых классов хранения данных в Java требуется много шаблонного кода. Это может быть громоздко и привести к ошибкам, когда разработчики не предоставляют все необходимые методы, такие как equals
и hashCode
.
Точно так же иногда разработчики пропускают необходимые шаги для создания правильных неизменяемых классов . Иногда мы заканчиваем тем, что повторно используем класс общего назначения, а не определяем специализированный класс для каждого отдельного варианта использования.
Большинство современных IDE предоставляют возможность автоматически генерировать код (такой как сеттеры, геттеры, конструкторы и т. д.), что помогает смягчить эти проблемы и сократить накладные расходы разработчика, пишущего код. Однако записи предоставляют встроенный механизм для сокращения шаблонного кода и создания того же результата.
6.2. Пример с записями
Вот Книга
, переписанная как Запись
:
public record Book(String title, String author, String isbn) {
}
Используя ключевое слово record
, мы сократили класс Book
до двух строк. Это делает его намного проще и менее подверженным ошибкам.
6.3. Новые дополнения к записям в Java 16
С выпуском Java 16 теперь мы можем определять записи как члены внутренних классов. Это связано с ослаблением ограничений, которые были пропущены в рамках добавочного выпуска Java 15 под JEP-384 :
class OuterClass {
class InnerClass {
Book book = new Book("Title", "author", "isbn");
}
}
7. Сопоставление с образцом для instanceof
(JEP-394)
Сопоставление шаблонов для ключевого слова instanceof
было добавлено в Java 16.
Раньше мы могли бы написать такой код:
Object obj = "TEST";
if (obj instanceof String) {
String t = (String) obj;
// do some logic...
}
Вместо того, чтобы просто сосредоточиться на логике, необходимой для приложения, этот код должен сначала проверить экземпляр obj
, затем привести объект к строке
и присвоить его новой переменной t.
С введением сопоставления с образцом мы можем переписать этот код:
Object obj = "TEST";
if (obj instanceof String t) {
// do some logic
}
Теперь мы можем объявить переменную — в данном случае t —
как часть проверки instanceof .
8. Герметичные классы (JEP-397)
Закрытые классы, впервые представленные в Java 15 , предоставляют механизм для определения того, каким подклассам разрешено расширять или реализовывать родительский класс или интерфейс.
8.1. Пример
Давайте проиллюстрируем это, определив интерфейс и два реализующих класса:
public sealed interface JungleAnimal permits Monkey, Snake {
}
public final class Monkey implements JungleAnimal {
}
public non-sealed class Snake implements JungleAnimal {
}
Запечатанное ключевое
слово используется вместе с разрешающим
ключевым словом, чтобы точно определить, каким классам разрешено реализовывать этот интерфейс. В нашем примере это Обезьяна
и Змея.
Все наследующие классы запечатанного класса должны быть отмечены одним из следующих:
запечатанный
— это означает, что они должны определить, какие классы могут наследоваться от него, используя ключевое словоразрешения
.final
- предотвращение любых дальнейших подклассовнезапечатанный
- позволяет любому классу иметь возможность наследовать от него.
Существенным преимуществом запечатанных классов является то, что они позволяют выполнять исчерпывающую проверку соответствия шаблону без необходимости перехватывать все непокрытые случаи. Например, используя наши определенные классы, мы можем иметь логику для охвата всех возможных подклассов JungleAnimal
:
JungleAnimal j = // some JungleAnimal instance
if (j instanceof Monkey m) {
// do logic
} else if (j instanceof Snake s) {
// do logic
}
Нам не нужен блок else
, так как запечатанные классы допускают только два возможных подтипа Monkey
и Snake
.
8.2. Новые дополнения к запечатанным классам в Java 16
Есть несколько дополнений к запечатанным классам в Java 16. Это изменения, которые Java 16 вводит в запечатанный класс:
- Язык Java распознает
запечатанные
,незапечатанные
иразрешает
как контекстные ключевые слова (аналогичноабстрактным
ирасширяет
) - Ограничить возможность создания локальных классов, являющихся подклассами запечатанного класса (аналогично невозможности создания анонимных классов запечатанных классов).
- Более строгие проверки при приведении запечатанных классов и классов, производных от запечатанных классов.
9. Другие изменения
Продолжая JEP-383 в выпуске Java 15, API внешнего компоновщика предоставляет гибкий способ доступа к собственному коду на хост-компьютере. Первоначально для совместимости с языком C, в будущем он может быть адаптирован к другим языкам, таким как C++ или Fortran. Цель этой функции — в конечном итоге заменить Java Native Interface .
Еще одно важное изменение заключается в том, что внутренние компоненты JDK теперь строго инкапсулированы по умолчанию . Они были доступны с Java 9. Однако теперь для JVM требуется аргумент –illegal-access=permit
. Это повлияет на все библиотеки и приложения (особенно когда речь идет о тестировании), которые в настоящее время напрямую используют внутренние компоненты JDK и просто игнорируют предупреждающие сообщения.
10. Заключение
В этой статье мы рассмотрели некоторые функции и изменения, представленные в рамках добавочного выпуска Java 16. Полный список изменений в Java 16 есть в примечаниях к выпуску JDK .
Как всегда, весь код в этом посте можно найти на GitHub .