Java 17: что нового по сравнению с Java 11

Java 17: что нового по сравнению с Java 11

https://t.me/data_analysis_ml

Версия Java 17 была выпущена не так уж давно. Отличие этого релиза в том, что это — новая TLS-версия (Long Term Support, с долговременной поддержкой) после Java 11.

В этой статье рассмотрим новые практические функции, которые были введены между 11-ой и 17-ой версиями.

  1. Switch-выражения.
  2. Текстовые блоки.
  3. Сопоставление с образцом (Pattern Matching) для instanceof.
  4. Полезные NullPointerException.
  5. Записи (Records).
  6. Запечатанные (sealed) классы.
  7. Сопоставление с образцом для switch.

Switch-выражения

Switch-выражения — это оператор switch с улучшенным синтаксисом и функциональностью. Как они работают?

Выведем информацию о том, является ли данный день будним или выходным. При использовании операторов switch это выглядит так:

DayOfWeek dayOfWeek = // назначение значений
switch (dayOfWeek) {
    case SUNDAY:
    case SATURDAY:
        System.out.println("Weekend");
        break;
    case FRIDAY:
    case THURSDAY:
    case WEDNESDAY:
    case TUESDAY:
    case MONDAY:
        System.out.println("Weekday");
        break;
    default:
        System.out.println("Unknown Day!");
}

С помощью новых switch-выражений этот код можно переписать так:

System.out.println(switch (day) {
    case SUNDAY, SATURDAY -> "Weekend";
    case FRIDAY, THURSDAY, WEDNESDAY, TUESDAY, MONDAY -> "Weekday";
});

В чем разница?

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

Во-вторых, больше не нужно использовать ключевое слово break, чтобы остановить выполнение. При использовании switch-выражений выполняется только правая часть соответствующего случая, если применяется синтаксис со стрелкой (->). 

В-третьих, поскольку операторы switch стали switch-выражениями, а выражения вычисляют значение, то теперь они могут возвращать значение.

В-четвертых, выражения switch являются исчерпывающими. Если вы забудете указать случай в выражении switch, то получите ошибку во время компиляции. Если вы охватываете все случаи, вам не нужно иметь случай “по умолчанию”.

В-пятых, если необходимо выполнить блок кода, то поддерживается следующее:

System.out.println(switch (day) {
    case SUNDAY, SATURDAY -> "Weekend";
    case FRIDAY, THURSDAY, WEDNESDAY, TUESDAY, MONDAY -> {
        // какая-то логика
        yield "Weekday";
    }
});

Просто откройте блок, выполните необходимые действия и верните значение, воспользовавшись ключевым словом yield.


Текстовые блоки

Текстовые блоки — это просто многострочные строковые литералы.

Чтобы поместить в код такой JSON:

{
   "name":"fatih",
   "surname":"iver",
   "birthYear":1996
}

Нужно отформатировать его таким образом:

String json = "{\n" +
        "   \"name\":\"fatih\",\n" +
        "   \"surname\":\"iver\",\n" +
        "   \"birthYear\":1996\n" +
        "}";

С помощью текстовых блоков можно просто написать следующее:

String json = """
        {
           "name":"fatih",
           "surname":"iver",
           "birthYear":1996
        }
        """;

С текстовыми блоками больше не нужно экранировать кавычки или объединять несколько линий, чтобы сформировать одну строку.

Особенно это облегчает жизнь, если вы храните HTML, JSON, SQL и XML в исходном коде.

Новые текстовые блоки делают код более красивым и простым для чтения.


Сопоставление с образцом (Pattern Matching) для instanceof

instanceof позволяет проверить, принадлежит ли объект к нужному типу. Если это так, мы приводим этот объект к желаемому типу, чтобы вызвать метод, доступный только для этого типа. Пример:

Object o = "Hello, World!";

if (o instanceof String) {
    String s = (String) o;
    System.out.println(s.length());
}

При сопоставлении с образцом не нужно явное приведение типов:

Object o = "Hello, World!";

if (o instanceof String s) {
    System.out.println(s.length());
}

Вы даже можете использовать переменную s, как это показано ниже, если выражение instanceof принимает значение true:

Object o = "Hello, World!";

if (o instanceof String s && !s.isBlank()) {
    System.out.println(s.toLowerCase());
}

Полезные NullPointerException

При выполнении следующего кода:

Map<String, String> map = new HashMap<>();
System.out.println(map.get("key").toLowerCase());

Мы получим:

Exception in thread "main" java.lang.NullPointerException
 at Main.example(Main.java:14)
 at Main.main(Main.java:10)

Фрагмент лишь сообщает, в какой строке произошло исключение. При этом ничего не сказано о том, приняла ли карта значение null или же в ней не оказалось нужного ключа.

С новым улучшением мы получаем:

Exception in thread "main" java.lang.NullPointerException: 
Cannot invoke "String.toLowerCase()" because the return value of "java.util.Map.get(Object)" is null
 at helpful_null_pointer_exception.Main.example(Main.java:15)
 at helpful_null_pointer_exception.Main.main(Main.java:9)

Сообщение говорит, где и почему произошло NPE. 


Записи

Чтобы определить неизменяемый класс, можно действовать так:

public class Person {

    private final String name;
    private final String surname;
    private final Integer age;

    public Person(String name, String surname, Integer age) {
        this.name = name;
        this.surname = surname;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public String getSurname() {
        return surname;
    }

    public Integer getAge() {
        return age;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (!Objects.equals(name, person.name)) return false;
        if (!Objects.equals(surname, person.surname)) return false;
        return Objects.equals(age, person.age);
    }

    @Override
    public int hashCode() {
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + (surname != null ? surname.hashCode() : 0);
        result = 31 * result + (age != null ? age.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", surname='" + surname + '\'' +
                ", age=" + age +
                '}';
    }
}

Получается довольно много строк. Упростим синтаксис, воспользовавшись библиотекой Lombok:

@Value
public class Person {
    String name;
    String surname;
    Integer age;
}

Не слишком расписывая код и не завися от стороннего решения, с помощью записей можно написать этот класс следующим образом:

public record Person(String name, String surname, Integer age) {
 
}

Тип record создает неизменяемый класс с приватными финализированными полями, геттерами, методом toString, а также методами equals и hashCode.

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


Запечатанные классы

При помощи этой функции класс может указывать, какие классы могут его расширять. Например:

public abstract sealed class Developer permits BackendDeveloper, FrontendDeveloper {
}

Следующий код не будет компилироваться с приведенным выше определением:

public final class ProductManager extends Developer {
}

Этот код не компилируется, потому что класс Developer разрешает расширять себя только классам BackendDeveloper и FrontendDeveloper. Однако его пытается расширить класс ProductManager. И возникает ошибка компиляции.

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


Сопоставление с образцом для switch

Для этого объяснения используется пример с сайта OpenJDK.

Вот код:

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:

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

Мы сопоставили объект на основе его типа и не выполняли никакого явного приведения.

Эту функцию можно объединить с запечатанными классами. Так получится писать точные и довольно емкие switch-выражения.


Бонус

До Java 16 мы собирали потоки в виде списка, как показано ниже:

List<Integer> digits = List.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenDigits = digits.stream()
        .filter(digit -> digit % 2 == 0)
        .collect(Collectors.toList());

Теперь для той же цели можно вызвать новый метод ToList для потоков:

List<Integer> digits = List.of(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
List<Integer> evenDigits = digits.stream()
        .filter(digit -> digit % 2 == 0)
        .toList();

https://t.me/javatg

источник

Report Page