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.

    @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. Буду рад отзывам, предложениям по улучшению.

    Средняя зарплата в IT

    120 000 ₽/мес.
    Средняя зарплата по всем IT-специализациям на основании 5 953 анкет, за 1-ое пол. 2021 года Узнать свою зарплату
    Реклама
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее

    Комментарии 11

      0
      Сто лет существует замечательная библиотека github.com/johnlcox/motif
      Maven: com.leacox.motif:motif:0.1
      john.leacox.com/motif
        0
        В библиотеке motif используется похожее на java stream цепочки. В этой библиотеке используется простые функции (статические методы) более приближенный вид к switch.
        0
        Много орфографических ошибок в тексте. «можна», «перемен», «на много». Видимо торопились донести ))

        По теме, очень интересно. Ява сильно ускорилась в последние годы. Клиенты еще сильно отстают. Год назад только на восьмерку переехали и видимо еще лет девять так и будет. расширенный LTS до 2030.

        Но мне интересно, что станет с языком к 2030. Будет слияние всех языков работающих на JVM в один?
          0
          >Будет слияние всех языков работающих на JVM в один?
          Java со Scala, и кложа за компанию? Как вы себе это представляете, и нафига бы это было нужно?
            0
            Бог его знает. Я имею ввиду, что языки перенимают друг у друга синтаксис и становятся все больше и больше похожи друг на друга. Чем они будут отличаться друг от друга через десять лет?
              0
              Ну, я бы не сказал, что это везде так и со всеми языками. Ну да, Java вбирает какие-то свойства (текстовые блоки из груви и скалы) — но в тоже время скоро будет Scala 3, которая снова будет отличаться. Кложа вообще не похожа ничем. Хаскель опять же есть под JVM — он тоже мало чем похож. Груви с Java всегда и были похожи сильно — вплоть до того, что валидный Java код вроде бы был и валидным груви кодом одновременно (не помню, как сейчас).
          +2

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

            0

            А на каком языке примеры?
            в моей Java 8 так написать нельзя


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

            будет ошибка на первой ;

              0

              В конце визова println() нужно ставит запятую ",".
              Как параметры функции.
              Спасибо за замечения поправлю в статье.

                +1

                А точно этот пример работает даже с запятыми?
                А то я вижу только 2 параметра у метода org.kl.jpml.pattern.ConstantPattern::as
                и ноль перегрузок
                Или это только задумка?

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое