Многие современные языки поддерживают сопоставление с образцом (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)
);
```<cut/>
<i><b>Tuple pattern</b></i> позволяет проверить на равность нескольких перемен с константами одновременно. 
```java
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.

Комментарии (37)


  1. pin2t
    20.07.2019 18:30
    -1

    Pattern matching — функциональщина, притащенная «во многие языки» без понимания принципов, просто для того чтобы была лишняя фича, в рамках «развития языка». Притащенная соответственно функциональщиками, людьми, которые не знают что такое инкапсуляция.

    Печально смотреть, как Java, хороший язык, со стройными изначально принципами ООП, все больше и больше превращают в PHP. Даже термин специальный придумали — деконструкция, ужас какой.


    1. datacompboy
      20.07.2019 18:34

      Простите, а при чем тут PHP?


      1. pin2t
        20.07.2019 18:39
        -1

        PHP просто как пример того как бездумное добавление новых возможностей в язык в итоге превращает его в непонятно что. Место PHP в этом примере легко займут и JavaScript, и Kotlin


        1. datacompboy
          20.07.2019 19:12
          +1

          Как ни странно, но ФП вполне сочетается с ООП. Даже в одном проекте.
          Главное использовать подходящий к задаче в текущий момент.


          1. pin2t
            21.07.2019 17:28
            -1

            Вообще-то нет, не сочетается, причем совсем. Вот рассмотрим «чудесный» паттерн декоструктор из статьи

            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);
            };

            В ООП это было бы
            out.println(String.format("square: %f", figure.square()));

            или
            figure.printSquare();

            Но никак не так как в примерах из статьи. Тоесть, либо одно, либо другое, вместе никак.
            Ну а если в программе возникает необходимость писать подобный код
            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);
            };

            это значит лишь то что объектно-ориентированный дизайн зашел куда-то совсем не туда. Ну или это код внутри самой JDK, что врядли.


            1. sshikov
              21.07.2019 19:25

              Совершенно независимо от качества примеров автора (и ограниченности данной библиотеки), вы похоже не понимаете, что такое pattern matching, и зачем он нужен.

              >В ООП это было бы
              >out.println(String.format(«square: %f», figure.square()));

              Не было бы. Представьте на мгновение, что figure — это не ваши классы, что они написаны сторонним разработчиком, и менять (а то и посмотреть на их код) вы не можете.

              >зашел куда-то совсем не туда
              А даже если и зашел? Если часть этого кода не ваша, у вас мало шансов поменять там дизайн.

              И да, то что тут рассмотрено — это библиотека, она не может превращать Java как язык во что-либо.


              1. pin2t
                22.07.2019 07:54
                -1

                Совершенно независимо от качества примеров автора (и ограниченности данной библиотеки), вы похоже не понимаете, что такое pattern matching, и зачем он нужен.

                Я прекрасно понимаю что это за паттерн и зачем он нужен. И что он принципиально не совместим с Java.
                Не было бы. Представьте на мгновение, что figure — это не ваши классы, что они написаны сторонним разработчиком, и менять (а то и посмотреть на их код) вы не можете.

                В этом случае тем более никак и никогда не могу использовать паттерн матчинг, потому что все что я вижу из библиотеки — это интерфейсы. я не вижу внутренностей объектов. Попытка использовать тяжелую артиллерию вроде рефлекшена для того чтобы подсмотреть внутренности объектов в библиотеке — это ядерная бомба замедленного действия. В следующей версии библиотеки автор поменяет свои объекты, и весь паттерн матчинг рухнет как карточный домик.
                Если я хочу что-то добавить к коду библиотеки, я оборачиваю библиотечные классы своими классами, но никак не лезу рефлекшеном внутрь библиотечных классов.
                Вы просто не понимаете суть ООП, если для вас добавление чего-то к библиотечному коду это обязательно означает разобрать объект на кусочки.


                1. mayorovp
                  22.07.2019 08:50

                  Простите, а как связаны pattern matching и залезание рефлексией внутрь чужих объектов?


                1. sshikov
                  22.07.2019 19:26

                  >Я прекрасно понимаю что это за паттерн и зачем он нужен.
                  Совершенно не похоже. Вы бы хоть википедию что-ли прочитали? В тамошней вполне приличной формулировке матчинг — это поиск паттерна в последовательности токенов . Не в объекте — это лишь частный случай, а в последовательности объектов. Типичный случай — дерево (ну или список из головы и хвоста).


            1. datacompboy
              21.07.2019 19:25
              +1

              примеры из статьи — искуственные и показывают что можно делать (но отнюдь не что нужно).

              сопоставление с образцом используется в конструировании потока управления данными, например. писать отдельный класс под каждый промежуточный вариант — та еще тухлятина.

              те же конечные автоматы гораздо удобнее получаются.

              еще хорошо для _лаконичных_ деконструкций. когда метод возвращает например два значения — и распаковка pair в две отдельных переменных это тоже сопоставление с образцом. особенно если нужна только одна из них дальше.

              да, всегда можно написать иначе. ну на то и сахар, чтоб вкуснее было.


              1. kommie2050
                21.07.2019 20:17

                Не было бы. Представьте на мгновение, что figure — это не ваши классы, что они написаны сторонним разработчиком, и менять (а то и посмотреть на их код) вы не можете.

                это делается через визитор. Вот такие свичи это практичеси всегда быдлокод.
                в конструировании потока управления данными,

                не очень оптимально использовать рефлекшн, потому что он дольше чем обычный доступ к переменным. Тут прикручивается стирание классов например Питона к яве. Так берите Питон для этой задачи.


                1. alex_blank
                  21.07.2019 21:19
                  +1

                  > это делается через визитор. Вот такие свичи это практичеси всегда быдлокод.


                  Визитор это и есть такой свитч, только obscured.


                  1. kommie2050
                    22.07.2019 14:10
                    -1

                    быдлокод в том что ты делаешь ветку вручную в то время как то же самое сделает для тебя компилятор такой фичей ОО как полиморфизм функций. Кроме того ручной свич работает в жаюе медленнее чем вызов функций — давным давно протестил. Я согласен что следует ввести другой синтаксис для визитора а не как сейчас через зад. Но это не оправдывает свич.


                    1. mayorovp
                      22.07.2019 14:12

                      быдлокод в том, что приходится писать все эти фунцкии вручную ради всего одного вызова, в то время как это мог бы сделать поддерживающий сопоставление с образцом компилятор


                1. sshikov
                  22.07.2019 19:29

                  Быдлокод оно именно потому, что язык не поддерживает матчинг как следует — например, не отлавливает, все ли возможные случаи были рассмотрены.

                  Ну и да, визитор — это неудобный (частный, для ООП) способ реализации матчинга, если угодно.


              1. pin2t
                22.07.2019 07:59

                примеры из статьи — искуственные и показывают что можно делать (но отнюдь не что нужно).

                Приведите нормальный пример, который доказывает нужность этой концепции и этой библиотеки. Пока что все абсолютно примеры доказывают её вредность и ненужность.
                Ну может вот этот только можно использовать
                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 ");
                };


              1. pin2t
                22.07.2019 08:12

                сопоставление с образцом используется в конструировании потока управления данными, например. писать отдельный класс под каждый промежуточный вариант — та еще тухлятина.

                Сопоставление с образцом используется в функциональных языках, потому что в них нет понятия инкапсуляции и типов (в том понимании в котором они используются в Java например).
                В Java да, именно что под каждый промежуточный вариант надо делать свой класс. Так принято в Java. Но ненадо в Java тащить чужеродные концепции. Если не умеете программировать на Java, и очень хочется использовать pattern matching — идите в Haskel, Lisp, Scala и т.п.


                1. datacompboy
                  22.07.2019 13:42
                  +1

                  Перевожу что вы сказали: «не трогайте то к чему я привык».
                  ОК, вам не нужен синтаксический сахар.
                  Я знаю как жавистам больно от введения «var».
                  Простите. Придётся учиться, энтерпрайз не повод продолжать писать как 50 лет назад.


                  1. sshikov
                    22.07.2019 19:32

                    >Я знаю как жавистам больно от введения «var».
                    Вот не надо обобщать. Это мнение одного человека, не более того. У меня вот энтерпрайз в чистом виде — и никто даже не задумается, применять ли матчинг, если он вдруг появится в Java (в Scala — уже).


  1. datacompboy
    20.07.2019 19:05
    +1

    Вообще по статье: гораздо интереснее читать как это было реализовано, какие накладыне расходы, что с совместимостью, какие требования.


    1. aleksandy
      20.07.2019 20:34

      Препроцессор какой-нибудь, типа ретролямбд, скорее всего.


      1. datacompboy
        20.07.2019 20:36
        +1

        тогда откуда странное ограничение на число веток? :)
        тут явно напрашивается «ненормальное программирование» в хабы и хотел бы прочесть как человек дошел до такой жизни.


        1. koowaah Автор
          23.07.2019 13:58

          Поскольку для разных вариантов параметров лямбды используются перегрузки метода matches().


  1. sshikov
    20.07.2019 21:34
    +1

    Было бы неплохо сравнить скажем с vavr, где почти все тоже самое уже есть довольно давно.


  1. jreznot
    20.07.2019 22:32

    BTW, давно (4 года уже) существует библиотека Motif: github.com/johnlcox/motif
    Она реализует Scala-like pattern matching для Java 8+


    1. koowaah Автор
      23.07.2019 13:55

      Motif библиотека использует Java Stream style.
      Эта библиотека предоставляет более простой и подобный к стилю написанию C# pattern matching. Не нужно писать when(), get() и другие методы.


  1. Ruins007
    20.07.2019 23:10
    +3

    У меня одного кат улетел?


    1. ALacroix
      21.07.2019 01:10
      -1

      me too


    1. Boomburum
      21.07.2019 01:10

      fixed )


  1. kommie2050
    21.07.2019 01:10

    только это не паттерн матчинг а инстанс валидатор.
    короче вроде универсального визитора? Достаёт значения через рефлекшн и конструирует реальные инстанции обьекта и сравнивает по equals? Ну так джава код и не предназначет для массивной сериализации инстанций)))
    боже какие замороты с тем что в питоне есть просто так.

    Притащенная соответственно функциональщиками, людьми, которые не знают что такое инкапсуляция.

    есть также принцип разделения функций. Я тоже взглянув с перва подумал что это быдлокод со свичами, заменяющими полиморфизм, но тут можно выделить произвольную проверку в отдельный класс — принцип визитора

    И кстати много кода не нужно. Тут в один небольшой класс поместится весь функционал вышеперечисленного, если с рефлекшном, а если без, то как вы достаёте поля?

    А всё, на гитхабе кто посмотрит тот поймёт что достают рефлекшном например через fields. Можно делать одним классом вроде кода ниже только расширить.
    package utils;
    
    import  java.lang.reflect.Field;
    import java.util.HashMap;
    import java.util.Iterator;
    import java.util.Map;
    
    public class AnyValidator {
    	
    	private Class lastClass = null;
    	
    	private String lastResult = "";
    	
    	private Object dummy = new Integer(10);
    	
    	
    	
    	
    	public boolean validateFields(Object instance, Map<String, Object> mp) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
    		
    		lastClass = instance.getClass();
    		
    		Iterator<Map.Entry<String, Object>> it = mp.entrySet().iterator();
    		
    		while (it.hasNext()) {
    			
    			Map.Entry<String, Object> entry = (Map.Entry<String, Object>)it.next();
    			
    			String fieldName = entry.getKey();
    			
    			Object goodValue = entry.getValue();
    			
    			boolean isGoodField = equalsObjectField(fieldName, instance, goodValue);
    			
    			if (! isGoodField) {
    				
    				lastResult = "validateFields: field " + fieldName + " is wrong";
    				return false;
    			}
    			
    			
    		}
    		return true;
    		
    		
    		
    	}
    	
    	public boolean equalsObjectField(String fieldName, Object instance, Object goodValue) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
    	
    		Field field = lastClass.getDeclaredField(fieldName);
    		
    		Object  fieldValue = field.get(instance);
    		
    		if (goodValue == null)
    				return fieldValue == null;
    		
    		return goodValue.equals(fieldValue);
    		
    		
    	}
    	
    	
    	
    	
    	public static void main(String[] args) {
    		
    		AnyValidator dummy = new AnyValidator();
    		
    		AnyValidator val = new AnyValidator();
    		
    		Map<String, Object> mp = new HashMap<String, Object> ();
    		
    		mp.put("dummy" ,  new Integer(10));
    		
    		mp.put("lastResult" ,  "");
    	
    		mp.put("lastClass" ,  null);
    		
    		
    		
    		
    		try {
    			boolean res = val.validateFields(dummy, mp);
    			
    			System.out.println(res);
    			
    			dummy.dummy = new Integer(11);
    			
    			res = val.validateFields(dummy, mp);
    					
    			System.out.println(res);
    			
    			
    		} catch (Exception e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		} 
    		
    	}
    
    }
    
    
    



  1. koowaah Автор
    21.07.2019 07:53

    Никакой магии под капотом нету. Используется рефлексия где нужно и те же if/else.
    Ограничения на количество веток вызвано тем что, что для каждого случая используется разные перегрузки функции matches().


  1. koowaah Автор
    21.07.2019 08:01

    По сравнению с if/else цепочкой, matches() методы под капотом делает тоже, но где нужно используют рефлексия.
    Накладные расходы добавляются с рефлексией, но без неё не как.
    Написано на чистой Java 8.


    1. PROgrammer_JARvis
      23.07.2019 13:43
      +1

      Не рассматривали, в качестве оптимизации, замену рефлексии возможностями java.lang.invoke, а, особенно, LambdaMetaFactory, т.е. генерацию на лету для вызовов методов реализаций неких функциональных интерфейсов? + кэширование соответствующих вещей
      Допустим, в Вашем DeconstructPattern вызов verifyExtractMethods(..) основывается на (для каждого вызова) анализе класса для поиска аннотированных методов (takeExtractMethods(..)) и дальнейшем их вызове через открытие доступа и рефлективый вызов. Вместо этого же можно было результат поиска метода кэшировать, причём для вызова самого метода генерируя объект функ. интерфейса, а ля


      interface Extractor {
          void extract(Object... parameters);
      } 

      или даже JDKшный Consumer<Object[]>,
      что позволило бы избавиться от постоянных накладных расходов на открытие доступа к рефлективной сущности, да и сделало код более явным.


      Как по мне, учитывая то, что эта библиотека, в первую очередь, синтаксический сахар, вопрос производительности тут критичен, потому что, даже принимая во внимание магию JIT, сахар с сравнительно большим оверхедом проигрывает более многословным, но примитивных, с точки зрения генерируемого байткода, способам достижения такой же логики.


      Если же углубляться в оптимизации, то можно попробовать генерировать в рантайме полностью свои сущности для доступа к каким-то вещам, или даже, используя Intrumentation API, при наличии соответствующего агента, заменять при загрузке засахаренных классов вызовы сложных методов байткодом подобного же, но без сахара (e.g, для всё того же Deconstructor'а производить, фактически, инструкции доступа к полям.


      В целом же необычных подход к добавлению сахара :)


      При вашем желании, могу накидать Issues на гитхабе


      1. koowaah Автор
        23.07.2019 13:48

        Идею использования java.lang.invoke, а, особенно, LambdaMetaFactory не рассматривал для оптимизации. Но обязательно попробую рассмотреть этот вариант.

        Можете более детально описать как именно можно закэшировать, вместо прямого поиска и вызова метода через рефлексию?

        На гитхаб можете писать Issues, буду рад.


        1. PROgrammer_JARvis
          24.07.2019 13:16

          Можете более детально описать как именно можно закэшировать, вместо прямого поиска и вызова метода через рефлексию?

          Что касается кэша, то, с точки зрения реализации, тут варианты любые, в зависимости от ваших же предпочтений (JDK: Map, Guava: Cache и т.д.). Я, скорее, говорил о подходе, что, учитывая, что у вас зачастую происходит вызов методов с одинаковыми параметрами, для которых происходит полный анализ класса, уместно результат этих самых вычислений сохранять для дальнейшего переиспользования.
          Для примера, тот самый <V> List<Method> takeExtractMethods(V value)уместно заменить на List<Method> getExtractMethods(Class<?> value) (простите мне моё переименование, не позволяет тут совесть take оставить :) ) (кстати, генерик тоже тут излишен), который будет анализировать содержимое класса (ваше нынешнее тело метода) только в том случае, если в кэше для данного класса это ещё не найдено, после чего это значение будет заноситься в этот самый кэш, дабы при повторном вызове не пришлось производить эту затратную операцию. С этой точки зрения особенно хороша, как по мне, реализация Guava, так как она и функциональна (V get(K, Callable<? extends V>), где K и V типы ключей и значений соотв-но) и позволяет удобно настраивать режим освобождения кэша.


          На гитхаб можете писать Issues, буду рад.

          Отлишно, постараюсь накидать в ближайшее время


          1. koowaah Автор
            24.07.2019 14:07

            Ок. Попробую реализовать кеш аналогичен Guava Cache.
            И внедрить в проект.


  1. koowaah Автор
    23.07.2019 10:17

    Вдохновениям для реализации библиотеки для паттерн матчинга стал С#. В С# 7,8 добавлены почти все эти паттерны. Как можно видеть паттерн матчинг прекрасно вписывается в С#.

    З другой стороны паттерн матчинг является одной с приоритетных направлений Java.
    Есть уже два jep:
    pattern matching for instanceof.
    pattern matching for switch.

    Но ждать прийдется какия минимум к LTS Java 17 а то и больше.