Современные возможности языка
Records, sealed-типы, паттерн-матчинг в instanceof и switch, record-паттерны, текстовые блоки и вывод типов.
7 вопросов
JuniorТеорияЧастоКак паттерн-матчинг для instanceof убирает приведение типа после проверки?
Как паттерн-матчинг для instanceof убирает приведение типа после проверки?
Паттерн типа связывает переменную прямо в проверке: if (obj instanceof String s) проверяет тип и при успехе присваивает уже приведённое значение в s. Связанная переменная видна там, где условие доказуемо истинно, поэтому отдельное явное приведение и лишняя вторая ссылка на тип оба исчезают.
Типичные ошибки
- ✗Ожидать, что связанная переменная видна даже в ветке, где тест
instanceofпровалился - ✗Писать лишнее явное приведение после паттерна, сводя на нет весь смысл возможности
- ✗Думать, что связанная переменная — свежая копия, тогда как она ссылается на тот же объект в более узком типе
Уточняющие вопросы
- →Как поток-скоуп (flow scoping) определяет, где видна паттерн-переменная
s? - →Можно ли сочетать паттерн типа с условиями
&&в том жеifи почему это работает?
JuniorТеорияЧастоКакой шаблонный код избавляет record и что генерирует компилятор?
Какой шаблонный код избавляет record и что генерирует компилятор?
record — прозрачный носитель неизменяемых данных. Из списка компонентов компилятор генерирует по private final полю и аксессору на компонент, канонический конструктор и основанные на значениях equals, hashCode и toString, все выведенные из этих компонентов. Вы не пишете ни одного, но можете переопределить любой.
Типичные ошибки
- ✗Думать, что поля record изменяемы, тогда как каждый компонент становится
private finalполем без setter - ✗Полагать, что
equals/hashCodeвсё ещё нужно писать самому, тогда как компилятор выводит их из компонентов - ✗Считать, что record может наследовать другой класс, тогда как record неявно
finalи наследует лишьRecord
Уточняющие вопросы
- →Как проверить или нормализовать компонент внутри канонического конструктора record?
- →Почему record неявно
finalи что это значит для наследования?
JuniorТеорияЧастоЧто выводит ромбовидный оператор <> и где допустимо применять var?
Что выводит ромбовидный оператор <> и где допустимо применять var?
Ромбовидный <> позволяет компилятору вывести аргументы обобщённого типа из цели присваивания, поэтому List<String> xs = new ArrayList<>(); не повторяет их. var выводит тип локальной переменной из инициализатора на этапе компиляции, и переменная остаётся статически типизированной. var допустим только для локальной переменной с инициализатором — никогда для поля, параметра или возвращаемого типа.
Типичные ошибки
- ✗Думать, что
varделает переменную динамически типизированной, тогда как её тип фиксируется на компиляции из инициализатора - ✗Пытаться применить
varк полю, к параметру метода или к объявлению без инициализатора - ✗Считать
new ArrayList<>()тем же, что сыройnew ArrayList(), который теряет всю проверку обобщений
Уточняющие вопросы
- →Какой тип выводится для
var xs = new ArrayList<>();и почему это удивляет? - →Почему
varникогда не может стоять у поля или у параметра метода?
MiddleТеорияЧастоКак паттерны типа и record-паттерны в switch заменяют цепочку instanceof?
Как паттерны типа и record-паттерны в switch заменяют цепочку instanceof?
Каждый case может нести паттерн типа (case Circle c ->), который проверяет тип и связывает значение прямо в метке, а страж when его уточняет. Record-паттерн разбирает компоненты record в той же метке (case Point(int x, int y) ->), вкладываясь рекурсивно. По sealed-иерархии компилятор доказывает полноту switch, поэтому default не нужен; null совпадает лишь с явной меткой case null, иначе switch бросает исключение.
Типичные ошибки
- ✗Оставлять ветку
defaultпо sealed-иерархии, которая молча проглатывает вновь добавленный подтип - ✗Забывать, что селектор
nullбросает исключение, если у switch нет явной меткиcase null - ✗Ставить более широкий паттерн типа раньше более узкого, отчего поздняя ветка становится недостижимой
Уточняющие вопросы
- →Почему ветка со стражем
whenне засчитывается в полноту switch? - →Как доминирование паттернов задаёт порядок, в котором должны идти ветки?
JuniorТеорияИногдаЧто такое текстовый блок и как компилятор определяет его отступ?
Что такое текстовый блок и как компилятор определяет его отступ?
Текстовый блок — многострочный строковый литерал, открываемый """ и переводом строки, поэтому вложенные JSON, SQL или HTML сохраняют форму без \n и экранированных кавычек. Компилятор срезает случайный отступ — пробельный префикс, общий для всех содержательных строк и закрывающего разделителя, — и убирает хвостовые пробелы. Остаётся обычный String без затрат во время выполнения.
Типичные ошибки
- ✗Думать, что положение закрывающего
"""не важно, тогда как оно участвует в расчёте случайного отступа - ✗Ожидать от текстового блока подстановки переменных, тогда как это лишь литерал без шаблонного синтаксиса
- ✗Считать, что текстовый блок даёт какой-то особый тип вместо обычного
String
Уточняющие вопросы
- →Как сдвиг закрывающего разделителя влево или вправо меняет получившуюся строку?
- →Что делают внутри текстового блока escape
\sи обратный слэш\в конце строки?
MiddleТеорияИногдаЧто ограничивает sealed и что обязан объявить каждый разрешённый подтип?
Что ограничивает sealed и что обязан объявить каждый разрешённый подтип?
sealed ограничивает, какие типы могут наследовать или реализовывать тип: клауза permits перечисляет их, а каждый названный подтип обязан лежать в том же модуле или пакете и быть объявлен final, sealed или non-sealed. Поскольку полный набор подтипов известен на компиляции, switch по иерархии — обычно по record — проверяется на полноту.
Типичные ошибки
- ✗Забывать, что каждый разрешённый подтип обязан сам быть объявлен
final,sealedилиnon-sealed - ✗Помещать разрешённый подтип в другой модуль, что компилятор отвергает
- ✗Всё равно добавлять ветку
default, выбрасывая ту самую полноту, ради которой тип запечатан
Уточняющие вопросы
- →Что
non-sealedделает с собственной иерархией разрешённого подтипа? - →Почему
switchпо sealed-интерфейсу может обойтись без веткиdefault?
SeniorДизайнИногдаСервис заказов из 400 классов всё ещё написан в идиомах Java 8: его DTO — изменяемые JavaBean с написанными вручную equals/hashCode, а иерархия PaymentEvent разбирается 200-строчной цепочкой if/else instanceof. Команда только что перешла на Java 21 и хочет record, sealed-типы и паттерн-матчинг в switch. Ограничения: DTO сериализуются в публичный API, от которого зависят платящие внешние клиенты; релизный поезд уходит еженедельно и не может быть остановлен ради big-bang переписывания; часть DTO до сих пор мутируется на месте старыми мапперами; а та цепочка if/else — единственное сегодня место, где обрабатывается новый тип события, поэтому забытый подтип проваливается молча. Как вы выстроите порядок миграции, какие классы переведёте первыми и что поставите, чтобы нетронутая точка вызова не сломалась молча?
Сервис заказов из 400 классов всё ещё написан в идиомах Java 8: его DTO — изменяемые JavaBean с написанными вручную equals/hashCode, а иерархия PaymentEvent разбирается 200-строчной цепочкой if/else instanceof. Команда только что перешла на Java 21 и хочет record, sealed-типы и паттерн-матчинг в switch. Ограничения: DTO сериализуются в публичный API, от которого зависят платящие внешние клиенты; релизный поезд уходит еженедельно и не может быть остановлен ради big-bang переписывания; часть DTO до сих пор мутируется на месте старыми мапперами; а та цепочка if/else — единственное сегодня место, где обрабатывается новый тип события, поэтому забытый подтип проваливается молча. Как вы выстроите порядок миграции, какие классы переведёте первыми и что поставите, чтобы нетронутая точка вызова не сломалась молча?
Мигрируйте от листьев, а не big-bang. Переводите в record только уже неизменяемые DTO, а мутируемые оставьте за маппером, пока их вызывающие не перестанут их менять. Запечатайте иерархию PaymentEvent, затем замените цепочку instanceof полным паттерн-switch: необработанный подтип станет ошибкой компиляции, а не молчаливым провалом. Каждый шаг уезжает отдельно, закреплённый тестами контракта, которые стерегут публичный API.
Типичные ошибки
- ✗Переводить в record то DTO, которое старые мапперы всё ещё мутируют на месте, чего record не поддерживает
- ✗Оставлять ветку
defaultв новом паттерн-switch, что снова прячет необработанный подтип, который запечатывание должно было вскрыть - ✗Считать имена компонентов record внутренней деталью, тогда как они часть сериализованного публичного контракта
Уточняющие вопросы
- →Как удержать публикуемую форму JSON неизменной, пока стоящее за ней DTO становится record?
- →Почему добавление ветки
defaultсводит на нет ту полноту, ради которой иерархия и запечатана?