JavaRush /Java блог /Random /Кофе-брейк #204. Запечатанные классы (Sealed Classes) в J...

Кофе-брейк #204. Запечатанные классы (Sealed Classes) в Java 17. Что такое сопоставление с образцом и как оно улучшает читаемость кода Java

Статья из группы Random

Запечатанные классы (Sealed Classes) в Java 17

Источник: Medium Эта публикация поможет вам изучить синтаксис и преимущества Sealed Classes в Java 17. Также вы узнаете, как эти классы работают на практических примерах и как их можно использовать для написания более безопасного и удобного в сопровождении кода. Кофе-брейк #204. Запечатанные классы (Sealed Classes) в Java 17. Что такое сопоставление с образцом и как оно улучшает читаемость кода Java - 1В релизе Java 17 компания Oracle представила новую функцию под названием Sealed Classes (запечатанные классы). По идее, она предназначена для повышения безопасности и гибкости приложений Java. Запечатанные классы позволяют разработчикам ограничивать иерархию наследования класса или интерфейса. Иными словами, они обеспечивают больший контроль над тем, как класс может быть расширен или реализован. Чтобы лучше все это понять, давайте рассмотрим пример класса Shape, который может быть расширен классами Circle, Square или Triangle. С помощью запечатанных классов мы можем указать, что только эти три класса могут наследовать от Shape, и никакой другой класс не может его расширять. Синтаксис для определения запечатанного класса:

public sealed class Shape permits Circle, Square, Triangle {
    // Определение класса
}
В этом примере мы определили запечатанный класс Shape, который позволяет расширять его только Circle, Square и Triangle.

Преимущества запечатанных классов

Запечатанные классы предоставляют несколько преимуществ, в том числе:
  1. Повышение безопасности: с помощью запечатанных классов разработчики могут ограничивать иерархию наследования класса или интерфейса, предотвращая несанкционированный доступ к конфиденциальному коду.
  2. Лучшая ремонтопригодность. Ограничивая иерархию наследования, запечатанные классы способствуют ремонтопригодности кода, упрощая понимание и изменение кодовой базы.
  3. Улучшенная производительность: запечатанные классы позволяют JVM выполнять дополнительные оптимизации во время выполнения, что приводит к повышению производительности.

Примеры кода

Давайте рассмотрим несколько примеров кода, чтобы лучше понять, как работают запечатанные классы. Пример 1: Определение запечатанного интерфейса.

public sealed interface Vehicle permits Car, Bike {
    void start();
}
В этом примере мы определили закрытый интерфейс с именем Vehicle, который позволяет реализовать его только классам Car и Bike. Интерфейс определяет единственный метод, называемый start(). Пример 2: Расширение запечатанного класса.

public final class Circle extends Shape {
    private final double radius;
    
    public Circle(double radius) {
        this.radius = radius;
    }
    
    // Определение класса
}
Тут мы определили final-класс под названием Circle, который расширяет запечатанный класс Shape. У класса Circle есть конструктор, который принимает параметр радиуса и инициализирует поле радиуса. Пример 3: Попытка расширить неразрешенный класс (Unpermitted Class).

public class Rectangle extends Shape {
    private final double width;
    private final double height;
    
    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }
    
    // Определение класса
}
В этом примере мы определили класс с именем Rectangle, который пытается расширить запечатанный класс Shape. Однако, поскольку Rectangle не указан в списке разрешенных классов, мы получаем ошибку времени компиляции.

Заключение

Запечатанные классы — это новая мощная функция в Java 17, которая может помочь разработчикам создавать более безопасный, удобный и производительный код. Ограничивая иерархию наследования класса или интерфейса, запечатанные классы позволяют разработчикам лучше контролировать свою кодовую базу.

Что такое сопоставление с образцом и как оно улучшает читаемость кода Java

Источник: Medium В этой статье рассмотрены основы сопоставления с образцом и то, как его можно использовать для повышения удобочитаемости и удобства сопровождения вашего кода. Кофе-брейк #204. Запечатанные классы (Sealed Classes) в Java 17. Что такое сопоставление с образцом и как оно улучшает читаемость кода Java - 2Сопоставление с образцом появилось в Java 14. Эта функция позволяет разработчикам сопоставлять различные типы данных, такие как объекты, примитивы и экземпляры классов, с помощью оператора instanceof, что делает код более кратким и удобочитаемым. Сопоставление с образцом также позволяет извлекать данные из сопоставленных шаблонов и назначать их переменными, что упрощает работу с данными.

