Многие современные языки поддерживают сопоставление с образцом (pattern matching) на уровне языка.
Язык Java не является исключениям. И в Java 16 будет добавлено поддержка сопоставление с образцом для оператора instanceof, как финальной фичи.
В будущем надеемся, что сопоставление с образцом будем расширено и для других языковых конструкций.
Сопоставление с образцом раскрывают перед разработчиком возможность писать код более гибко и красивее, при этом оставляя его понятным.
Но что если нельзя перейти с тех или иных причин на новые версии Java. Благо используя возможности Java 8, можно реализовать некоторые возможности pattern matching в виде библиотеки.
Рассмотрим некоторые паттерны, и как их можно реализовать с помощью простенькой библиотеки.
Constant pattern позволяет проверить на равность с константами. В Java оператор switch позволяет проверить на равность числа, перечисления и строки. Но иногда хочется проверить на равность константы объектов используя метод equals().
Подобный код можно написать следующим образом. При этом под капотом осуществляется сравнения значений и проверка их в операторе if. Можно использовать как форме утверждение так и как выражения.
Так же можно очень просто работать с диапазонами значений.
Tuple pattern позволяет проверить на равность нескольких переменных с константами одновременно.
При этом кроме использования в форме switch, можно разложить на сопоставляющие или пройти последовательно в цикле.
Type test pattern позволяет одновременно сопоставить тип и извлечь значение переменной.
В Java для этого нам нужно сначала проверить тип, привести к типу и потом присвоить новой переменной. С помощью такого паттерна код стает на много проще.
Guard pattern позволяет одновременно сопоставить тип и проверить на условия.
Подобную конструкцию можно реализовать следующим образом. Чтобы упростить написания условий, можно использовать следующее функции для сравнения: lessThan/lt, greaterThan/gt, lessThanOrEqual/le, greaterThanOrEqual/ge, equal/eq, notEqual/ne. А для того чтобы опустить условия можно пременить: always/yes, never/no.
Deconstruction pattern позволяет одновременно сопоставить тип и разложить объект на составляющие.
В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса.
При этом чтобы получить составляющее, класс должен иметь один или несколько деконструирующих методов. Эти методы должны быть помечены аннотаций Extract.
Все параметры должны быть открытыми. Поскольку примитивы нельзя передать в метод по ссылке, нужно использовать обертки на примитивы IntRef, FloatRef и т.д.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования и приемы с стандартным классом LambdaMetafactory.
Property pattern позволяет одновременно сопоставить тип и доступиться к полям класса по их именам.
Это упрощенная форма деконструирующего паттерна, где нужны только конкретные поля класса разложить.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования и приемы с стандартным классом LambdaMetafactory.
Также для упрощения именования полей можно использовать другой способ с ссылками на методы.
Position pattern позволяет одновременно сопоставить тип и проверить значение полей в порядке объявления.
В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса и проверить на равность.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования.
Также если разработчик не хочет проверять некоторые поля, эти поля должны быть помечены аннотаций Exclude. Эти поля должны быть объявлены последними.
Static pattern позволяет одновременно сопоставить тип и деконструировать объект используя фабричные методы.
Подобный до деконструирующего паттерна, но имя деконструирующих методов, которые помеченные аннотаций Extract, должны быть указаны явно.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования и приемы с стандартным классом LambdaMetafactory.
Sequence pattern позволяет проще обрабатывать последовательности данных.
Используя библиотечные методы можно просто работать с последовательностями данных.
Также для упрощения кода, можно использовать следующее функции, которые можно увидеть в современных языках как языковые фичи или функции.
Как можно видеть pattern matching сильный инструмент, который намного упрощает написание кода. Используя возможности Java 8 можно сэмулировать возможности pattern matching самыми средствами языка.
Исходной код библиотеки можно посмотреть на github: link. Буду рад отзывам, предложениям по улучшению.
Язык Java не является исключениям. И в Java 16 будет добавлено поддержка сопоставление с образцом для оператора instanceof, как финальной фичи.
В будущем надеемся, что сопоставление с образцом будем расширено и для других языковых конструкций.
Сопоставление с образцом раскрывают перед разработчиком возможность писать код более гибко и красивее, при этом оставляя его понятным.
Но что если нельзя перейти с тех или иных причин на новые версии Java. Благо используя возможности Java 8, можно реализовать некоторые возможности pattern matching в виде библиотеки.
Рассмотрим некоторые паттерны, и как их можно реализовать с помощью простенькой библиотеки.
Constant pattern позволяет проверить на равность с константами. В Java оператор switch позволяет проверить на равность числа, перечисления и строки. Но иногда хочется проверить на равность константы объектов используя метод equals().
switch (data) {
case new Person("man") -> System.out.println("man");
case new Person("woman") -> System.out.println("woman");
case new Person("child") -> System.out.println("child");
case null -> System.out.println("Null value ");
default -> System.out.println("Default value: " + data);
};
Подобный код можно написать следующим образом. При этом под капотом осуществляется сравнения значений и проверка их в операторе if. Можно использовать как форме утверждение так и как выражения.
Так же можно очень просто работать с диапазонами значений.
import static org.kl.jpml.pattern.ConstantPattern.*;
matches(data).as(
new Person("man"), () -> System.out.println("man"),
new Person("woman"), () -> System.out.println("woman"),
new Person("child"), () -> System.out.println("child"),
Null.class, () -> System.out.println("Null value "),
Else.class, () -> System.out.println("Default value: " + data)
);
matches(data).as(
or(1, 2), () -> System.out.println("1 or 2"),
in(3, 6), () -> System.out.println("between 3 and 6"),
in(7), () -> System.out.println("7"),
Null.class, () -> System.out.println("Null value "),
Else.class, () -> System.out.println("Default value: " + data)
);
Tuple pattern позволяет проверить на равность нескольких переменных с константами одновременно.
var (side, width) = border;
switch (side, width) {
case ("top", 25) -> System.out.println("top");
case ("bottom", 30) -> System.out.println("bottom");
case ("left", 15) -> System.out.println("left");
case ("right", 15) -> System.out.println("right");
case null -> System.out.println("Null value ");
default -> System.out.println("Default value ");
};
for ((side, width) : listBorders) {
System.out.println("border: " + [side + "," + width]);
}
При этом кроме использования в форме switch, можно разложить на сопоставляющие или пройти последовательно в цикле.
import static org.kl.jpml.pattern.TuplePattern.*;
let(border, (String side, int width) -> {
System.out.println("border: " + side + "," + width);
});
matches(side, width).as(
of("top", 25), () -> System.out.println("top"),
of("bottom", 30), () -> System.out.println("bottom"),
of("left", 15, () -> System.out.println("left"),
of("right", 15), () -> System.out.println("right"),
Null.class, () -> System.out.println("Null value"),
Else.class, () -> System.out.println("Default value")
);
foreach(listBorders, (String side, int width) -> {
System.out.println("border: " + side + "," + width);
}
Type test pattern позволяет одновременно сопоставить тип и извлечь значение переменной.
switch (data) {
case Integer i -> System.out.println(i * i);
case Byte b -> System.out.println(b * b);
case Long l -> System.out.println(l * l);
case String s -> System.out.println(s * s);
case null -> System.out.println("Null value ");
default -> System.out.println("Default value: " + data);
};
В Java для этого нам нужно сначала проверить тип, привести к типу и потом присвоить новой переменной. С помощью такого паттерна код стает на много проще.
import static org.kl.jpml.pattern.VerifyPattern.matches;
matches(data).as(
Integer.class, i -> { System.out.println(i * i); },
Byte.class, b -> { System.out.println(b * b); },
Long.class, l -> { System.out.println(l * l); },
String.class, s -> { System.out.println(s * s); },
Null.class, () -> { System.out.println("Null value "); },
Else.class, () -> { System.out.println("Default value: " + data); }
);
Guard pattern позволяет одновременно сопоставить тип и проверить на условия.
switch (data) {
case Integer i && i != 0 -> System.out.println(i * i);
case Byte b && b > -1 -> System.out.println(b * b);
case Long l && l < 5 -> System.out.println(l * l);
case String s && !s.empty() -> System.out.println(s * s);
case null -> System.out.println("Null value ");
default -> System.out.println("Default: " + data);
};
Подобную конструкцию можно реализовать следующим образом. Чтобы упростить написания условий, можно использовать следующее функции для сравнения: lessThan/lt, greaterThan/gt, lessThanOrEqual/le, greaterThanOrEqual/ge, equal/eq, notEqual/ne. А для того чтобы опустить условия можно пременить: always/yes, never/no.
import static org.kl.jpml.pattern.GuardPattern.matches;
matches(data).as(
Integer.class, i -> i != 0, i -> { System.out.println(i * i); },
Byte.class, b -> b > -1, b -> { System.out.println(b * b); },
Long.class, l -> l == 5, l -> { System.out.println(l * l); },
Null.class, () -> { System.out.println("Null value "); },
Else.class, () -> { System.out.println("Default value: " + data); }
);
matches(data).as(
Integer.class, ne(0), i -> { System.out.println(i * i); },
Byte.class, gt(-1), b -> { System.out.println(b * b); },
Long.class, eq(5), l -> { System.out.println(l * l); },
Null.class, () -> { System.out.println("Null value "); },
Else.class, () -> { System.out.println("Default value: " + data); }
);
Deconstruction pattern позволяет одновременно сопоставить тип и разложить объект на составляющие.
let (int w, int h) = figure;
switch (figure) {
case Rectangle(int w, int h) -> out.println("square: " + (w * h));
case Circle (int r) -> out.println("square: " + (2 * Math.PI * r));
default -> out.println("Default square: " + 0);
};
for ((int w, int h) : listFigures) {
System.out.println("square: " + (w * h));
}
В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса.
import static org.kl.jpml.pattern.DeconstructPattern.*;
Figure figure = new Rectangle();
let(figure, (int w, int h) -> {
System.out.println("border: " + w + " " + h));
});
matches(figure).as(
Rectangle.class, (int w, int h) -> out.println("square: " + (w * h)),
Circle.class, (int r) -> out.println("square: " + (2 * Math.PI * r)),
Else.class, () -> out.println("Default square: " + 0)
);
foreach(listRectangles, (int w, int h) -> {
System.out.println("square: " + (w * h));
});
При этом чтобы получить составляющее, класс должен иметь один или несколько деконструирующих методов. Эти методы должны быть помечены аннотаций Extract.
Все параметры должны быть открытыми. Поскольку примитивы нельзя передать в метод по ссылке, нужно использовать обертки на примитивы IntRef, FloatRef и т.д.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования и приемы с стандартным классом LambdaMetafactory.
@Extract
public void deconstruct(IntRef width, IntRef height) {
width.set(this.width);
height.set(this.height);
}
Property pattern позволяет одновременно сопоставить тип и доступиться к полям класса по их именам.
let (w: int w, h:int h) = figure;
switch (figure) {
case Rectangle(w: int w == 5, h: int h == 10) -> out.println("sqr: " + (w * h));
case Rectangle(w: int w == 10, h: int h == 15) -> out.println("sqr: " + (w * h));
case Circle (r: int r) -> out.println("sqr: " + (2 * Math.PI * r));
default -> out.println("Default sqr: " + 0);
};
for ((w: int w, h: int h) : listRectangles) {
System.out.println("square: " + (w * h));
}
Это упрощенная форма деконструирующего паттерна, где нужны только конкретные поля класса разложить.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования и приемы с стандартным классом LambdaMetafactory.
import static org.kl.jpml.pattern.PropertyPattern.*;
Figure figure = new Rectangle();
let(figure, of("w", "h"), (int w, int h) -> {
System.out.println("border: " + w + " " + h));
});
matches(figure).as(
Rect.class, of("w", 5, "h", 10), (int w, int h) -> out.println("sqr: " + (w * h)),
Rect.class, of("w", 10, "h", 15), (int w, int h) -> out.println("sqr: " + (w * h)),
Circle.class, of("r"), (int r) -> out.println("sqr: " + (2 * Math.PI * r)),
Else.class, () -> out.println("Default sqr: " + 0)
);
foreach(listRectangles, of("x", "y"), (int w, int h) -> {
System.out.println("square: " + (w * h));
});
Также для упрощения именования полей можно использовать другой способ с ссылками на методы.
Figure figure = new Rect();
let(figure, Rect::w, Rect::h, (int w, int h) -> {
System.out.println("border: " + w + " " + h));
});
matches(figure).as(
Rect.class, Rect::w, Rect::h, (int w, int h) -> System.out.println("sqr: " + (w * h)),
Circle.class, Circle::r, (int r) -> System.out.println("sqr: " + (2 * Math.PI * r)),
Else.class, () -> System.out.println("Default sqr: " + 0)
);
foreach(listRectangles, Rect::w, Rect::h, (int w, int h) -> {
System.out.println("square: " + (w * h));
});
Position pattern позволяет одновременно сопоставить тип и проверить значение полей в порядке объявления.
switch (data) {
case Circle(5) -> System.out.println("small circle");
case Circle(15) -> System.out.println("middle circle");
case null -> System.out.println("Null value ");
default -> System.out.println("Default value: " + data);
};
В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса и проверить на равность.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования.
import static org.kl.jpml.pattern.PositionPattern.*;
matches(data).as(
Circle.class, of(5), () -> { System.out.println("small circle"); },
Circle.class, of(15), () -> { System.out.println("middle circle"); },
Null.class, () -> { System.out.println("Null value "); },
Else.class, () -> { System.out.println("Default value: " + data); }
);
Также если разработчик не хочет проверять некоторые поля, эти поля должны быть помечены аннотаций Exclude. Эти поля должны быть объявлены последними.
class Circle {
private int radius;
@Exclude
private int temp;
}
Static pattern позволяет одновременно сопоставить тип и деконструировать объект используя фабричные методы.
switch (some) {
case Result.value(var v) -> System.out.println("value: " + v)
case Result.error(var e) -> System.out.println("error: " + e)
default -> System.out.println("Default value")
};
Подобный до деконструирующего паттерна, но имя деконструирующих методов, которые помеченные аннотаций Extract, должны быть указаны явно.
Чтобы уменьшить оверхед с использованием рефлексии, используется кеширования и приемы с стандартным классом LambdaMetafactory.
import static org.kl.jpml.pattern.StaticPattern.*;
matches(figure).as(
Result.class, of("value"), (var v) -> System.out.println("value: " + v),
Result.class, of("error"), (var e) -> System.out.println("error: " + e),
Else.class, () -> System.out.println("Default value")
);
Sequence pattern позволяет проще обрабатывать последовательности данных.
List<Integer> list = ...;
switch (list) {
case empty() -> System.out.println("Empty value")
case head(var h) -> System.out.println("list head: " + h)
case tail(var t) -> System.out.println("list tail: " + t)
default -> System.out.println("Default value")
};
Используя библиотечные методы можно просто работать с последовательностями данных.
import static org.kl.jpml.pattern.SequencePattern.*;
List<Integer> list = List.of(1, 2, 3);
matches(figure).as(
empty() () -> System.out.println("Empty value"),
head(), (var h) -> System.out.println("list head: " + h),
tail(), (var t) -> System.out.println("list tail: " + t),
Else.class, () -> System.out.println("Default value")
);
Также для упрощения кода, можно использовать следующее функции, которые можно увидеть в современных языках как языковые фичи или функции.
import static org.kl.jpml.pattern.CommonPattern.*;
var rect = lazy(Rectangle::new);
var result = elvis(rect.get(), new Rectangle());
with(rect, it -> {
it.setWidth(5);
it.setHeight(10);
});
when(
side == Side.LEFT, () -> System.out.println("left value"),
side == Side.RIGHT, () -> System.out.println("right value")
);
repeat(3, () -> {
System.out.println("three time");
)
int even = self(number).takeIf(it -> it % 2 == 0);
int odd = self(number).takeUnless(it -> it % 2 == 0);
Как можно видеть pattern matching сильный инструмент, который намного упрощает написание кода. Используя возможности Java 8 можно сэмулировать возможности pattern matching самыми средствами языка.
Исходной код библиотеки можно посмотреть на github: link. Буду рад отзывам, предложениям по улучшению.