Источник: FreeCodeCamp
Эта публикация посвящена вопросу улучшения качества кода Java с помощью сопоставления с образцом и запечатанными классами.
Pattern Matching (Сопоставление с образцом) позволяет писать более лаконичный и читаемый код Java при работе со сложными структурами данных. Также оно упрощает извлечение данных из структур данных и выполнение операций над ними.
Что такое сопоставление с образцом в Java?
Pattern Matching сопоставляет значение с шаблоном, который включает переменные и условия. Если значение соответствует шаблону, соответствующие части значения привязываются к переменным в шаблоне. Это позволяет создавать более читаемый и интуитивно понятный код. Существует два типа сопоставления с образцом: традиционный и современный. Давайте посмотрим на различия между ними.Традиционное сопоставление с образцом
При традиционном сопоставлении с образцом оператор switch расширяется для поддержки сопоставления с образцом путем добавления ключевого слова case с аргументом шаблона. Оператор switch может сопоставляться с примитивным типом, оболочками, перечислениями и строками. Пример кода:
private static void printGreetingBasedOnInput(String input){
switch (input){
case "hello":
System.out.println("Hi There");
break;
case "goodbye":
System.out.println("See you Later!");
break;
case "thank you":
System.out.println("You are welcome");
break;
default:
System.out.println("I don't understand");
break;
}
}
Здесь метод printGreetingBasedOnInput принимает строку input и печатает соответствующее приветственное сообщение на основе ее значения с помощью операторов switch-case. Он охватывает случаи “hello” “goodbye” и “thank you”, предоставляет соответствующие ответы и по умолчанию использует “I don't understand” (Я не понимаю) для любых других входных данных.
Современное сопоставление с образцом
В современном сопоставлении с образцом оператор switch может сопоставляться с различными шаблонами, такими как объекты любого типа, перечисления или примитивы. Ключевое слово case используется для указания шаблона для сопоставления.
private static void printGreetingBasedOnInput(String input){
switch (input){
case "hello" -> System.out.println("Hi There");
case "goodbye" -> System.out.println("See you Later!");
case "thank you" -> System.out.println("You are welcome");
default -> System.out.println("I don't understand");
}
}
В этом фрагменте используется более краткий синтаксис. Это упрощает код, напрямую указывая действие, которое необходимо выполнить для каждой метки case.
До Java 16 нам нужно было проверять тип объекта, а затем явно приводить его к переменной. Расширенный оператор instanceof, представленный в Java 16, может как проверять тип, так и выполнять неявное приведение к переменной, как в примере ниже:
private static void printType(Object input){
switch (input) {
case Integer i -> System.out.println("Integer");
case String s -> System.out.println("String!");
default -> System.out.println("I don't understand");
}
}
Улучшение instanceof становится особенно полезным при работе с защитой шаблонов (pattern guards). Защита шаблонов — это способ сделать операторы case в сопоставлении шаблонов Java более конкретными за счет включения логических выражений (boolean expressions).
Это обеспечивает более точный контроль над сопоставлением шаблонов и может сделать ваш код более читабельным и выразительным.
private static void printType(Object input){
switch (input) {
case Integer i && i > 10 -> System.out.println("Integer is greater than 10");
case String s && !s.isEmpty()-> System.out.println("String!");
default -> System.out.println("Invalid Input");
}
}
На основе приведенных выше примеров вы можете увидеть, что сопоставление шаблонов Java предоставляет различные преимущества:- Оно улучшает читаемость кода, позволяя эффективно сопоставлять значения с шаблонами и извлекать данные.
- Оно уменьшает дублирования кода, обрабатывая различные случаи (cases) с помощью одного фрагмента кода.
- Оно повышает безопасность типов, позволяя сопоставлять значения с конкретными типами.
- Защитники шаблонов (Pattern guards) могут использоваться в case для дальнейшего улучшения читаемости и удобства сопровождения кода.
Что такое запечатанные классы в Java?
Запечатанные классы (Sealed Classes) позволяют разработчикам ограничивать набор классов, которые могут расширять или реализовывать данный класс или интерфейс. С помощью запечатанных классов можно создать иерархию классов или интерфейсов, которую затем можно расширить или реализовать только с помощью определенного набора классов. Например:
public sealed class Result permits Success, Failure {
protected String response;
public String message(){
return response;
}
}
public final class Success extends Result {
@Override
public String message() {
return "Success!";
}
}
public final class Failure extends Result {
@Override
public String message() {
return "Failure!";
}
}
В этом примере мы определили запечатанный класс Result, который можно расширить с помощью классов Success или Failure.
Любой другой класс, который попытается расширить Result, приведет к ошибке компиляции. Это дает нам возможность ограничить набор классов, которые можно использовать для расширения Result, делая код более удобным в сопровождении и расширяемым.
Несколько важных моментов, которые стоит учесть:
- Если подкласс хочет быть разрешенным подклассом запечатанного класса в Java, он должен быть определен в том же пакете, что и запечатанный класс. Если подкласс не определен в том же пакете, то произойдет ошибка компиляции.
- Если подклассу разрешено расширять запечатанный класс в Java, он должен иметь один из трех модификаторов: final, sealed (запечатанный) или non-sealed (незапечатанный).
- Запечатанный подкласс должен определять тот же или более ограничительный набор разрешенных подклассов, что и его запечатанный суперкласс. Запечатанные подклассы должны быть либо final, либо sealed. Незапечатанные (non-sealed) подклассы не допускаются в качестве разрешенных подклассов запечатанного. суперкласса, и все разрешенные подклассы должны принадлежать к тому же пакету, что и запечатанный суперкласс.
Как объединить сопоставление шаблонов Java и запечатанные классы
Вы можете использовать запечатанные классы и их разрешенные подклассы в операторах switch с сопоставлением шаблонов. Это может сделать ваш код более кратким и легким для чтения. Взгляните на пример:
private static String checkResult(Result result){
return switch (result) {
case Success s -> s.message();
case Failure f -> f.message();
default -> throw new IllegalArgumentException("Unexpected Input: " + result);
};
}
В случае запечатанных классов компилятору требуется ветвь по умолчанию (default branch) для сопоставления с образцом, чтобы обеспечить охват всех возможных случаев.
Поскольку запечатанные классы имеют фиксированный набор разрешенных подклассов, все случаи можно охватить с помощью конечного числа операторов case.
Если ветвь по умолчанию не включена, в будущем в иерархию можно добавить новый подкласс, который не будет охватываться существующими операторами case. Это приведет к ошибке выполнения, которую будет сложно отладить.
Требуя ветвь по умолчанию, компилятор гарантирует, что код является полным и охватывает все возможные случаи, даже если в будущем к иерархии запечатанных классов будут добавлены новые подклассы.
Это помогает предотвратить ошибки во время выполнения и делает код более надежным и удобным в сопровождении.
Если мы изменим класс Result, включив в него новый подкласс Pending, и мы не включили его в сопоставление с образцом, он будет включен в ветвь по умолчанию.
Что такое запечатанный интерфейс в Java?
При работе с запечатанным интерфейсом в Java компилятору не потребуется ветка по умолчанию для сопоставления с образцом, если охвачены все случаи. В случае отсутствия ветки компилятору потребуется ветка по умолчанию, чтобы гарантировать обработку всех возможных случаев. Помните, что при работе с запечатанными классами нам всегда необходимо включать ветку по умолчанию. Вот пример кода:
public sealed interface OtherResult permits Pending, Timeout {
void message();
}
public final class Pending implements OtherResult{
@Override
public void message() {
System.out.println("Pending!");
}
}
public final class Timeout implements OtherResult{
@Override
public void message() {
System.out.println("Timeout!");
}
}
private static void checkResult(OtherResult result){
switch (result) {
case Pending p -> p.message();
case Timeout t -> t.message();
};
}
Заключение
Вот несколько ключевых выводов об использовании сопоставления с образцом и запечатанных классов в коде Java:- Улучшенная читаемость: сопоставление с образцом и запечатанные классы могут сделать ваш код более выразительным и легким для чтения, поскольку они обеспечивают более краткий и интуитивно понятный синтаксис.
- Лучший контроль над иерархиями классов. Запечатанные классы позволяют iконтролировать иерархии классов и гарантировать возможность использования только разрешенных подклассов. Это может улучшить безопасность и удобство сопровождения кода.
- Неявная безопасность типов. Сопоставление с образцом и запечатанные классы обеспечивают неявную безопасность типов, что может снизить риск ошибок во время выполнения и упростить поддержку кода.
- Уменьшение дублирования кода. Сопоставление с образцом и запечатанные классы могут уменьшить дублирование кода, позволяя обрабатывать различные случаи в одном фрагменте кода.
- Лучшая организация кода. Запечатанные классы могут помочь организовать код и уменьшить сложность иерархии классов за счет группировки связанных классов.
- Улучшение удобства сопровождения. Сопоставление с образцом и запечатанные классы могут улучшить удобство сопровождения кода, упрощая его понимание и обновление, что может сэкономить время и усилия в долгосрочной перспективе.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