Преуведомление Вся нотация, используемая в этой статье, не является общепринятой для представления математических выражений. Возможно, вы ранее изучали эту тему либо продолжаете изучать, поэтому заранее прошу прощения, если допустил какие-либо фактические ошибки или некорректно использовал термины.    Выпуск Java 21 состоялся 19 сентября 2023 года. В этой версии поддерживаются паттерны записи в switch-блоках и выражениях. Такой синтаксис выглядит монументально (как минимум, по меркам Java). Это водораздел, после которого мы вправе говорить, что в Java полноценно поддерживаются паттерны функционального программирования, подобно тому, как это сделано в Kotlin, Rust или C#. Вот и первый пункт, который пробуждает во мне зависть (я Kotlin-разработчик).  По состоянию на 2023 год язык Java ускоренно развивается на протяжении вот уже 10 лет. Релиз Java 9 был последним из «медленных», так как все последующие релизы выпускались с разницей в 6 месяцев. В следующей таблице показано, как язык Java обновлялся в течение последних 10 лет, подчёркнуты основные синтаксические изменения и дополнения, сделанные в каждой из версий (большинство изменений я опустил, чтобы не отвлекаться от темы). Версия    Java  Дата    релиза Основные    синтаксические нововведения Java 8 18 марта   2014 Лямбда-выражения       Java 9 21   сентября 2017 Модули (проект        Jigsaw)       Java 10 20 марта   2018 var для локальных переменных       Java 11 25   сентября 2018 Без   синтаксических изменений Java 12 19 марта   2019 Switch-выражения (превью 1)       Java 13 17   сентября 2019 Switch-выражения (превью 2)    текстовые блоки (превью 1)       Java 14 17 марта   2020 Switch-выражения (стабильный релиз)    сопоставление с образцом instanceof при помощи if (превью 1)    Текстовые блоки (превью 2)       Java 15 15   сентября 2020 запечатанные классы (превью 1)    сопоставление с образцом instanceof при помощи if (превью 2)    текстовые блоки (стабильный релиз)    записи (превью 2)       Java 16 16 марта 2021 сопоставление с образцом instanceof при помощи if (стабильный релиз)    записи (стабильный релиз)    запечатанные классы (превью 2)       Java 17 14   сентября 2021 Сопоставление switch-выражения с образцом (превью 1)    Запечатанные классы (Стабильный        релиз)       Java 18 22 марта   2022 Сопоставление switch-выражения с образцом (превью 2)       Java 19 20   сентября 2022 Паттерны записей (превью 1)    Сопоставление switch-выражения с образцом (превью 3)       Java 20 21 марта   2023 Паттерны записей (превью 2)    Сопоставление switch-выражения с образцом (превью 4)       Java 21 19   сентября 2023 Паттерны записей (стабильный релиз)    Сопоставление switch-выражения с образцом (стабильный        релиз)       Некоторые из вышеперечисленных релизов стоит рассмотреть отдельно. В Java 14 были стабилизированы switch‑выражения, в Java 16 — записи и сопоставление с образцом для instanceof, в Java 17 — запечатанные классы. Теперь же, в Java 21, стабилизируются паттерны записей и сопоставление switch‑выражения с образцом. Благодаря данному набору изменений в Java появляется возможность выражать такие базовые феномены функционального программирования, к которым ранее в Java было просто не подступиться. В частности, я имею в виду алгебраические типы данных, а также идиоматические способы их использования. Алгебраические типы данных — это концепция, родившаяся в рамках теории типов. Это ответвление теории множеств; теория типов сосредоточена на разрешении таких вопросов как «фрукт ли яблоко?» и прочих причудливых связок, которые учителя математики любят подбрасывать ничего не подозревающим студентам, чтобы у тех голова пошла кругом. В теории типов достаточно много «мяса», но большая часть этого материала неважна в контексте данной статьи. Поэтому не буду объяснять всю теорию типов целиком, а затрону некоторые конкретные разновидности типов, разобраться в которых будет полезно.   Данный тип описывает множество всех значений, являющихся невычислимыми (в смысле полноты по Тьюрингу). В любом нормальном языке программирования это множество обычно является пустым (Ø). Ни один объект нельзя привести к низшему типу, поскольку это пустое множество.  Пример такого типа — это Nothing, применяемый в  Kotlin. Если в программе существует хотя бы один экземпляр Nothing, это считается ошибкой. В Kotlin, чтобы предотвратить создание экземпляров Nothing, соответствующий конструктор нужно сделать приватным. Аналогичный тип в Java называется Void, это класс-обёртка для примитивного типа void. Опять же, создать экземпляр Void невозможно, поскольку его конструктор является приватным. Но Void нельзя считать подлинно низшим типом, так как в переменной Void всё-таки может содержаться null. То есть, чисто технически это единичный тип (подробнее об этом ниже). В таком смысле примитив void подойдёт нам лучше. Нет абсолютно никакой возможности использовать этот тип в качестве переменного, так что у вас даже не может быть пустой переменной void. Данный тип представляет любое значение любого типа — универсальное множество значений U. В Kotlin такой тип называется Any. Причём, может быть соблазнительно счесть высшим тип Object (в Java он аналогичен Any), если, конечно, не учитывать примитивов. Примитивы совершенно обособлены от объектной модели Java и взаимодействуют с объектами довольно нестандартными способами. Именно поэтому в Java, строго говоря, нет такого высшего типа, какой есть в некоторых других языках. Тем временем, C себе в этом не изменяет и просто перегружает void: при помощи void * представляет указатель, который может быть направлен на значение любого типа. Как смешно!  Любой объект может быть приведён к высшему, поскольку в U содержатся все существующие значения.  Более о высшем типе почти нечего добавить, разве что в переменной этого типа может содержаться что угодно. В том числе значение низшего типа. Однако, если возьмётесь искать это значение — что ж, удачи вам. Единичный тип (()) Этот тип имеет всего одно значение. Существует всего один экземпляр с этим значением, и создать второй такой экземпляр невозможно. Технически, именно так устроен примитив void в Java. Когда метод возвращает void, можете считать, что под капотом он неявно возвращает единственный экземпляр типа void (но в виртуальной машине Java тип void обрабатывается не так). Здесь Java отклоняется от нормы, закреплённой в теории, так как void ни при каких условиях не может быть передан в метод как параметр.  Примечание Фактически, Java просто скрещивает низший и единичный тип — так и получается void. Если потребуется, вы можете сымитировать в Java единичный тип. Для этого объявите новый финальный класс, в котором нет никаких полей сверх одного статического значения экземпляра. В таком случае вы сможете обращаться с этим экземпляром как с единичным типом, имеющим единственное значение.  На самом деле, именно так в Kotlin определяется предусмотренный там тип  Unit. Если вы заглянете в определение этого Unit, то увидите, насколько просто он трактуется; это просто object! Kotlin, в отличие от Java, позволяет использовать Unit где угодно, в том числе, передавать методу в качестве параметра. Следовательно, вот так делать можно:  Логический тип Мы снова перебрались на знакомую территорию. У логического (булева) типа есть два допустимых значения, true и false (или вы можете назвать их как-то иначе, как вам удобнее). На самом деле, этот тип можно представить, даже не прибегая к нативному логическому типу того языка, с которым вы работаете. С тем же успехом можно справиться, воспользовавшись таким экземпляром единичного типа, который может принимать null. Если переменная не равна null, то она true, а если равна null, то она false. Естественно, всё это пустая трата времени, и заниматься такими вещами могут только те, кто старается обфусцировать свой исходный код.  Итак, мы рассмотрели простейшие примеры тех «правил», по которым определяются типы в соответствии с теорией типов. Теперь давайте перейдём к сути и обсудим тип-суммы и тип-произведения, а также обсудим, как в Java 21 можно представлять их при помощи записей и запечатанных классов. Тип-произведения Тип-произведения — это составные конструкции, состоящие из двух или более типов. Вообще, тип‑произведение — это список из двух или более типов, сгруппированных воедино. Арность или степень тип‑произведения — это количество типов, входящих в состав этого произведения. Если вы хотели бы посмотреть красивый конкретный пример тип-произведения, то вот вам самая обычная структура из C (struct): В вышеприведённой структуре some_type — это тип‑произведение, в состав которого входят четыре разных типа: int, char *, double и опять int. Обратите внимание: здесь у нас повторяется int. Как нам определить, какой int в каком случае используется, если мы выполняем операции над некоторым some_type? Это может показаться очевидным, но с математической точки зрения это проблема, поскольку приходится самостоятельно с нуля собрать все кирпичики и концепции, которые вы собираетесь использовать! В данном случае у нас уже есть инструментарий, при помощи которого можно всё это запустить в работу. Каждый тип мы ассоциируем с именем, присвоенным структуре (уф). С математической точки зрения тип‑произведение — это не просто список типов, а список упорядоченных пар, где каждая из этих пар состоит из типа и имени, ассоциированного с этим типом.  Например, первое значение some_type можно представить как упорядоченную пару (int, "val1"). Таким образом, невозможно перепутать два компонента int; у них же разные имена! А что насчёт кортежей, таких, как применяются, например, в Python или Rust? Их также можно расценивать как тип-произведения, где «именем» служит индекс данного типа-компонента в составе кортежа. Почему же мы всё равно называем их тип-произведениями? В теории множеств под «произведением» обычно понимается декартово произведение двух множеств. Примечание Декартово произведение двух множеств — это множество упорядоченных пар, представляющих собой все возможные комбинации из всех элементов, входящих в оба множества. Если изложить эту проблему в простых математических терминах, имеем: количество элементов в декартовом произведении C двух множеств A и B — это произведение количества элементов в A и количества элементов в B. Можно воспользоваться нотацией из теории множеств, чтобы выразить произведение двух типов A и B как C = A × B. Данная операция произведения некоммутативна; A × B не идентично B × A. Призадумайтесь об этом ненадолго, и поймёте, почему: ведь здесь приходится менять порядок декларируемых компонентов! В том примере, который я разбирал выше, в качестве компонентов используются всего два типа: A и B. Как нам, например, представить some_type? Для этого нужно сцепить вместе несколько операций произведения, вот так: Множество значений, входящих в состав тип-произведения, можно выразить следующим образом (своё возмущение тем, как я (зло)употребляю математические символы, изложите пожалуйста в комментариях): Можно вот так представить множество всех значений, относящихся к some_type: Отлично, вот и разобрались, что представляют собой типы-произведения. Как всё это связано с Java? Когда вышла версия Java 16, была стабилизирована фича «записи». Классы записей — отличный пример типов‑произведений. Все поля в этих классах финальные, наследовать от них также нельзя. Состояние записи целиком устанавливается в момент её создания и, после того как запись создана, так она и будет выглядеть весь оставшийся период своего существования — примерно 200 миллисекунд. Этим они резко отличаются от нормальных классов, встречающихся в Java повсюду. Работая с ними, можно иметь публичное и приватное состояние, а из-за наследования может возникать и скрытое состояние. О котором вы даже не подозреваете, пока оно не выскочит, как одержимый чёртик из табакерки, чтобы напугать вас странными багами. У вас при этом могут быть изменяемые поля, статические поля, а также всевозможные прочие раздражающие вещи (ну, вы меня поняли).   С обычными типами Java возникает такая проблема: невозможно сделать обобщение, под которое подходили бы все компоненты типов. А если вы хотите эффективно обрабатывать данные, то иметь такое обобщение очень важно. Приходится пробраться сквозь лабиринт потенциально нестандартных геттеров, чтобы хотя бы получить ваши данные, не говоря уж о том, чтобы их переработать. Так сложилось, что в Java никогда не поддерживалась деструктуризация — в отличие от JavaScript или Rust. Но, даже если бы в Java она поддерживалась, в спецификации допускалось бы применять эту возможность только с записями. Давайте попробуем ответить на несколько вопросов, чтобы лучше понять, почему так. Примечание Деструктуризация — это возможность, присутствующая в некоторых языках (самый известный из них — JavaScript), позволяющая вам принять сложное значение, а затем, позвольте мне такой PHP‑изм, EXPLODE его на компоненты, чтобы в итоге получился список совершенно независимых переменных. Подробнее об этой возможности можно почитать здесь. Как же вообще можно деструктурировать обычный класс Java? Во внутреннем состоянии класса Java содержатся все его поля, как публичные, так и приватные. Но кажется, что разрешить извлечение приватных полей при помощи деструктуризации — не лучшая идея. Все мы знаем, как дядюшку Боба бесит нарушение инкапсуляции. Что ж, хорошо, давайте не распространять деструктуризацию на приватное состояние. Что же насчёт публичного состояния? Давайте сначала вот о чём задумаемся: как объекты Java предоставляют публичное состояние? Естественно, поле можно определить как публичное, а, если хочется добиться, чтобы его как попало не меняли — сделайте это поле финальным. Но для этого есть и другой крайне распространённый подход. В большинстве объектов Java все поля делаются приватными, а доступ ко всем полям (как для чтения, так и для записи) разрешается только через специальные методы доступа. К сожалению, в языке нет обязывающих соглашений, которые требовалось бы соблюдать при определении методов доступа. Например, геттер для foo вполне можно назвать getBar, а он всё равно будет работать нормально — разве что запутает любого, кому понадобится обратиться к bar, а не к `foo'. Разумеется, можно воспользоваться фреймворком наподобие Lombok и устранить подобную сложность и неопределённость, всего лишь расставив несколько аннотаций по вашим классам POJO. Но всё равно, как правило, об обычных классах Java очень сложно судить в статике из‑за того, как много «переменных» участвует в определении состояния класса. Подозреваю, в этом и заключается одна из причин, по которым авторы спецификации Java сразу не добавили ко всем классам сопоставление с образцом (В JEP-441 это нововведение упомянуто среди планов на будущее).  Чтобы справиться с этой проблемой, в язык были добавлены записи — совершенно иная иерархия классов. Подобный прецедент уже был ранее, когда в Java 5 ввели перечисления, наследующие от java.lang.Enum. Аналогично, все записи наследуют от java.lang.Record. Итак, что же такое можно сделать при помощи записей, что не делается при помощи обычных классов? При работе с записями эта проблема решается так: вводятся ограничения на определения записей, а вдобавок жёстко задаётся тот набор свойств, которые они могут иметь. А именно: Записи неявно являются финальными классами, и от них нельзя наследовать. Больше никаких недопустимых дочерних классов, которые могли бы быть прижиты от случайной связи со сторонней библиотекой. Сами записи не могут наследовать ни один класс кроме java.lang.Record. Так устраняются подводные камни, которые могли бы возникнуть, если унаследованное состояние загрязнит код записи. У компонентов записи не может быть никаких модификаторов видимости. Любые компоненты записей всегда финальные и неизменяемые. При этом правило неизменяемости не распространяется на любой компонент записи. Только ссылки из компонента записи должны быть неизменяемыми. Когда вы объявляете запись и не определяете для неё геттеров, методы-геттеры в дальнейшем будут определяться при помощи очень специфичного синтаксиса. Этот синтаксис очень строг; в качестве имени геттера Java использует только имя поля. Геттер для поля a будет a(). Java воспользуется вашим определением, если вы вручную определите геттер, соответствующий соглашениям об именовании. В противном случае Java автоматически определит геттер, который полностью соответствует всем соглашениям. Нестандартный геттер большой разницы не сделает. Поля, соответствующие компонентам записи, всегда неявно приватные, доступ к ним возможен только через геттеры. (Есть ещё некоторые детали, но, пожалуй, остановлюсь на уже сказанном.) Благодаря тому, что записи обладают этими свойствами, гарантируется: любая новая фича Java, использующая записи (например, сопоставление с образцом) непременно будет работать. Ведь в самой спецификации языка гарантируются и все варианты поведения, и структура записей. Красота. Расскажите, как я смогу с этим работать. Сопоставление с образцом. Продолжайте… Довольно обременительно писать глубоко вложенный код, когда приходится учитывать множество условий, зависящих от типов ваших данных. Эта проблема станет просматриваться особенно явно, когда ниже в статье мы поговорим о тип-суммах. Сопоставление с образцом — это способ статически (то есть во время компиляции, по мере написания кода) убедиться, что определённые паттерны действительно присутствуют в обрабатываемых вами данных. Рассмотрим нижеприведённый пример. Обратите внимание, что в A содержится экземпляр Record, и вообще там может быть любой тип записи. Сначала попробуем вывести в консоль содержимое r, воспользовавшись обычными для Java if-операторами, а потом попытаемся добиться того же, сопоставляя switch-выражения с образцом. Блок switch структурно явно более удобен, чем приведённая выше лесенка if-else. Switch-паттерны обладают большим потенциалом, когда требуется быстро и легко извлечь глубоко вложенные данные, не возясь с проверками instanceof и трудоёмкими приведениями типов. Если вам когда-либо доведётся поработать с Java 21 (да повезёт вам с начальством, которое не чурается новых версий Java) — уверен, вы по достоинству оцените эту фичу. Если вы хотели бы попробовать эти возможности самостоятельно, то для начала установите Java 21. Для этого в AUR есть удобный пакет jdk21-jetbrains-bin, если хотите управиться максимально быстро. Я, к слову, пользуюсь Arch. Скопируйте этот код в main.java и выполните его командой: Здесь также во всей красе предстаёт ещё одна новая фича, которая сейчас в состоянии превью: безымянные main-методы. В предыдущем примере нам удалось при помощи сопоставления с образцом переключаться между разными типами записей. Теперь давайте ненадолго отложим эту тему и поговорим о том, как в Java управлять операциями выбора. Мы выбираем… Что же делать, если вам требуется выбирать из ограниченного набора альтернативных вариантов? Например, если работаете с перечислениями в Java: они представляют собой группы статических вариантов, и вы не можете менять данные, содержащиеся внутри них. В вышеприведённом перечислении определяется три цвета: красный, зелёный и синий, причём устанавливаются значения для разных полей. Цветовые значения в них изменить невозможно, если вы не хотите учинить беспорядок буквально в каждой точке, где используется это перечисление (в рассмотренном коде значения финальные, а представьте, что бы было, не будь они финальными). Теперь переформулируем задачу. Вам нужны различные представления цветов — например, в системах RGB, HSL и CMYK. Может быть, просто сделать перечисление для этой цели?  Так у нас получается красивое ограниченное множество значений, из которых мы можем выбирать. Но работать c ним неудобно: если вам требуется иметь по несколько цветовых значений для разных представлений, то придётся отдельно хранить данные о конкретных цветах, а также держать под рукой значение перечисления ColorRepresentation, чтобы понимать, что именно сейчас происходит… Очевидно, что любой, кто разбирается в Java, НЕ СТАЛ бы так проектировать класс Color. Гораздо лучше было бы реализовать множественные представления цвета при помощи полиморфизма — не жертвуя удобочитаемостью!  Теперь, если у вас есть экземпляр Color, вам всего лишь нужно проверить, является ли он instanceof того представления цвета, которое вам требуется. Всё, после этого вы сможете обращаться за данными из этого представления. Но в этой реализации есть изъян. Как мы ограничим место цвета в иерархии наших классов. Любой пользователь нашей библиотеки мог бы создать новый класс RYB, наследующий от Color или, например, от RGB. Здесь возникает проблема, если ваша библиотека не рассчитана на существование каких-либо новых вариантов Color или если в ней не предусмотрено, что поведение конкретных вариантов Color может измениться. Если только API сразу не проектировался с расчётом на расширяемость, то при создании новых представлений вы в лучшем случае отделаетесь отказами (тогда у вас хотя бы будет шанс узнать, что пошло не так). В худшем случае вы получите малозаметные баги, которые затронут код очень далеко от фактического источника проблемы. Чтобы исправить ситуацию, можно сделать следующие вещи: Сделать все варианты final. Притом, что это поможет, такой ход не исключает создания новых вариантов непосредственно от Color, ведь сам Color невозможно сделать финальным. Предусмотреть для всей внутренней логики обязательный применяемый по умолчанию при возникновении нераспознанных вариантов. Так расширяемость библиотеки ограничится, но, если такая расширяемость не наша главная цель, то данный ход будет полезен.  Но такой подход также гораздо сильнее чреват ошибками. Если хотя бы в одном элементе логики не будет учтён данный неблагоприятный случай, то возникнут проблемы. Либо можно было бы воспользоваться запечатанными классами и убить сразу двух      зайцев. Тип-суммы Появившиеся в Java 17 запечатанные классы позволяют применять паттерны проектирования, основанные на тип-суммах. Тогда как диапазон значений у тип-произведения получается путём перемножения значений тех типов, из которых оно состоит, диапазон значений тип-суммы получается путём суммирования.  Что ж, из названия это очевидно… но что понимается под суммированием диапазона значений?  В тип-суммах заложено, что тип может быть любой из составляющих тип-суммы в каждый момент времени. Такие типы также называются мечеными объединениями, так как в теории типов обычно считается, что диапазон значений такого типа представляет собой объединённое множество его компонентов, причём, каждый тип-компонент в таком множестве «помечен».  Вот как можно выразить тип-сумму, воспользовавшись моей нотацией, которая якобы соответствует теории типов: Множество значений, содержащихся в T, можно выразить таким логическим предикатом: Возможно, всё это напоминает вам объединения из C. MyUnion состоит из трёх типов-компонентов, а C позволяет трактовать значение MyUnion как контейнер для любого из трёх этих типов: Обратите внимание: значение объединения затирается при втором присваивании doubleValue. Если бы вы хотели вывести myUnion.intValue после второго присваивания, то увидели бы просто тарабарщину. На самом деле, это просто байты doubleValue, разрезанного пополам и интерпретированного как целое число. Здесь мы сталкиваемся с самым серьёзным изъяном, присущим объединениям C; отсутствует встроенный механизм, который позволил бы (не обладая дополнительной информацией) узнать, каково именно значение объединения. Следовательно, чтобы это определить, нам требуется внешний дискриминант. Предусмотренный в Java полиморфизм позволяет решить эту задачу при помощи instanceof. Но иерархия классов Java слишком открыта; не существует способа как-либо ограничить количество вариантов в «объединении» Java. Нам в данном случае нужны меченые объединения, и именно их удобно представлять при помощи запечатанных типов. Модификатор sealed нужен для того, чтобы ясно указать: невозможно расширить запечатанный класс какими-либо иными классами сверх тех, которым разрешено от него наследовать. Этот механизм позволяет разработчику контролировать, как именно пользователь взаимодействует с API его библиотеки. Обратите внимание на синтаксис запечатанного класса Color. Здесь есть модификатор sealed и условие permits, затрагивающее имена всех подклассов Color. При помощи permits мы указываем, какие классы могут наследовать от конкретного класса. Так мы предотвращаем любое нежелательное наследование. Также обратите внимание, что каждая из реализаций Color помечена как final, так что у вас в распоряжении будут только те четыре цветовых представления, которые показаны здесь. Собственного цветового представления вы сделать не сможете. Таким образом, это закрытая иерархия классов. Никакие новые классы не могут наследовать от Color, независимо от того, находятся ли они в одном с ним пакете или нет.  Если ваша иерархия начинается с запечатанного класса, то все наследники (прямые или косвенные) вы должны пометить как sealed, non-sealed или final. Если у наследующего класса нет этих модификаторов, то возникнет ошибка компиляции.  Вот что означает каждый из этих модификаторов: sealed — от этого класса нельзя наследовать, если только имя наследника не указано после permits. non-sealed — от этого класса можно наследовать как обычно. Это удобно, если нужно контролировать область действия специализированных поведений. final — этот класс — «лист» в дереве наследования; более наращивать его нельзя. Эти модификаторы подходят для точного контроля, как именно могут себя вести классы некоторого API, и как они используются. Так удаётся избежать ситуаций, в которых единственной страховкой, не позволяющей всей системе взорваться, были бы обязательные правила — и договорённость между разработчиками соблюдать эти правила во имя добра. Предположим, мы пишем код, который обрабатывал бы цвета в зависимости от их формата. Мы уже выявили, как можно красиво ограничить и обойтись без сюрпризов. Но как нам структурировать код, который обрабатывал бы экземпляры запечатанных классов? В ранних версиях Java у нас практически бы не было выбора: лучшее, что мы могли бы соорудить — это лесенка из if-else: Можно было бы управиться лучше, работая с Java 16+ и применяя сопоставление с образцом if: Но как бы было здорово просто переключаться на цветовой переменной, одновременно с этим извлекая её содержимое — так, как это делается в Rust? Вот, например, как такая ситуация могла бы решаться в Rust: Обратите внимание: эти цветовые значения деструктурируются выше в блоке match. Как можно было бы сделать то же самое при помощи запечатанных классов? Сопоставление switch-выражения с образцом и одновременное применение деструктуризации работает только с записями, а записи не могут наследовать ни от одного класса кроме Record… Решение — просто воспользоваться запечатанными интерфейсами. Функционально они аналогичны запечатанным классам, но с той оговоркой, что реализовывать их могут даже записи и перечисления. Вот здесь можно красиво применить сопоставление с образцом и извлечь данные из экземпляра Color — и это хорошо, если всё, что вам требуется — вынуть данные из класса, не вызывая в нём каких-либо методов: Примечание В Java 21 теперь есть возможность отловить случай null внутри блоков switch и выражений, поэтому не требуется предварительно проверять на null перед тем, как перейти к переключателю. Как вы также можете заметить, здесь не используется случай, заданный по умолчанию. Как правило, Java в таком случае выбросил бы ошибку, указывающую, что не все возможные случаи учтены. Но, поскольку Color — это запечатанный класс, Java может считать, что все случаи обработаны. Стража! Стража! А я уже упомянул о граничных операторах (guard clauses)? Граничные операторы! С их помощью можно подключать дополнительные условия к switch-веткам, и, чтобы ветка была выполнена, эти условия должны выполняться. При помощи граничных операторов можно лаконично постулировать более сложные условия, заложенные внутри switch-инструкций и выражений. Больше никаких глубоко вложенных if-условий в ваших переключателях. Рассмотрим ситуацию, в которой обрабатываются специальные случаи RGB-цветов, где red > 200 является true. Поскольку нам приходится вставить это условие в if-оператор в теле соответствующего случая: Это не самый уродливый случай, но он всё равно означает, что в долгосрочной перспективе глубина вложенности кода постепенно растёт. Как правило, проще парсить код, ветвящийся по вертикали, а не по горизонтали, поскольку в первом случае приходится отслеживать меньше областей видимости. В Java 21 можно встроить это условие в метку альтернативы (case label); это делается при помощи ключевого слова when. Примечание Java так или иначе будет сначала жадно проверять, какие случаи результируют в true, чтобы обязательно первым делом обработать более конкретные случаи (ограниченные или нет), вслед за которыми идут менее специфичные. Уф, исключения (с примером из JEP 441) Теперь нам приходится иметь дело с новым классом исключений. А именно java.lang.MatchException. Что происходит, если сопоставление с образцом не пройдёт? Рассмотрим случай, когда плохо реализован геттер записи: Вышеприведённый switch-блок выбросит исключение MatchException, так как при вызове геттера i выдаётся исключение ArithmeticException. Как указано в JEP 441: (Метод доступа к записи, который всегда выбрасывает исключение, очень необычен¸ равно как и исчерпывающий переключатель образцов, выбрасывающий исключение MatchException.) Исчерпывающие switch-блоки будут выбрасывать исключение, если ни один из указанных вариантов не совпадёт с селектором. Об этом в JEP сказано: (Исчерпывающий переключатель для некоторого перечисления не найдёт совпадения только в том случае, если класс перечисления изменился уже после того, как переключатель скомпилировался, а это крайне необычно.) Исключение MatchException будет выброшено, если необходимость в нём возникает при выполнении граничного оператора. Приведённый здесь пример очень характерен: даже статически здесь сразу понятно, что происходит ошибка, связанная с делением на ноль. Но ситуация становится гораздо более мутной, если делитель подбирается динамически, и его значение может составить 0. Заключение В этой статье были рассмотрены некоторые новые вещи, которые можно сделать в Java 21 (некоторые вопросы я не затронул, например, мы не обсудили, как дженерики взаимодействуют со switch-паттернами).