17 ноября 2023
Тэги: Java, ООП, руководство.
В Java 21 наряду с SequencedCollection (см. SequencedCollection и SequencedSet и SequencedMap) из статуса preview в статус production-ready перешла ещё одна фича – шаблоны сравнения (pattern matching) для конструкции switch. Рассмотрим возможности этой конструкции на конкретных примерах.
Предположим, что мы пишем метод displayScale(), в котором надо определить количество знаков после запятой в переданном нам числе. Причём это число поступает на вход как самый базовый тип Object. Напомню, что базовым для всех чисел является класс Number. Его производными типами являются, например, целые числа Integer и числа с произвольной точностью BigDecimal.
Очевидно, что у целого числа нет ни одного знака после запятой, тогда как у BigDecimal для определения точности есть специальный метод scale(). Для других типов чисел будем писать, что точность определить невозможно. А для всех прочих объектов добавим ветку default. С помощью pattern matching наш метод будет выглядеть так:
Здесь мы после case указываем интересующий нас тип и название переменной. Тогда в этой ветке нам будут доступны все методы данного типа.
Здесь очень важен порядок проверки типов. Сначала надо проверять более частные случаи, а затем – более общие. Поэтому компилятор будет следить, чтобы проверка на Number была после Integer и BigDecimal.
Но что будет, если на вход нам придёт null? Мы получим NullPointerException (извечная проблема в Java). Чтобы этого не произошло, добавим отдельную ветку для обработки null:
Помимо Integer в Java есть и другие целочисленные типы: Byte, Short, Long, BigInteger и для них тоже хотелось бы выводить 0 в качестве результата. Как это сделать? Хочется перечислить типы через запятую в одном case, но пока Java не позволяет это сделать. Зато можно налагать дополнительные условия.
Например, если в текстовом представлении числа нет точки (разделитель целой и дробной части), то мы можем считать такой объект целым числом. Перепишем ветку Integer, заменив его на Number и добавив туда when для проверки условия:
Здесь мы после when преобразуем объект Number в строку с помощью toString() и затем проверяем наличие точки в этой строке с помощью contains().
Теперь протестируем наш метод, передавая ему на вход разные числа и объекты:
В результате получим:
Как видите, pattern matching в switch делает код более гибким и выразительным.
Отдельно стоит рассмотреть работу с record-классами в switch. Предположим, что у нас есть интерфейс RotationBody, представляющий различные геометрические тела вращения (конус, шар, цилиндр). Его непременным атрибутом является радиус:
Также у нас есть пара record-классов, реализующих этот интерфейс. Например, класс Ball имеет только радиус:
Поскольку для record-классов компилятор автоматически генерирует get-методы, совпадающие по названию с полями конструктора, то в этом классе явно реализовывать ничего не надо.
Другой класс Cylinder имеет два атрибута: радиус и высоту.
Теперь мы готовы написать метод displayVolume(), который получает на вход RotationBody и выводит на экран объём соответствующей фигуры.
По уже знакомой схеме мы делаем ветки для null и default для всех прочих реализаций интерфейса, если таковые появятся. А каждый record-класс мы прямо в case «раскладываем» на отдельные поля, которые нам будут доступны для вычисления объёма фигуры. По сути мы повторяем конструктор соответствующего класса.
Например, объём шара вычисляется как «4/3 * число Пи * радиус в кубе». Объём цилиндра – «число Пи * радиус в квадрате * высота».
Протестируем наш метод:
В результате получим:
P.S. Обратите внимание, что в формулах расчёта объёма первым множителем всегда идёт тип double (константа Math.PI), а не int (радиус, высота). Иначе мы потеряем дробную часть.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.