Статьи
Утилиты Telegram YouTube Отзывы

Pattern matching в switch

Видеогайд

17 ноября 2023

Тэги: Java, ООП, руководство.

Содержание

  1. Простой пример
  2. Обработка null
  3. Проверка типа с условием
  4. Тестируем pattern matching
  5. Pattern matching и record

В Java 21 наряду с SequencedCollection (см. SequencedCollection и SequencedSet и SequencedMap) из статуса preview в статус production-ready перешла ещё одна фича – шаблоны сравнения (pattern matching) для конструкции switch. Рассмотрим возможности этой конструкции на конкретных примерах.

Простой пример

Предположим, что мы пишем метод displayScale(), в котором надо определить количество знаков после запятой в переданном нам числе. Причём это число поступает на вход как самый базовый тип Object. Напомню, что базовым для всех чисел является класс Number. Его производными типами являются, например, целые числа Integer и числа с произвольной точностью BigDecimal.

Иерархия числовых классов в Java

Очевидно, что у целого числа нет ни одного знака после запятой, тогда как у BigDecimal для определения точности есть специальный метод scale(). Для других типов чисел будем писать, что точность определить невозможно. А для всех прочих объектов добавим ветку default. С помощью pattern matching наш метод будет выглядеть так:

private void displayScale(Object obj) {
    switch (obj) {
        case Integer i -> System.out.println("Scale of integer: 0");
        case BigDecimal decimal -> System.out.println("Scale of BigDecimal: " + decimal.scale());
        case Number n -> System.out.println("Cannot detect scale of Number");
        default -> System.out.println("Object is not a Number");
    }
}

Здесь мы после case указываем интересующий нас тип и название переменной. Тогда в этой ветке нам будут доступны все методы данного типа.

Здесь очень важен порядок проверки типов. Сначала надо проверять более частные случаи, а затем – более общие. Поэтому компилятор будет следить, чтобы проверка на Number была после Integer и BigDecimal.

Обработка null

Но что будет, если на вход нам придёт null? Мы получим NullPointerException (извечная проблема в Java). Чтобы этого не произошло, добавим отдельную ветку для обработки null:

switch (obj) {
    case null -> System.out.println("Null");
    case Integer i -> System.out.println("Scale of integer: 0");
    case BigDecimal decimal -> System.out.println("Scale of BigDecimal: " + decimal.scale());
    case Number n -> System.out.println("Cannot detect scale of Number");
    default -> System.out.println("Object is not a Number");
}

Проверка типа с условием

Помимо Integer в Java есть и другие целочисленные типы: Byte, Short, Long, BigInteger и для них тоже хотелось бы выводить 0 в качестве результата. Как это сделать? Хочется перечислить типы через запятую в одном case, но пока Java не позволяет это сделать. Зато можно налагать дополнительные условия.

Например, если в текстовом представлении числа нет точки (разделитель целой и дробной части), то мы можем считать такой объект целым числом. Перепишем ветку Integer, заменив его на Number и добавив туда when для проверки условия:

switch (obj) {
    case null -> System.out.println("Null");
    case Number n when !n.toString().contains(".") -> System.out.println("Scale of integer: 0");
    case BigDecimal decimal -> System.out.println("Scale of BigDecimal: " + decimal.scale());
    case Number n -> System.out.println("Cannot detect scale of Number");
    default -> System.out.println("Object is not a Number");
}

Здесь мы после when преобразуем объект Number в строку с помощью toString() и затем проверяем наличие точки в этой строке с помощью contains().

Тестируем pattern matching

Теперь протестируем наш метод, передавая ему на вход разные числа и объекты:

displayScale(null); // null
displayScale(""); // строка, а не число
displayScale(123); // int, scale: 0
displayScale(123456L); // long, scale: 0
displayScale(new BigInteger("12300000000000")); // BigInteger, scale: 0
displayScale(new BigDecimal("123.45")); // BigDecimal, scale: 2

В результате получим:

Null
Object is not a Number
Scale of integer: 0
Scale of integer: 0
Scale of integer: 0
Scale of BigDecimal: 2

Как видите, pattern matching в switch делает код более гибким и выразительным.

Pattern matching и record

Отдельно стоит рассмотреть работу с record-классами в switch. Предположим, что у нас есть интерфейс RotationBody, представляющий различные геометрические тела вращения (конус, шар, цилиндр). Его непременным атрибутом является радиус:

public interface RotationBody {

    int radius();
}

Также у нас есть пара record-классов, реализующих этот интерфейс. Например, класс Ball имеет только радиус:

public record Ball(int radius) implements RotationBody {
}

Поскольку для record-классов компилятор автоматически генерирует get-методы, совпадающие по названию с полями конструктора, то в этом классе явно реализовывать ничего не надо.

Другой класс Cylinder имеет два атрибута: радиус и высоту.

public record Cylinder(int radius, int height) implements RotationBody {
}

Теперь мы готовы написать метод displayVolume(), который получает на вход RotationBody и выводит на экран объём соответствующей фигуры.

private void displayVolume(RotationBody body) {
    switch (body) {
        case null -> System.out.println("No body");
        case Ball(int radius) -> System.out.println("Ball volume: " + Math.PI * 4 / 3 * radius * radius * radius);
        case Cylinder(int radius, int height) ->
                System.out.println("Cylinder volume: " + Math.PI * radius * radius * height);
        default -> System.out.println("Unsupported rotation body");
    }
}

По уже знакомой схеме мы делаем ветки для null и default для всех прочих реализаций интерфейса, если таковые появятся. А каждый record-класс мы прямо в case «раскладываем» на отдельные поля, которые нам будут доступны для вычисления объёма фигуры. По сути мы повторяем конструктор соответствующего класса.

Например, объём шара вычисляется как «4/3 * число Пи * радиус в кубе». Объём цилиндра – «число Пи * радиус в квадрате * высота».

Протестируем наш метод:

displayVolume(null);
displayVolume(new Ball(4));
displayVolume(new Cylinder(4, 5));

В результате получим:

No body
Ball volume: 268.082573106329
Cylinder volume: 251.32741228718345

P.S. Обратите внимание, что в формулах расчёта объёма первым множителем всегда идёт тип double (константа Math.PI), а не int (радиус, высота). Иначе мы потеряем дробную часть.



Комментарии

Добавить комментарий

×

devmark.ru