Русский
Русский
English
Статистика
Реклама

Pattern matching в Java 8

Многие современные языки поддерживают сопоставление с образцом (pattern matching) на уровне языка.
Язык 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.

@Extractpublic 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,      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. Буду рад отзывам, предложениям по улучшению.
Источник: habr.com
К списку статей
Опубликовано: 27.02.2021 22:08:03
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Java

Kotlin

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru