Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Многие современные языки поддерживают сопоставление с образцом (pattern matching) на уровне языка. Java в данный момент не поддерживает pattern matching, но есть надежды что в будущем все может измениться.
Сопоставление с образцом раскрывают перед разработчиком возможность писать код более гибко и красивее, при этом оставляя его понятным.
Используя возможности 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);
};
В данный момент библиотека поддерживает возможность проверять значения переменной с 6 константами.
import org.kl.state.Else;
import org.kl.state.Null;
import static org.kl.pattern.ConstantPattern.matches;
matches(data,
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)
);
Tuple pattern позволяет проверить на равность нескольких перемен с константами одновременно.
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 ");
};
В данный момент библиотека поддерживает возможность указывать 4 переменные и 6 веток.
import org.kl.state.Else;
import org.kl.state.Null;
import static org.kl.pattern.TuplePattern.matches;
matches(side, width,
"top", 25, () -> System.out.println("top");
"bottom", 30, () -> System.out.println("bottom");
"left", 15, () -> System.out.println("left");
"right", 15, () -> System.out.println("right");
Null.class, () -> System.out.println("Null value"),
Else.class, () -> System.out.println("Default value")
);
Type test pattern позволяет одновременно сопоставить тип и извлечь значение переменной. В Java для этого нам нужно сначала проверить тип, привести к типу и потом присвоить новой переменной.
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);
};
В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами.
import org.kl.state.Else;
import org.kl.state.Null;
import static org.kl.pattern.VerifyPattern.matches;
matches(data,
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);
};
В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами и условиями.
import org.kl.state.Else;
import org.kl.state.Null;
import static org.kl.pattern.GuardPattern.matches;
matches(data,
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); }
);
Для упрощения написания условия, разработчик может использовать следующее функции для сравнения: lessThan/lt, greaterThan/gt, lessThanOrEqual/le, greaterThanOrEqual/ge,
equal/eq, notEqual/ne. А для того чтобы опустить условия можно пременить: always/yes, never/no.
matches(data,
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 позволяет одновременно сопоставить тип и разложить объект на составляющие. В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса.
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));
}
В данный момент библиотека поддерживает возможность проверять значения переменной с 3 типами и раскладывать объект на 3 составляющее.
import org.kl.state.Else;
import static org.kl.pattern.DeconstructPattern.matches;
import static org.kl.pattern.DeconstructPattern.foreach;
import static org.kl.pattern.DeconstructPattern.let;
Figure figure = new Rectangle();
let(figure, (int w, int h) -> {
System.out.println("border: " + w + " " + h));
});
matches(figure,
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 и т.д.
import org.kl.type.IntRef;
...
@Extract
public void deconstruct(IntRef width, IntRef height) {
width.set(this.width);
height.set(this.height);
}
Также используя Java 11, можно выводить типы деконструирующих параметров.
Figure figure = new Rectangle();
let(figure, (var w, var h) -> {
System.out.println("border: " + w + " " + h));
});
matches(figure,
Rectangle.class, (var w, var h) -> out.println("square: " + (w * h)),
Circle.class, (var r) -> out.println("square: " + (2 * Math.PI * r)),
Else.class, () -> out.println("Default square: " + 0)
);
foreach(listRectangles, (var w, var h) -> {
System.out.println("square: " + (w * h));
});
Property pattern позволяет одновременно сопоставить тип и доступиться к полям класса по их именам. Вместо того, чтобы предоставить все поля, можно доступиться к нужным и в любой последовательности.
let (w: int w, h:int h) = figure;
switch (figure) {
case Rect(w: int w == 5, h: int h == 10) -> out.println("sqr: " + (w * h));
case Rect(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));
}
В данный момент библиотека поддерживает возможность проверять значения переменной с 3 типами и раскладывать объект на 3 составляющее.
import org.kl.state.Else;
import static org.kl.pattern.PropertyPattern.matches;
import static org.kl.pattern.PropertyPattern.foreach;
import static org.kl.pattern.PropertyPattern.let;
import static org.kl.pattern.PropertyPattern.of;
Figure figure = new Rectangle();
let(figure, of("w", "h"), (int w, int h) -> {
System.out.println("border: " + w + " " + h));
});
matches(figure,
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,
Rect.class, Rect::w, Rect::h, (int w, int h) -> out.println("sqr: " + (w * h)),
Circle.class, Circle::r, (int r) -> 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 позволяет одновременно сопоставить тип и проверить значение полей в порядке объявления. В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса и проверить на равность.
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);
};
В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами и проверять сразу 4 поля.
import org.kl.state.Else;
import org.kl.state.Null;
import static org.kl.pattern.PositionPattern.matches;
import static org.kl.pattern.PositionPattern.of;
matches(data,
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 позволяет одновременно сопоставить тип и деконструировать объект используя фабричные методы.
Optional some = ...;
switch (some) {
case Optional.empty() -> System.out.println("empty value");
case Optional.of(var v) -> System.out.println("value: " + v);
default -> System.out.println("Default value");
};
В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами и раскладывать объект на 3 составляющее.
import org.kl.state.Else;
import static org.kl.pattern.StaticPattern.matches;
import static org.kl.pattern.StaticPattern.of;
Optional some = ...;
matches(figure,
Optinal.class, of("empty"), () -> System.out.println("empty value"),
Optinal.class, of("of") , (var v) -> System.out.println("value: " + v),
Else.class, () -> System.out.println("Default value")
);
При этом чтобы получить составляющее, класс должен иметь один или несколько деконструирующих методов, помеченные аннотаций Extract.
@Extract
public void of(IntRef value) {
value.set(this.value);
}
Sequence pattern позволяет проще обрабатывать последовательности данных.
List<Integer> list = ...;
switch (list) {
case Ranges.head(var h) -> System.out.println("list head: " + h);
case Ranges.tail(var t) -> System.out.println("list tail: " + t);
case Ranges.empty() -> System.out.println("Empty value");
default -> System.out.println("Default value");
};
Используя библиотеку можно писать следующим образом.
import org.kl.state.Else;
import org.kl.range.Ranges;
import static org.kl.pattern.SequencePattern.matches;
List<Integer> list = ...;
matches(figure,
Ranges.head(), (var h) -> System.out.println("list head: " + h),
Ranges.tail(), (var t) -> System.out.println("list tail: " + t),
Ranges.empty() () -> System.out.println("Empty value"),
Else.class, () -> System.out.println("Default value")
);
Также для упрощения кода, можно использовать следующее функции.
import static org.kl.pattern.CommonPattern.with;
import static org.kl.pattern.CommonPattern.when;
Rectangle rect = 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")
);
Как можно видеть pattern matching сильный инструмент, который намного упрощает написание кода. Используя лямбды-выражения, ссылки на метод и вывод типов параметров лямбды можно сэмулировать возможности pattern matching самыми средствами языка.
Исходной код библиотеки открыт и доступный на github.