Когда будет вызываться случай по умолчанию в переключателе для закрытой иерархии
Учитываяsealed
иерархия, как показано ниже
sealed interface Shape permits Rectangle, Square
record Rectangle() implements Shape
record Square() implements Shape
СRectangle
&Square
являются записями, это по своей сути делает всю иерархию нерасширяемой, т. е. больше не допускается создание подклассов.
Начиная с JDK 21, сопоставление шаблонов для принудительного переключения должно быть исчерпывающим, охватывая все возможныеcase
или предоставив чехол для покрытия оставшегося.
Таким образом, ниже при каких обстоятельствахdefault
case будет выполнен, поскольку охвачены все возможные комбинации, и почему это вообще разрешено?
switch (shape) {
case Rectangle r -> // do something with r;
case Square sq -> // do something with sq;
case null -> // shape could be null
default -> // WHY is this permitted when all possible cases are covered already??
}
PS : Запечатанная иерархия определенно может развиваться, но когда это произойдет, компилятор автоматически пометитswitch
обновить себя тоже.
2 ответа
Ответы на данный момент в основном правильные, но это еще не все.
Простой ответ, данный до сих пор, заключается в том, что могут быть значения времени выполнения, которые не соответствуют ни одному регистру, независимо от проверки типа во время компиляции на полноту, - верен. Класс по умолчанию разрешен, поскольку он может быть выбран (и если вы его не укажете, компилятор предоставит вам синтетический класс, который выдаст MatchException).
Есть две основные причины, по которым переключатель, который является исчерпывающим во время компиляции, может не быть действительно исчерпывающим во время выполнения: отдельная компиляция и остаток.
В других ответах отдельная компиляция рассматривалась адекватно; Новые константы перечисления и новые подтипы запечатанных типов могут появиться во время выполнения, поскольку можно перекомпилировать иерархию без перекомпиляции переключателей над ней. Обычно компилятор передает его вам молча (нет смысла заставлять вас объявлятьdefault
пункт, который просто выдает «не могу сюда попасть»), но вы можете справиться с этим сами, если хотите.
Вторая причина — остаток , который отражает тот факт, что разумное значение слов «достаточно исчерпывающий» и фактическая полнота не полностью совпадают, и если бы мы потребовали, чтобы переключатели были действительно исчерпывающими, программировать было бы очень неинтересно.
Простой пример:
Box<Box<String>> bbs = ...
switch (bbs) {
case Box(Box(String s)): ...
}
Должен ли этот переключатель быть исчерпывающим?Box<Box<String>>
? Как оказалось, есть одно возможное значение во время выполнения, которому оно не соответствует: . (Напомним, что вложенные шаблоны соответствуют внешнему шаблону, а затем используйте привязки внешнего шаблона в качестве кандидатов на соответствие внутреннему шаблону, а шаблон записи не может соответствовать нулю, поскольку он хочет вызвать методы доступа компонента.)
Мы могли бы потребовать, чтобы в целях полноты существовал отдельный случай обработки ошибок дляBox(null)
, но это никому не понравится, а в менее тривиальных примерах обработка ошибок затмит полезные случаи. Таким образом, Java делает прагматичный выбор, определяя полноту переключателей в «человеческих» терминах — код, который кажется исчерпывающим для разумных классов — и позволяя обрабатывать «глупые» случаи с помощью синтетического значения по умолчанию. (Вы по-прежнему можете явно обрабатывать любые глупые случаи, если хотите.) Этот переключатель считается исчерпывающим, но с непустым остатком.
Вся эта концепция более подробно объяснена в разделе «Шаблоны: исчерпываемость, безусловность и остаток».
Запечатанная иерархия определенно может развиваться, но когда это произойдет, компилятор автоматически пометит переключатель для обновления.
Это неправда. Кажется, вы предполагаете, что и оператор переключения, и интерфейсы записей/запечатанных интерфейсов всегда будут компилироваться вместе. Конечно, это, вероятно, будет верно в большинстве практических ситуаций, но это не всегда так. Вы можете скомпилировать запечатанные интерфейсы и записи без перекомпиляции оператора переключения.
Например, предположим, что все типы находятся в отдельных файлах .java, названных по их именам, поэтому Shape.java, Rectangle.java, Square.java, а оператор переключения находится вmain
метод в Main.java.
Сначала я компилирую все имеющиеся у меня исходные файлы Java и создаю файлы .class для каждого файла .java.
Затем предположим, что я изменю Shape.java на:
sealed interface Shape permits Rectangle, Square, Triangle
и добавил файл Triangle.java.
После этого я буду компилировать только записи и запечатанный интерфейс, не компилируя Main.java. Это возможно, поскольку они не зависят от Main.java.
Наконец, я бегуjava Main
. Это запустит файл Main.class, который не знает о новом классе, поскольку он был скомпилирован до того, как я добавилTriangle
.
Здесь будет выполнена ветка, или, если у вас нетdefault
ветка, сюда будет брошено.
(Обратите внимание, чтоMatchException
часть применяется только к выражениям переключения и расширенным операторам переключения , например, в вашем вопросе. Нерасширенные операторы переключения просто ничего не делают, если ни один регистр не соответствует.)
См. также «Выполнение оператора переключателя» и «Оценка выражений переключателя во время выполнения» в спецификации языка Java.