14-го сентября состоялась презентация Apple, в этот же день произошло не менее важное событие - релиз Java 17.

Среди новых фич подъехал паттерн матчинг для switch в preview моде JEP 406.

История началась с того, что в jdk 16 расширили instanceof оператор, который теперь может принимать type pattern и выполнять матчинг по паттерну. Это маленькое изменение позволило упростить типичную конструкцию с проверкой на тип и последующее приведение.

// before
if (o instanceof String) {
    String s = (String)o;
    ... use s ...
}

// after
if (o instanceof String s) {
    ... use s ...
}

Обычно проверка производится на совпадение среди нескольких типов и пример показывает насколько код далек от идеала.

static String formatter(Object o) {
    String formatted = "unknown";
    if (o instanceof Integer i) {
        formatted = String.format("int %d", i);
    } else if (o instanceof Long l) {
        formatted = String.format("long %d", l);
    } else if (o instanceof Double d) {
        formatted = String.format("double %f", d);
    } else if (o instanceof String s) {
        formatted = String.format("String %s", s);
    }
    return formatted;
}

Для таких операций идеально подходил бы switch, но в силу ограниченности поддержки типов и сравнения только на соответствие константному значению, приходится использовать цепочку if else.

Разработчики подумали над ситуацией и добавили ряд улучшений:

  • возможность работы с любым типом

  • проверка на соответствие паттерну в case

  • возможность обрабатывать null значения через встроенный case

Теперь предыдущий код выглядит так:

static String formatterPatternSwitch(Object o) {
    return switch (o) {
        case Integer i -> String.format("int %d", i);
        case Long l    -> String.format("long %d", l);
        case Double d  -> String.format("double %f", d);
        case String s  -> String.format("String %s", s);
        default        -> o.toString();
    };
}

Зачастую, после совпадения типа, нужно делать дополнительные проверки и описывать их внутри, что может приводить к раздуванию кода:

static void test(Object o) {
        switch (o) {
            case Integer i:
                if (i.intValue() > 100) { ... }
                if (i.intValue() > 3 && i.intValue() < 7) { ... }
            .......
        }
    }

Для покрытия таких кейсов были введены 2 новых вида паттернов:

  • guarded patterns в формате type pattern && boolean expression, которые позволяют дополнять матчинг по типу boolean выражением

  • parenthesized patterns, которые позволяют избегать неочевидности при формировании логики из нескольких boolean

static void test(Object o) {
        switch (o) {
            case Integer i && i.intValue() > 100 -> { ...}
            case (Integer i && i.intValue() > 3) && (i.intValue() < 7) -> { ...}         
            .......
        }
    }

Обработка null

Традиционно, switch выбрасывал NullPointerException если проверяемый объект был null. Проверку необходимо было реализовывать за пределами блока.