Определение

В контексте языков программирования сопоставление с образцом — это метод, используемый для сопоставления определенного образца с заданным входом. Его можно использовать для упрощения процесса идентификации и извлечения соответствующей информации из структурированных данных, таких как объекты, списки и другие сложные типы данных. В функциональных языках сопоставление с образцом можно использовать в качестве альтернативы традиционным операторам потока управления, таким как if-else или switch-case.

Преимущества

  • Простота и удобочитаемость: сопоставление с образцом обеспечивает более краткий и удобочитаемый синтаксис по сравнению с традиционными операторами потока управления.
  • Улучшенная безопасность типов: сопоставление с образцом позволяет разработчикам сопоставлять различные типы данных, такие как объекты, примитивы и экземпляры классов, с помощью оператора instanceof. Это повышает безопасность типов, поскольку снижает риск ошибок типов, упрощает обработку исключений и ошибок.
  • Извлечение значений из совпадающих образцов. При сопоставлении с образцом можно извлекать значения из совпадающих образцов и назначать их переменным, что упрощает работу с совпадающими данными.

Сопоставление с образцом на примере кода


 public interface Shape {
    Double area();
  }

  public final static class Circle implements Shape {
    public final double radius;

    Circle(double radius) {
      this.radius = radius;
    }

    @Override
    public Double area() {
      return Math.PI * radius * radius;
    }
  }

  public final static class Square implements Shape {
    public final double side;

    Square(double side) {
      this.side = side;
    }

    @Override
    public Double area() {
      return side * side;
    }
  }

Сопоставление типов

instanceof — это традиционный способ сопоставления типов. Он проверяет, является ли объект экземпляром определенного типа, и возвращает логическое значение. Он часто используется в условных операторах для определения типа объекта и выполнения соответствующих действий. Также существует оператор switch, представленный в Java 17, который является более кратким и выразительным способом выполнения сопоставления типов. Он позволяет вам сопоставлять тип объекта и извлекать его поля, снижая риск ошибок типа.

 //используем instance of
  public static void printLength(Shape shape) {
    if (shape instanceof Circle) {
      Circle circle = (Circle) shape;
      System.out.println("Circle radius=" + circle.radius);
    } else if (shape instanceof Square) {
      Square square = (Square) shape;
      System.out.println("Square side=" + square.side);
    } else {
      System.out.println("Unknown shape");
    }
  }

  // используем switch 
  public static void printLengthPM(Shape shape) {
    switch (shape) {
      case Circle c -> System.out.println("Circle radius=" + c.radius);
      case Square s -> System.out.println("Square side=" + s.side);
      default -> System.out.println("Unknown shape");
    }
  }
Как мы видим, использование сопоставления с образцом делает код более лаконичным и читабельным, а также избавляет от необходимости приведения совпадающих объектов к правильному типу.

Нулевое совпадение (Null matching)

Обработка значений null — обычная задача в программировании, и сопоставление с образцом обеспечивает элегантный способ ее решения. В приведенном ниже примере “printLengthPM(Shape shape)” оператор switch проверяет наличие null путем обработки отсутствующих элементов shape. По сравнению с традиционным методом проверки значений null с помощью операторов if, сопоставление с образцом упрощает код и устраняет необходимость в отдельных проверках значений null. Это делает код более читабельным и снижает вероятность возникновения проблем с обслуживанием по мере увеличения размера кода.

public static void printLength(Shape shape) {
    if (shape == null) {
      System.out.println("missing shape");
      return;
    }
    ----------
}


public static void printLengthPM(Shape shape) {
    switch (shape) {
      case null -> System.out.println("missing shape");
      case Circle c -> System.out.println("Circle radius=" + c.radius);
      case Square s -> System.out.println("Square side=" + s.side);
      default -> System.out.println("Unknown shape");
    }
  }

Dominance (Доминирование)

