JEP 455: Primitive Types in Patterns, instanceof, and switch (Preview)
Primitive Types in Patterns (примитивные типы в шаблонах), instanceof и switch (версия Preview (предварительная версия))
| Ответственный | Angelos Bimpoudis |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 23 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | M |
| Связан с | JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2022/06/15 10:05 |
| Обновлён | 2026/04/02 15:39 |
| Задача | 8288476 |
Аннотация
Улучшить Pattern Matching (сопоставление с образцом), разрешив шаблоны примитивных типов во всех контекстах шаблонов, и расширить instanceof и switch для работы со всеми примитивными типами. Это возможность языка в статусе Preview.
Цели
-
Обеспечить единообразное исследование данных: разрешить шаблоны типов для всех типов, как примитивных, так и ссылочных.
-
Согласовать шаблоны типов с
instanceof, аinstanceof— с безопасным приведением типов. -
Разрешить использовать шаблоны примитивных типов в Pattern Matching как во вложенных контекстах, так и в контекстах верхнего уровня.
-
Предоставить простые в использовании конструкции, которые устраняют риск потери информации из-за небезопасных приведений типов.
-
Вслед за улучшениями
switchв Java 5 (switchпо enum) и Java 7 (switchпо строкам) разрешитьswitchобрабатывать значения любого примитивного типа.
Что не является целью
- Добавление в язык Java новых видов преобразований не является целью.
Мотивация
Ряд ограничений, связанных с примитивными типами, создаёт неудобства при использовании Pattern Matching, instanceof и switch. Если устранить эти ограничения, язык Java станет более единообразным и выразительным.
Pattern Matching для switch не поддерживает шаблоны примитивных типов
Первое ограничение: Pattern Matching для switch (JEP 441) не поддерживает шаблоны примитивных типов, то есть шаблоны типов, в которых указан примитивный тип. Поддерживаются только шаблоны типов, в которых указан ссылочный тип, например case Integer i или case String s. (Начиная с Java 21, для switch также поддерживаются Record Patterns (JEP 440).)
Если бы в switch поддерживались шаблоны примитивных типов, мы могли бы улучшить выражение switch
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
default -> "unknown status: " + x.getStatus();
}
превратив ветку default в ветку case с шаблоном примитивного типа, который делает доступным сопоставленное значение:
switch (x.getStatus()) {
case 0 -> "okay";
case 1 -> "warning";
case 2 -> "error";
case int i -> "unknown status: " + i;
}
Поддержка шаблонов примитивных типов также позволила бы guard-условиям проверять сопоставленное значение:
switch (x.getYearlyFlights()) {
case 0 -> ...;
case 1 -> ...;
case 2 -> issueDiscount();
case int i when i >= 100 -> issueGoldCard();
case int i -> ... appropriate action when i > 2 && i < 100 ...
}
Record Patterns ограниченно поддерживают примитивные типы
Ещё одно ограничение: Record Patterns поддерживают примитивные типы лишь частично. Record Patterns упрощают обработку данных, раскладывая запись на отдельные компоненты. Если компонент — примитивное значение, такой шаблон должен точно указывать тип этого значения. Это неудобно для разработчиков и не согласуется с тем, что в остальной части языка Java есть полезные автоматические преобразования.
Например, пусть нам нужно обрабатывать данные JSON, представленные такими record-классами:
sealed interface JsonValue {
record JsonString(String s) implements JsonValue { }
record JsonNumber(double d) implements JsonValue { }
record JsonObject(Map<String, JsonValue> map) implements JsonValue { }
}
JSON не различает целые и нецелые числа, поэтому JsonNumber для максимальной гибкости представляет число компонентом double. Однако при создании записи JsonNumber нам не нужно передавать double: можно передать int, например 30, и компилятор Java автоматически расширит int до double:
var json = new JsonObject(Map.of("name", new JsonString("John"),
"age", new JsonNumber(30)));
К сожалению, компилятор Java не так услужлив, если мы хотим разложить JsonNumber с помощью Record Patterns. Поскольку JsonNumber объявлен с компонентом double, мы должны разложить JsonNumber относительно double и вручную преобразовать в int:
if (json instanceof JsonObject(var map)
&& map.get("name") instanceof JsonString(String n)
&& map.get("age") instanceof JsonNumber(double a)) {
int age = (int)a; // unavoidable (and potentially lossy!) cast
}
Иными словами, шаблоны примитивных типов можно вкладывать в record-шаблоны, но они инвариантны: примитивный тип в шаблоне должен совпадать с примитивным типом компонента record-класса. Нельзя разобрать JsonNumber через instanceof JsonNumber(int age) так, чтобы компилятор автоматически выполнил сужение компонента double до int.
Причина этого ограничения в том, что сужение может приводить к потерям: значение компонента double во время выполнения может оказаться слишком большим или иметь слишком высокую точность для переменной int. Однако ключевое преимущество Pattern Matching в том, что недопустимые значения отклоняются автоматически — просто за счёт несовпадения. Если компонент double объекта JsonNumber слишком велик или слишком точен, чтобы безопасно сузить его до int, то instanceof JsonNumber(int age) может просто вернуть false, и программа обработает большой компонент double в другой ветке.
Именно так Pattern Matching уже работает для шаблонов ссылочных типов. Например:
record Box(Object o) {}
var b = new Box(...);
if (b instanceof Box(RedBall rb)) ...
else if (b instanceof Box(BlueBall bb)) ...
else ....
Здесь компонент Box объявлен с типом Object, но instanceof можно использовать, чтобы попытаться сопоставить Box с компонентом RedBall или с компонентом BlueBall. Шаблон Box(RedBall rb) совпадает, только если во время выполнения b является Box и его компонент o можно сузить до RedBall; аналогично, Box(BlueBall bb) совпадает, только если его компонент o можно сузить до BlueBall.
В Record Patterns шаблоны примитивных типов должны работать так же гладко, как шаблоны ссылочных типов, и допускать JsonNumber(int age), даже если соответствующий компонент записи имеет числовой примитивный тип, отличный от int. Тогда после сопоставления с шаблоном не понадобятся многословные приведения, которые могут приводить к потерям.
Pattern Matching для instanceof не поддерживает примитивные типы
Ещё одно ограничение: Pattern Matching для instanceof (JEP 394) не поддерживает шаблоны примитивных типов. Поддерживаются только шаблоны типов, в которых указан ссылочный тип. (Начиная с Java 21, для instanceof также поддерживаются Record Patterns.)
Шаблоны примитивных типов были бы так же полезны в instanceof, как и в switch. Назначение instanceof, в широком смысле, — проверить, можно ли безопасно преобразовать значение в заданный тип; именно поэтому instanceof и операции приведения типов всегда встречаются рядом. Для примитивных типов такая проверка критически важна, потому что при преобразовании примитивных значений из одного типа в другой возможна потеря информации.
Например, преобразование значения int в float выполняется оператором присваивания автоматически, хотя оно может приводить к потерям, — и разработчик не получает об этом никакого предупреждения:
int getPopulation() {...}
float pop = getPopulation(); // silent potential loss of information
В то же время преобразование значения int в byte выполняется явным приведением, но приведение может приводить к потерям, поэтому перед ним нужна трудоёмкая проверка диапазона:
if (i >= -128 && i <= 127) {
byte b = (byte)i;
... b ...
}
Шаблоны примитивных типов в instanceof охватили бы преобразования с потерями, встроенные в язык Java, и избавили бы от кропотливых проверок диапазона, которые разработчики пишут вручную уже почти три десятилетия. Иными словами, instanceof мог бы проверять не только типы, но и значения. Два приведённых выше примера можно было бы переписать так:
if (getPopulation() instanceof float pop) {
... pop ...
}
if (i instanceof byte b) {
... b ...
}
Оператор instanceof сочетает удобство оператора присваивания с безопасностью Pattern Matching. Если входное значение (getPopulation() или i) можно безопасно преобразовать в тип из шаблона примитивного типа, то шаблон совпадает и результат преобразования сразу доступен (pop или b). Но если при преобразовании теряется информация, то шаблон не совпадает, и программа должна обработать недопустимое входное значение в другой ветви.
Примитивные типы в instanceof и switch
Если мы снимаем ограничения, касающиеся шаблонов примитивных типов, то было бы полезно снять и связанное с ними ограничение: когда instanceof принимает тип, а не шаблон, он принимает только ссылочный тип, но не примитивный. Принимая примитивный тип, instanceof проверял бы, безопасно ли преобразование, но не выполнял бы его:
if (i instanceof byte) { // value of i fits in a byte
... (byte)i ... // traditional cast required
}
Это улучшение instanceof восстанавливает согласованность семантики instanceof T и instanceof T t, которая была бы утрачена, если бы мы разрешили примитивные типы в одном контексте, но не в другом.
Наконец, было бы полезно снять ограничение, из-за которого switch может принимать значения byte, short, char и int, но не значения boolean, float, double или long.
Switch по значениям boolean был бы полезной альтернативой тернарному условному оператору (?:), потому что boolean switch может содержать не только выражения, но и операторы. Например, следующий код использует boolean switch, чтобы выполнить журналирование при false:
startProcessing(OrderStatus.NEW, switch (user.isLoggedIn()) {
case true -> user.id();
case false -> { log("Unrecognized user"); yield -1; }
});
Если выбор можно будет делать по значениям long, метки case смогут быть константами long, и не придётся обрабатывать очень большие константы отдельными операторами if:
long v = ...;
switch (v) {
case 1L -> ...;
case 2L -> ...;
case 10_000_000_000L -> ...;
case 20_000_000_000L -> ...;
case long x -> ... x ...;
}
Описание
В Java 21 шаблоны примитивных типов разрешены только как вложенные шаблоны в Record Patterns, например:
v instanceof JsonNumber(double a)
Чтобы поддержать более единообразное исследование данных кандидата на сопоставление v с помощью Pattern Matching, мы:
-
Расширим Pattern Matching так, чтобы шаблоны примитивных типов были применимы к более широкому кругу типов кандидатов на сопоставление. Это позволит писать выражения вида
v instanceof JsonNumber(int age). -
Улучшим конструкции
instanceofиswitch, чтобы они поддерживали шаблоны примитивных типов в качестве шаблонов верхнего уровня. -
Дополнительно улучшим конструкцию
instanceof, чтобы при использовании для проверки типа, а не для сопоставления с шаблоном, она могла проверять на соответствие всем типам, а не только ссылочным. Так текущая рольinstanceofкак предусловия безопасного приведения ссылочных типов распространится на все типы.В более широком смысле это означает, что
instanceofможет защищать все преобразования — проверяется ли тип кандидата на сопоставление (например,x instanceof intилиy instanceof String) или сопоставляется его значение (например,x instanceof int iилиy instanceof String s). -
Дополнительно улучшим конструкцию
switch, чтобы она работала со всеми примитивными типами, а не только с частью целочисленных примитивных типов.
Для этого мы меняем небольшое число правил языка Java, регулирующих использование примитивных типов:
-
Снимем запрет на примитивные типы и шаблоны примитивных типов в конструкциях
instanceofиswitch; -
Расширим
switchдля обработки константных ветвей с литералами всех примитивных типов; и -
Определим, когда преобразование одного типа в другой безопасно; для этого нужно знать преобразуемое значение, а также исходный и целевой типы преобразования.
Безопасность преобразований
Преобразование является точным, если при нём не теряется информация. Точно ли преобразование, зависит от пары участвующих типов и от входного значения:
-
Для некоторых пар на этапе компиляции известно, что преобразование из первого типа во второй гарантированно не теряет информацию ни для какого значения. Такое преобразование называется безусловно точным. Для безусловно точного преобразования во время выполнения никаких действий не требуется. Примеры:
byteвint,intвlongиStringвObject. -
Для других пар нужна проверка во время выполнения: можно ли преобразовать значение из первого типа во второй без потери информации или, если бы выполнялось приведение, без выбрасывания исключения. Преобразование точное, если не произошло бы ни потери информации, ни исключения; в противном случае преобразование не точное. Примеры преобразований, которые могут быть точными:
longвintиintвfloat, где потеря точности обнаруживается во время выполнения с помощью числового равенства (==) или эквивалентности представлений соответственно. Преобразование изObjectвStringтоже требует проверки во время выполнения, и оно точное или не точное в зависимости от того, является ли входное значение динамическиString.
Коротко говоря, преобразование между примитивными типами безусловно точное, если это расширение одного целочисленного типа до другого, одного типа с плавающей точкой до другого, byte, short или char до типа с плавающей точкой или int до double. Кроме того, упаковывающие преобразования и расширяющие ссылочные преобразования безусловно точные.
В следующей таблице показаны преобразования, разрешённые между примитивными типами. Безусловно точные преобразования обозначены символом ɛ. Символ ≈ означает тождественное преобразование, ω — расширяющее примитивное преобразование, η — сужающее примитивное преобразование, а ωη — расширяющее и сужающее примитивное преобразование. Символ — означает, что преобразование не допускается.
| В → | byte |
short |
char |
int |
long |
float |
double |
boolean |
|---|---|---|---|---|---|---|---|---|
| Из ↓ | ||||||||
byte |
≈ |
ɛ |
ωη |
ɛ |
ɛ |
ɛ |
ɛ |
— |
short |
η |
≈ |
η |
ɛ |
ɛ |
ɛ |
ɛ |
— |
char |
η |
η |
≈ |
ɛ |
ɛ |
ɛ |
ɛ |
— |
int |
η |
η |
η |
≈ |
ɛ |
ω |
ɛ |
— |
long |
η |
η |
η |
η |
≈ |
ω |
ω |
— |
float |
η |
η |
η |
η |
η |
≈ |
ɛ |
— |
double |
η |
η |
η |
η |
η |
η |
≈ |
— |
boolean |
— |
— |
— |
— |
— |
— |
— |
≈ |
Если сравнить эту таблицу с её аналогом в JLS §5.5, видно, что многие преобразования, разрешённые ω в JLS §5.5, «повышены» до безусловно точных ɛ выше.
instanceof как предварительное условие безопасного приведения
Проверки типов с помощью instanceof традиционно ограничены ссылочными типами. Классический смысл instanceof — проверка предусловия, которая задаёт вопрос: будет ли безопасно и полезно привести это значение к этому типу? Для примитивных типов этот вопрос даже более актуален, чем для ссылочных. Для ссылочных типов, если проверку случайно пропустить, небезопасное приведение, скорее всего, не причинит вреда: будет выброшено ClassCastException, и неправильно приведённое значение нельзя будет использовать. Для примитивных же типов, где нет удобного способа проверить безопасность, небезопасное приведение, скорее всего, приведёт к трудноуловимым ошибкам. Вместо того чтобы выбросить исключение, оно может незаметно потерять такую информацию, как величина, знак или точность, и неправильно приведённое значение попадёт в остальную часть программы.
Чтобы разрешить примитивные типы в операторе проверки типа instanceof, мы снимаем ограничения (JLS §15.20.2), согласно которым тип левого операнда должен быть ссылочным, а правый операнд должен указывать ссылочный тип. Оператор проверки типа принимает вид
InstanceofExpression:
RelationalExpression instanceof Type
...
Во время выполнения мы распространяем instanceof на примитивные типы, опираясь на точные преобразования: если значение в левой части можно преобразовать к типу в правой части с помощью точного преобразования, то привести значение к этому типу было бы безопасно, и instanceof возвращает true.
Вот несколько примеров того, как расширенный instanceof может защитить приведение. Безусловно точные преобразования возвращают true независимо от входного значения; все остальные преобразования требуют проверки во время выполнения, результат которой показан.
byte b = 42;
b instanceof int; // true (unconditionally exact)
int i = 42;
i instanceof byte; // true (exact)
int i = 1000;
i instanceof byte; // false (not exact)
int i = 16_777_217; // 2^24 + 1
i instanceof float; // false (not exact)
i instanceof double; // true (unconditionally exact)
i instanceof Integer; // true (unconditionally exact)
i instanceof Number; // true (unconditionally exact)
float f = 1000.0f;
f instanceof byte; // false
f instanceof int; // true (exact)
f instanceof double; // true (unconditionally exact)
double d = 1000.0d;
d instanceof byte; // false
d instanceof int; // true (exact)
d instanceof float; // true (exact)
Integer ii = 1000;
ii instanceof int; // true (exact)
ii instanceof float; // true (exact)
ii instanceof double; // true (exact)
Integer ii = 16_777_217;
ii instanceof float; // false (not exact)
ii instanceof double; // true (exact)
Мы не добавляем в язык Java новых преобразований, не меняем существующие преобразования и не меняем то, какие преобразования разрешены в существующих контекстах, например при присваивании. Применимость instanceof к данному значению и типу определяется тем, разрешено ли преобразование в контексте приведения и является ли оно точным. Например, b instanceof char никогда не допускается, если b — переменная типа boolean, потому что преобразования приведением из boolean в char не существует.
Шаблоны примитивных типов в instanceof и switch
Шаблон типа объединяет проверку типа с условным преобразованием. Благодаря этому при успешной проверке типа не нужно явное приведение, а неприведённое значение можно обработать в другой ветви, если проверка типа не прошла. Когда оператор проверки типа instanceof поддерживал только ссылочные типы, было естественно разрешать в instanceof и switch только шаблоны ссылочных типов; теперь, когда оператор проверки типа instanceof поддерживает примитивные типы, естественно разрешить шаблоны примитивных типов в instanceof и switch.
Для этого мы снимаем ограничение, согласно которому примитивные типы нельзя использовать в шаблоне типа верхнего уровня. В результате трудоёмкий и подверженный ошибкам код
int i = 1000;
if (i instanceof byte) { // false -- i cannot be converted exactly to byte
byte b = (byte)i; // potentially lossy
... b ...
}
можно записать как
if (i instanceof byte b) {
... b ... // no loss of information
}
потому что i instanceof byte b означает «проверить, выполняется ли i instanceof byte, и если да, привести i к byte и связать это значение с b».
Семантика шаблонов типов определяется тремя предикатами: применимостью, безусловностью и сопоставлением. Мы снимаем ограничения на обработку шаблонов примитивных типов следующим образом:
-
Применимость определяет, допустим ли шаблон на этапе компиляции. Раньше для применимости шаблона примитивного типа требовалось, чтобы входное выражение имело в точности тот же тип, что и тип в шаблоне. Например,
switch (... an int ...) { case double d: ... }не допускался, потому что шаблонdoubleне был применим кint.Теперь шаблон типа
T tприменим к кандидату на сопоставление типаU, еслиUможно привести кTбез предупреждения unchecked. Посколькуintможно привести кdouble, этотswitchтеперь допустим. -
Безусловность означает, что на этапе компиляции известно: применимый шаблон совпадёт со всеми возможными значениями кандидата на сопоставление во время выполнения. Безусловный шаблон не требует проверок во время выполнения.
Поскольку мы делаем шаблоны примитивных типов применимыми к большему числу типов, мы должны указать, для каких типов они безусловны. Шаблон примитивного типа для типа
Tбезусловен для кандидата на сопоставление типаU, если преобразование изUвTбезусловно точное. Дело в том, что безусловно точное преобразование безопасно независимо от входного значения. -
Раньше значение
v, не являющееся ссылкойnull, совпадало с шаблоном типаT, еслиvможно было привести кTбез выбрасыванияClassCastException. Такого определения совпадения было достаточно, пока роль шаблонов примитивных типов была ограничена. Теперь, когда шаблоны примитивных типов можно использовать широко, понятие совпадения обобщается: значение можно точно привести кT, что охватывает как выбрасываниеClassCastException, так и возможную потерю информации.
Исчерпываемость
Выражение switch или оператор switch, метки case которого являются шаблонами, должны быть исчерпывающими: в блоке switch должны обрабатываться все возможные значения выражения-селектора. switch является исчерпывающим, если содержит безусловный шаблон типа. Он может быть исчерпывающим и по другим причинам, например если охватывает все возможные разрешённые подтипы sealed-класса. В некоторых ситуациях switch может считаться исчерпывающим, даже если во время выполнения возможны значения, которые не будут сопоставлены ни с одной case. В таких ситуациях компилятор Java вставляет синтетическую ветвь default для обработки таких непредвиденных входных данных. Исчерпываемость подробнее рассматривается в документе Patterns: Exhaustiveness, Unconditionality, and Remainder.
С появлением шаблонов примитивных типов мы добавляем одно новое правило определения исчерпываемости: если в switch кандидат на сопоставление имеет тип-обёртку W для некоторого примитивного типа P, то шаблон типа T t исчерпывает W, если T безусловно точен на P. В этом случае null становится частью remainder (остатка). В следующем примере кандидат на сопоставление имеет тип-обёртку примитивного типа byte, а преобразование из byte в int безусловно точное. Поэтому следующий switch является исчерпывающим:
Byte b = ...
switch (b) { // exhaustive switch
case int p -> 0;
}
Это поведение аналогично тому, как исчерпываемость определяется для Record Patterns.
Так же как switch использует исчерпываемость шаблонов, чтобы определить, охватывают ли ветви все входные значения, switch использует доминирование, чтобы определить, есть ли ветви, с которыми не будет сопоставлено ни одно входное значение.
Один шаблон доминирует над другим, если с ним сопоставляются все значения, которые сопоставляются с другим шаблоном. Например, шаблон типа Object o доминирует над шаблоном типа String s, потому что всё, что сопоставилось бы с String s, сопоставилось бы и с Object o. В switch метка case с шаблоном типа P без охранного условия не может предшествовать метке case с шаблоном типа Q, если P доминирует над Q. Смысл доминирования не меняется: шаблон типа T t доминирует над шаблоном типа U u, если T t был бы безусловным для кандидата на сопоставление типа U.
Расширенная поддержка примитивных типов в switch
Мы расширяем конструкцию switch, чтобы разрешить выражение-селектор типа long, float, double и boolean, а также соответствующих упакованных типов.
Если выражение-селектор имеет тип long, float, double или boolean, все константы в метках case должны иметь тот же тип, что и выражение-селектор, или соответствующий ему упакованный тип. Например, если выражение-селектор имеет тип float или Float, то все константы case должны быть литералами с плавающей точкой (JLS §3.10.2) типа float. Это ограничение необходимо, потому что несоответствие между константами case и выражением-селектором может привести к преобразованиям с потерями, которые противоречат намерениям программиста. Следующий switch допустим, но он был бы недопустим, если бы константа 0f была случайно записана как 0.
float v = ...
switch (v) {
case 0f -> 5f;
case float x when x == 1f -> 6f + x;
case float x -> 7f + x;
}
Семантика литералов с плавающей точкой в метках case определяется через эквивалентность представлений во время компиляции и во время выполнения. Использование двух литералов с плавающей точкой, эквивалентных по представлению, является ошибкой компиляции. Например, следующий switch недопустим, потому что литерал 0.999999999f округляется вверх до 1.0f, из-за чего появляется повторяющаяся метка case.
float v = ...
switch (v) {
case 1.0f -> ...
case 0.999999999f -> ... // error: duplicate label
default -> ...
}
Поскольку тип boolean имеет только два различных значения, switch, в котором перечислены обе ветви true и false, считается исчерпывающим. Следующий switch допустим, но он был бы недопустим, если бы в нём была ветвь default.
boolean v = ...
switch (v) {
case true -> ...
case false -> ...
// Alternatively: case true, false -> ...
}