static void testFooBar(String s) {
    if (s == null) {
        System.out.println("oops!");
        return;
    }
    switch (s) {
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

Это имело смысл в рамках ограниченной поддержки типов. Но, так как теперь switch работает с любым типом, а case поддерживают паттерны, разработчики добавили total type pattern, с помощью которого можно обработать ситуацию с null.

static void testFooBar(String s) {
    switch (s) {
        case null         -> System.out.println("Oops");
        case "Foo", "Bar" -> System.out.println("Great");
        default           -> System.out.println("Ok");
    }
}

Планы на будущее

  • поддержка примитивных типов

  • general классы смогут объявлять deconstruction паттерны для указания как они могут быть сматчены

  • поддержка AND and OR паттернов

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


  1. tonhead
    21.09.2021 16:09
    +1

    Это прекрасно. Осталось только прод мигрировать с 8 версии

    Upd: вроде можно же писать код для версии языка X и компилировать в версию языка Y, где X > Y?


    1. dbudim Автор
      21.09.2021 17:47

      Да, 8ка наше все. Думаю что очень много проектов еще на ней.

      Кстати, в свое время мигрировали с 8 на 11 без особых проблем)


    1. aleksandy
      21.09.2021 20:14
      +1

      Можно, но это путь ко всевозможным NoClassDefFoundError, NoSuchFieldError, NoSuchMethodError и прочим LinkageError-ам.


    1. Maccimo
      22.09.2021 01:23

      Можно взять для этого Jabel от bsideup.
      Для pattern matching такой вариант не подойдёт так как нужна поддержка от стандартной библиотеки (см. SwitchBootstraps), но для чистого синтаксического сахара а-ля многострочные строковые литералы — вполне.


  1. MIKHTheCreator
    21.09.2021 20:58
    +1

    Определенно "годное" расширение возможностей Java. Вопрос только: насколько все это приживется, и как скоро перейдут на использование Java 17, после многолетнего использование Java8?

    Ну а так, 17 версия Java внесла немало полезных возможностей и неплохую оптимизацию.


  1. Coocos
    22.09.2021 00:05

    Preview и LTS немного противоречат друг другу.


    1. kacetal
      22.09.2021 04:11

      Ну через два года будет новый LTS, думаю к нему вполне успеют довести ее до конца, а может ещё и дистракчеринг добавят.


  1. vba
    22.09.2021 15:18
    -1

    Java 17: Pattern Matching for switch

    Лично у меня язык не поднимается назвать это сопоставлением с образцом, так, немного адекватный switch и все. Ведь тут чего не хватает? Конструкции сопоставления с образцом есть суть выражения(на уровне семантики языка), а switch в Java никак к выражениям не относится. Вот и получилось опять невесть что, как у последователей карго-культа.


    1. vba
      22.09.2021 15:30

      Хм, как то я и не заметил что все таки, в отличие от C#(<8.0) в Java это скорее сопоставление с образцом:

      Java:

      static String formatterPatternSwitch(Object o) {
      return switch (o) {
      case Integer i -> //....;
      default -> o.toString();
      };
      }

      C#:
      switch (о) {
      case Integer i:
      .....;
      break;
      default:
      ......;
      break;
      }

      Хотя вроде в C# 8.0 это все поправят и введут выражения. Интересно.


    1. Maccimo
      22.09.2021 20:15
      +1

      Switch expressions добавили ещё раньше, в рамках JEP 361.
      И даже в рамках этой короткой статьи они встречаются:


      static String formatterPatternSwitch(Object o) {
          return switch (o) {

      Вот и получилось опять невесть что, как у последователей карго-культа.

      Дружно бьём в бубен.


  1. pim_jewel
    11.10.2021 17:11

    case (Integer i && i.intValue() > 3) && (i.intValue() < 7) -> { ...} 

    case (Integer i && i.intValue() > 3) && (i.intValue() < 7) -> { ...}

    Это точно? Или должно быть

    case Integer i && (i.intValue() > 3 && i.intValue() < 7) -> { ...} 

    ?


    1. dbudim Автор
      11.10.2021 17:12

      Все верно, можете посмотреть JEP: 406 секцию Guarded and parenthesized patterns


      1. pim_jewel
        11.10.2021 17:17

        JEP: 406 секцию Guarded and parenthesized patterns

        GuardedPattern: PrimaryPattern && ConditionalAndExpression

        В данном случае

        PrimaryPattern = Integer i

        ConditionalAndExpression = (i.intValue() > 3 && i.intValue() < 7)

        Значит получаем так, как я написал:

        case Integer i && (i.intValue() > 3 && i.intValue() < 7) -> { ...} 


        1. dbudim Автор
          11.10.2021 17:23

          We also change the grammar for instanceof expressions to:

          InstanceofExpression:
            RelationalExpression instanceof ReferenceType
            RelationalExpression instanceof PrimaryPattern

          This change, and the non-terminal ConditionalAndExpression in the grammar rule for a guarded pattern, ensure that, for example, the expression e instanceof String s && s.length() > 1 continues to unambiguously parse as the expression (e instanceof String s) && (s.length() > 1). If the trailing && is intended to be part of a guarded pattern then the entire pattern should be parenthesized, e.g., e instanceof (String s && s.length() > 1).


          i.intValue() > 3 в данном примере является частью guarded паттерна (Integer i && i.intValue() > 3)


          1. pim_jewel
            11.10.2021 18:25

            На самом деле, что в лоб, что по лбу:

            public class GuardedSwitch {
                public static void main( String[] args ) {
                    switchMethod( null );
                    switchMethod( 1 );
                    switchMethod( 4 );
                    switchMethod( 7 );
                    switchMethod( 10 );
                    switchMethod( "Hello, world!" );
                }
            
                private static void switchMethod( Object a ) {
                    System.out.println( switch( a ) {
                        case null -> "NULL";
                        case Integer i && ( i > 3 && i < 7) -> "(3 .. 7)";
                        case ( Integer i && i >= 7) && ( i < 9 ) -> "[7 .. 9)";
                        case Integer i && i >= 0 && i <= 3 -> "[0 .. 3]";
                        case Integer i -> "Another Integer = " + i;
                        default -> "Another object type: " + a.getClass().getTypeName() + " = " + a;
                    } );
                }
            }

            Вывод:

            NULL
            [0 .. 3]
            (3 .. 7)
            [7 .. 9)
            Another Integer = 10
            Another object type: java.lang.String = Hello, world!