В сопоставлении с образцом доминирование играет решающую роль в обеспечении корректности кода и избежании двусмысленности. Доминирование в Java предполагает, что если определенный случай (case) соответствует входному значению, то все другие случаи, которые являются более конкретными или подтипами первого совпадения, не должны выполняться. Рассмотрим следующий пример:

public static void processNumber(Object input) {
  switch (input) {
    case Number n -> System.out.println("got a number=" + n);
    case Integer n -> System.out.println("got an integer=" + n);
    default -> System.out.println("Unknown number");
  }
}
В этом примере n для Integer никогда не будет выполнен, потому что он доминирует над предыдущим case для Number n. Это приводит к ошибке времени компиляции: “this case label is dominated by a preceding case label” (эта метка case доминирует над предыдущей меткой case). Чтобы исправить ошибку, следует изменить порядок case, чтобы конкретные case шли первыми.

Completeness

Completeness (завершенность) гарантирует, что все возможные случаи обрабатываются через сопоставления с образцом. При сопоставлении с образцом важно иметь завершенное выражение switch, чтобы избежать непредвиденного поведения или ошибок в коде. Важно учитывать завершенность, потому что, как показано в нашем первом примере метода printLengthPM, неспособность включить case по умолчанию в оператор switch приведет к ошибке компиляции, указывающей на то, что оператор switch не охватывает все возможные входные значения. Чтобы избежать необходимости в case по умолчанию, интерфейс Shape можно сделать запечатанным. Это можно сделать, просто добавив ключевое слово sealed в объявление интерфейса следующим образом:

public sealed interface Shape {}
Обновленная версия метода printLengthPM:

public static void printLengthPM(Shape shape) {
    switch (shape) {
      case null -> System.out.println("missing shape");
      case Circle c -> System.out.println("Circle radius=" + c.radius);
      case Square s -> System.out.println("Square side=" + s.side);
    }
  }
Если добавляется новая реализация Shape с названием Rectangle, то описанный выше метод, использующий запечатанный интерфейс, не будет компилироваться до тех пор, пока оператор switch не будет обновлен для обработки нового типа Rectangle. Не рекомендуется включать вариант по умолчанию, так как это может привести к неожиданным результатам, если оператор switch не покрывает все возможные входные значения. Вместо этого используйте запечатанный интерфейс для выражения switch — это лучший способ обеспечить завершенность. Объявляя интерфейс запечатанным, компилятор может убедиться, что все возможные подтипы выражения switch охватываются в case. Это приводит к более надежному и защищенному от ошибок коду.

Guarded patterns (Защищенные шаблоны)

Guarded patterns (Защищенные шаблоны) в сопоставлении с образцом обеспечивают способ сопоставления со значением, но только если выполняется определенное условие. Они позволяют более точно контролировать процесс сопоставления и добавляет дополнительный уровень сложности оператору switch. Вот пример:

public static void printLengthPMWithGuard(Shape shape) {
    switch (shape) {
      case Circle c when c.radius > 10 -> System.out.println("Circle with large radius=" + c.radius);
      case Circle c -> System.out.println("Circle with small radius=" + c.radius);
      case Square s when s.side > 10 -> System.out.println("Square with large side=" + s.side);
      case Square s -> System.out.println("Square with small side=" + s.side);
    }
  }
В этом примере метод printLengthPMWithGuard использует защищенные шаблоны для сопоставления с Circle или Square, но только если выполняется определенное условие, например, радиус или сторона больше 10. Это позволяет более точно обрабатывать значения и делает код более гибким и удобным в сопровождении.

Заключение

Можно с уверенностью сказать, что сопоставление с образцом является ценным дополнением к языку Java, которое обеспечивает более удобочитаемый и лаконичный способ сопоставления типов. Использование запечатанных интерфейсов, сопоставления с образцом и защищенных шаблонов обеспечивает новый способ обработки нулевых значений, обеспечения завершенности и упрощения сложного кода. Благодаря простоте использования и улучшенной удобочитаемости сопоставление с образцом — отличный способ улучшить код Java и упростить его обслуживание.
Комментарии (1)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
12 марта 2023
Есть ошибка в блоке про доминирование dominated by это пассивная форма глагола и значит что не он доминирует, а над ним доминируют. Integer не может доминировать над Number.