JEP 507: Primitive Types in Patterns, instanceof, and switch (Third Preview)
Primitive Types in Patterns (примитивные типы в шаблонах), instanceof и switch (третья версия Preview (предварительная версия))
| Ответственный | Angelos Bimpoudis |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 25 |
| Компонент | specification / language |
| Обсуждение | amber dash dev at openjdk dot org |
| Трудоёмкость | M |
| Длительность | M |
| Связан с | JEP 488: Primitive Types in Patterns, instanceof, and switch (Second Preview) |
| JEP 530: Primitive Types in Patterns, instanceof, and switch (Fourth Preview) | |
| Рецензенты | Alex Buckley, Brian Goetz |
| Одобрен | Brian Goetz |
| Создан | 2025/02/03 12:20 |
| Обновлён | 2025/10/02 17:52 |
| Задача | 8349215 |
Аннотация
Расширить Pattern Matching (сопоставление с образцом): разрешить примитивные типы во всех контекстах шаблонов, а также расширить instanceof и switch для работы со всеми примитивными типами. Это языковая возможность в статусе Preview.
История
Эта возможность была впервые предложена в JEP 455 (JDK 23) и без изменений повторно представлена в статусе Preview в JEP 488 (JDK 24). Здесь мы предлагаем в третий раз представить её в статусе 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 упрощают обработку данных, раскладывая record-объект на отдельные компоненты. Если компонент является примитивным значением, в 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 для максимальной гибкости. Однако при создании record-объекта 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 Patterns. Поскольку любой double можно преобразовать в int, шаблон примитивного типа int a был бы применим к соответствующему компоненту JsonNumber типа double. Тогда и только тогда, когда компонент double можно преобразовать в int без потери информации, instanceof совпадал бы с шаблоном и выполнялась бы ветвь if, в области видимости которой находится локальная переменная a:
if (json instanceof JsonObject(var map)
&& map.get("name") instanceof JsonString(String n)
&& map.get("age") instanceof JsonNumber(int a)) {
... n ...
... a ...
}
Благодаря этому вложенные шаблоны примитивных типов работали бы так же гладко, как вложенные шаблоны ссылочных типов.
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; }
});
switch по значениям 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, которые регулируют использование примитивных типов, и определяя, когда преобразование из одного типа в другой безопасно, — для этого нужно знать как преобразуемое значение, так и исходный и целевой типы преобразования.
Безопасность преобразований
Преобразование является точным, если при нём не происходит потери информации. Будет ли преобразование точным, зависит от пары участвующих типов и от входного значения:
-
Для некоторых пар на этапе компиляции известно, что преобразование из первого типа во второй гарантированно не теряет информацию ни для какого значения. Такое преобразование называется безусловно точным. Для безусловно точного преобразования во время выполнения никаких действий не требуется. Примеры:
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 становится частью остатка. В следующем примере кандидат на сопоставление имеет тип-обёртку примитивного типа 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 определяется через representation equivalence во время компиляции и во время выполнения. Использование двух литералов с плавающей точкой, которые эквивалентны по представлению, — ошибка компиляции. Например, следующий 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 -> ...
}
Дальнейшая работа
Упорядочив правила языка Java, касающиеся сравнения типов и Pattern Matching, мы можем затем рассмотреть введение constant patterns. Сейчас в switch константы могут появляться только как константы ветвей case, например 42 в этом коде:
short s = ...
switch (s) {
case 42 -> ...
case int i -> ...
}
Константы не могут появляться в Record Patterns, и это ограничивает полезность Pattern Matching. Например, следующий switch невозможен:
record Box(short s) {}
Box b = ...
switch (b) {
case Box(42) -> ... // Box(42) is not a valid record pattern
case Box(int i) -> ...
}
Благодаря определённым здесь правилам применимости можно было бы разрешить константам появляться в Record Patterns. В switch запись case Box(42) означала бы case Box(int i) when i == 42, поскольку 42 — литерал типа int.