Будучи занятым разработчиком, трудно следить за новыми возможностями и глубоко понимать, где и как их можно использовать.
В этой статье блога я расскажу о 5 местах, где вы можете использовать сопоставление с шаблоном в Java, не погружаясь в тонкие детали. Когда вы решите, что готовы к дальнейшему изучению, посмотрите ссылки, включенные в эту статью.
Давайте начнем!
1. Улучшение читабельности кода путем преобразования длинных операторов if-else в switch
Во-первых, давайте ответим на самый важный вопрос — почему нам важно это преобразование?
Одно из главных преимуществ заключается в том, что код становится более лаконичным и его легче читать и понимать. Поскольку длинные операторы if‑else обычно не умещаются на одном экране и могут потребовать вертикальной прокрутки, трудно понять код, который выполняется для всех сравнений if. Кроме того, синтаксис условий if может быть неясен, поскольку каждое условие if может иметь другой набор условий.
Часто, просматривая кодовую базу, вы замечаете код, похожий на показанный ниже. Это длинный оператор if‑else, который условно присваивает значение локальной переменной. Взгляните на приведенный ниже код. Чуть позже я помогу вам сориентироваться в нем, выделив определенные разделы:
private static String getValueText(Object value) {
final String newExpression;
if (value instanceof String) {
final String string = (String)value;
newExpression = '"' + StringUtil.escapeStringCharacters(string) + '"';
}
else if (value instanceof Character) {
newExpression = '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\'';
}
else if (value instanceof Long) {
newExpression = value.toString() + 'L';
}
else if (value instanceof Double) {
final double v = (Double)value;
if (Double.isNaN(v)) {
newExpression = "java.lang.Double.NaN";
}
else if (Double.isInfinite(v)) {
if (v > 0.0) {
newExpression = "java.lang.Double.POSITIVE_INFINITY";
}
else {
newExpression = "java.lang.Double.NEGATIVE_INFINITY";
}
}
else {
newExpression = Double.toString(v);
}
}
else if (value instanceof Float) {
final float v = (Float) value;
if (Float.isNaN(v)) {
newExpression = "java.lang.Float.NaN";
}
else if (Float.isInfinite(v)) {
if (v > 0.0F) {
newExpression = "java.lang.Float.POSITIVE_INFINITY";
}
else {
newExpression = "java.lang.Float.NEGATIVE_INFINITY";
}
}
else {
newExpression = Float.toString(v) + 'f';
}
}
else if (value == null) {
newExpression = "null";
}
else {
newExpression = String.valueOf(value);
}
return newExpression;
}
Давайте выделим код, на котором следует сосредоточиться. На следующем изображении метод getValueText
определяет, относится ли значение переменной value
к определенному типу данных, например String
, Character
, Long
, Double
или другим:
Чтобы понять другие части этого оператора if-else, давайте сосредоточимся на переменной newExpression
. Обратите внимание, что этой переменной присваивается значение для всех возможных значений переменной value
:
Интересно, что все блоки кода, кроме двух, соответствующие условиям if, длиннее, чем другие блоки if, которые обычно состоят из одной строки кода:
Давайте извлечем эти два длинных блока кода в отдельные методы, а затем перейдем к преобразованию оператора if‑else в switch.
Чтобы извлечь код в отдельный метод, выделите код, вызовите контекстные действия с помощью клавиш Alt+Enter или (Option+Enter для macOS) и выберите опцию «extract method». Вы можете выбрать одно из предложенных имен для нового метода или ввести имя по своему усмотрению. Для выделения логических фрагментов кода я чаще всего использую сочетание клавиш Ctrl+W (или Ctrl+Shift+W для уменьшения выделения). После извлечения метода следуйте указаниям IntelliJ IDEA, обращая внимание на ключевые слова с желтым фоном и вызывая контекстные действия (Alt+Enter). Чтобы преобразовать if‑else в switch, я вызвала контекстные действия для «if» и выбрал «convert 'if' to 'switch'»:
Вот оператор switch в методе, getValueText
, который является более лаконичной и понятной:
private static String getValueText(Object value) {
final String newExpression = switch (value) {
case String string -> '"' + StringUtil.escapeStringCharacters(string) + '"';
case Character character -> '\'' + StringUtil.escapeStringCharacters(value.toString()) + '\'';
case Long aLong -> value.toString() + 'L';
case Double aDouble -> getNewExpression(aDouble);
case Float aFloat -> getNewExpression(aFloat);
case null -> "null";
default -> String.valueOf(value);
};
return newExpression;
}
Заставляет ли это вас задуматься, почему вы не использовали в своем коде выражения switch так же часто, как операторы if‑else? На это есть несколько причин. Конструкция switch была усовершенствована в последних выпусках Java — они могут возвращать значения (выражения switch) и больше не ограничиваются сравнением значений для примитивных типов данных, классов‑оберток и других, таких как String
или enum. Кроме того, их case метки могут включать шаблоны и условия.
С помощью сопоставления с образцом (pattern matching) и switch вы также можете работать со значениями null, используя null в качестве метки case. Кроме того, каждая метка case объявляет переменную шаблона независимо от того, используются ли они в соответствующем блоке кода. Если вас беспокоит отсутствие меток break, они не требуются, когда вы используете стили стрелок со switch.
Однако эта функция находится на стадии предварительного просмотра, что означает, что она может измениться в будущей версии Java. Поэтому вам не следует использовать ее в своем производственном коде.
Пожалуйста, перейдите по этой ссылке, чтобы проверить конфигурации, если вы не знакомы с ними.
Не все операторы if‑else могут быть преобразованы в операторы switch. Вы можете использовать оператор if‑else для определения сложных условий, которые могут использовать комбинацию переменных, констант или вызовов методов. Такие сложные сравнения пока не поддерживаются операторами switch.
Подробный материал о сопоставлении шаблонов можно найти в этом сообщении блога.
Выполнение инспекции «if can be replaced with switch» в вашей кодовой базе
Поиск операторов if‑else в коде и проверка возможности их замены на switch может занять много времени. Вы можете запустить проверку «if can be replaced with switch» для всех классов в вашей кодовой базе или ее подмножестве, как описано в этом сообщении блога.
Давайте поработаем со следующим примером, в котором используется сопоставление с шаблоном для instanceof, производственной функции в Java.
2. Написание лаконичного кода с использованием сопоставления с шаблоном для instanceof
Использование сопоставления с шаблоном для оператора instanceof
было доступно в качестве производственной функции, начиная с версии Java 16, и может использоваться в производственном коде.
Чтобы использовать эту возможность, я просто последую указаниям IntelliJ IDEA и вызову контекстные действия для ключевого слова if
, которое выделено желтым фоном.
Представьте, что у вас есть класс, скажем, Monitor
. Вот один из распространенных примеров, которые вы можете найти в кодовых базах для реализации метода equals
:
public class Monitor {
String model;
double price;
@Override
public boolean equals(Object object) {
if (object instanceof Monitor) {
Monitor other = (Monitor) object;
return model.equals(other.model) && price == other.price;
}
return false;
}
}
На следующем рисунке показано, как можно использовать сопоставление с шаблоном, вызывая контекстные действия для переменной с именем other
, выделенной желтым фоном, а затем выбирая опцию 'replace 'other' with pattern variable'. Рефакторинг полученного кода путем вызова контекстных действий для оператора if может сделать этот код еще более лаконичным. Окончательный код легче читать и понимать — он возвращает true, если все три упомянутых условия истинны.
Что произойдет, если вместо обычного класса вы работаете с экземпляром записи (Record)? Для записей сопоставление с шаблоном для instanceof может деконструировать экземпляр записи путем, определяя переменные шаблона для компонентов записи. В следующем примере Citizen(String name, int age)
, используемый с оператором instanceof, является шаблоном записи:
Легко не заметить силу таких возможностей, если начать с простых примеров кода, подобных двум предыдущим. Давайте быстро рассмотрим другой пример использования сопоставления с шаблоном для оператора instanceof, где удаление объявления локальной переменной приводит к другим возможностям рефакторинга или улучшения кода. Короче говоря, сочетание этой функции с другими техниками рефакторинга или улучшения кода может помочь вам написать более качественный код (просто следуйте указаниям IntelliJ IDEA!):
3. Игнорируйте состояния, которые не имеют смысла
Оператор if-else может быть не лучшим выбором для итерации значений типа, который имеет исчерпывающий набор значений, например, перечисление или подтипы запечатанного класса.
Например, представьте, что у вас есть перечисление, которое определяет фиксированный набор значений следующим образом:
enum SingleUsePlastic {
BOTTLE, SPOON, CARRY_BAG;
}
Даже если вы знаете, что экземпляр типа SingleUsePlastic
может иметь любое из трех значений, то есть BOTTLE
, SPOON
и CARRY_BAG
, следующий код не будет компилироваться для замены финальной локальной переменной:
public class Citizen {
String getReplacements(SingleUsePlastic plastic) {
final String replacement;
if (plastic == SingleUsePlastic.BOTTLE) {
replacement = "Booth 4: Pick up a glass bottle";
} else if (plastic == SingleUsePlastic.SPOON) {
replacement = "Pantry: Pick up a steel spoon";
} else if (plastic == SingleUsePlastic.CARRY_BAG) {
replacement = "Booth 5: Pick up a cloth bag";
}
return replacement;
}
}
Чтобы заставить его компилироваться, вам нужно добавить в конце предложение else, которое не имеет смысла.
public class Citizen {
String getReplacements(SingleUsePlastic plastic) {
final String replacement;
if (plastic == SingleUsePlastic.BOTTLE) {
replacement = "Booth 4: Pick up a glass bottle";
} else if (plastic == SingleUsePlastic.SPOON) {
replacement = "Pantry: Pick up a steel spoon";
} else if (plastic == SingleUsePlastic.CARRY_BAG) {
replacement = "Booth 5: Pick up a cloth bag";
} else {
replacement = "";
}
return replacement;
}
}
При использовании оператора switch вам не нужно кодировать блок default
для несуществующих значений:
public class Citizen {
String getReplacements(SingleUsePlastic plastic) {
final String replacement = switch (plastic) {
case BOTTLE -> "Booth 4: Pick up a glass bottle";
case SPOON -> "Pantry: Pick up a steel spoon";
case CARRY_BAG -> "Booth 5: Pick up a cloth bag";
};
return replacement;
}
}
Аналогично, если вы определяете запечатанный класс, вы можете использовать оператор switch для перебора его исчерпывающего списка подклассов без определения блока default
:
sealed interface Lego {}
final class SquareLego implements Lego {}
non-sealed class RectangleLogo implements Lego {}
sealed class CharacterLego implements Lego permits PandaLego {}
final class PandaLego extends CharacterLego {}
public class MyLegoGame {
int processLego(Lego lego) {
return switch (lego) {
case SquareLego squareLego -> 100;
case RectangleLego rectangleLego-> 200;
case CharacterLego characterLego -> 300;
};
}
}
Если вы не знакомы с запечатанными классами и хотите погрузиться в эту тему, вы можете обратиться к этой статье в блоге.
4. Мощная и лаконичная обработка данных
Вы можете создать мощный, но лаконичный и выразительный код для обработки данных, используя комбинацию шаблонов записей, выражений switch и запечатанных классов. Вот пример запечатанного интерфейса TwoDimensional
, который реализуется в виде записей Point
, Line
, Triangle
и Square
:
sealed interface TwoDimensional {}
record Point (int x,
int y) implements TwoDimensional { }
record Line (Point start,
Point end) implements TwoDimensional { }
record Triangle (Point pointA,
Point pointB,
Point PointC) implements TwoDimensional { }
record Square (Point pointA,
Point pointB,
Point PointC,
Point pointD) implements TwoDimensional { }
Следующий метод определяет рекурсивный процесс метода, который использует оператор switch для возврата суммы координат x и y всех точек двумерных фигур, таких как Line
, Triangle
или Square
:
static int process(TwoDimensional twoDim) {
return switch (twoDim) {
case Point(int x, int y) -> x + y;
case Line(Point a, Point b) -> process(a) + process(b);
case Triangle(Point a, Point b, Point c) -> process(a) + process(b) + process(c);
case Square(Point a, Point b, Point c, Point d) -> process(a) + process(b) + process(c) + process(d);
};
}
IntelliJ IDEA также отображает значок рекурсивного вызова в поле gutter для этого метода:
5. Разделение вычислений и побочного эффекта
Часто можно встретить код, который сочетает в себе вычисления и побочный эффект (например, вывод на консоль) в одном блоке кода. Например, следующий код использует блок if-else и оператор instanceof
для определения типа переменной и выводит значение условно в каждом блоке кода if.
void printObject(Object obj) {
if (obj instanceof String s) {
System.out.println("String: \"" + s + "\"");
} else if (obj instanceof Collection<?> c) {
System.out.println("Collection (size = " + c.size() + ")");
} else {
System.out.println("Other object: " + obj);
}
}
На следующем рисунке показано, как можно преобразовать этот блок if‑else в оператор switch, а затем использовать новую инспекцию для оператора switch «Push down for „switch“ expressions» с последующим извлечением переменной для разделения вычислений и их побочных эффектов:
Резюме
В этой статье блога мы рассмотрели 5 мест, где занятые разработчики могут использовать сопоставление с шаблоном в Java.
Если вы хотите узнать больше об этих функциях или о том, как IntelliJ IDEA помогает вам использовать их, обратитесь к ссылкам, которые приведены в описании этих примеров.