Pull to refresh

Возможности Java — от Java 8 до Java 17

Reading time19 min
Views29K
Original author: Mateo Stjepanović

С момента появления в 1995 году до сегодняшнего дня в Java многое изменилось. Java 8 была революционным выпуском, вернувшим Java на пьедестал лучших языков программирования.

Мы рассмотрим большинство изменений в языке Java, которые произошли с Java 8 в 2014 году до сегодняшнего дня. Мы постараемся быть как можно более краткими по каждой функции. Намерение состоит в том, чтобы иметь ссылку на все новые фичи языка Java версий 8 - 17 включительно.

Эта статья сопровождается примером рабочего кода на GitHub.

Эта статья переведена по просьбе одного из читателей Хабр. Надеюсь она будет полезна как краткий справочник по новым фичам языка Java 8 - 17.

Java 8

Основные изменения в выпуске Java 8:

  • Лямбда-выражения и Stream API

  • Ссылка на метод

  • Методы по умолчанию

  • Аннотации типов

  • Повторяющиеся аннотации

Лямбда-выражения и Stream API

Java всегда была известна наличием большого количества шаблонного кода. С выпуском Java 8 это утверждение стало немного менее актуальным. Stream API и лямбда-выражения - это новые возможности, которые приближают нас к функциональному программированию.

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

Мир до лямбда-выражений

Допустим у нас есть автосалон. Чтобы избавиться от всей бумажной работы, мы хотим создать программу, которая находит все доступные в настоящее время автомобили с пробегом менее 50 000 км.

Давайте посмотрим, как наивно реализовать функцию для чего-то вроде этого:

public class LambdaExpressions {
    public static List<Car> findCarsOldWay(List<Car> cars) {
        List<Car> selectedCars = new ArrayList<>();
        for (Car car : cars) {
            if (car.kilometers < 50000) {
                selectedCars.add(car);
            }
        }
        return selectedCars;
    }
}

Чтобы реализовать ее, мы создаем статическую функцию, которая принимает список автомобилей List<Car>. Он должен возвращать отфильтрованный список в соответствии с указанным условием.

Использование потока и лямбда-выражения

У нас та же проблема, что и в предыдущем примере.

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

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

public class LambdaExpressions {
    public static List<Car> findCarsUsingLambda(List<Car> cars) {
        return cars.stream().filter(car -> car.kilometers < 50000)
                .collect(Collectors.toList());
    }
}

Нам нужно передать список машин в поток, вызвав метод stream(). Внутри метода filter() мы устанавливаем наше условие. Мы сравниваем каждую запись с желаемым условием. Мы сохраняем только те записи, которые имеют пробег менее 50 000 километров. Последнее, что нам нужно сделать, это свернуть результат в список.

Подробнее о лямбда-выражениях можно найти в документации.

Ссылка на метод

Без ссылки на метод

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

Ссылка на метод позволяет нам вызывать функции в классах, используя особый синтаксис ::. Есть четыре вида ссылок на методы:

  • Ссылка на статический метод

  • Ссылка на метод экземпляра объекта

  • Ссылка на метод экземпляра для типа

  • Ссылка на конструктор

Давайте посмотрим, как это сделать с помощью стандартного вызова метода:

public class MethodReference {
    List<String> withoutMethodReference =
            cars.stream().map(car -> car.toString())
                    .collect(Collectors.toList());
}

Мы используем лямбда-выражение для вызова метода toString() на каждого объекта car.

Использование ссылки на метод

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

public class MethodReference {
    List<String> methodReference = cars.stream().map(Car::toString)
            .collect(Collectors.toList());
}

Мы снова используем лямбда-выражение, но теперь мы вызываем метод toString() по ссылке на метод. Мы видим, насколько он лаконичнее и легче читается.

Чтобы узнать больше о справочнике по методам, посмотрите документацию.

Методы по умолчанию

Представим, что у нас есть простой метод, log(String message), который при вызове выводит сообщения журнала. Мы поняли, что хотим предоставлять сообщениям отметки времени, чтобы в журналах было легко найти их. Мы не хотим, чтобы наши клиенты перестали работать после того, как мы внесем это изменение. Мы сделаем это, используя реализацию метода по умолчанию в интерфейсе.

Реализация метода по умолчанию - это функция, которая позволяет нам создать резервную реализацию метода интерфейса.

Сценарий использования

Посмотрим, как выглядит наш контракт:

public class DefaultMethods {

    public interface Logging {
        void log(String message);
    }

    public class LoggingImplementation implements Logging {
        @Override
        public void log(String message) {
            System.out.println(message);
        }
    }
}

Мы создаем простой интерфейс с помощью всего одного метода и реализуем его в классе LoggingImplementation.

Добавление нового метода

Мы добавим новый метод внутрь интерфейса. Метод принимает второй аргумент с именем date, который представляет отметку времени.

public class DefaultMethods {

    public interface Logging {
        void log(String message);
        
        void log(String message, Date date);
    }
}

Мы добавляем новый метод, но не реализуем его во всех клиентских классах. Компилятор выдаст исключение:

Class 'LoggingImplementation' must either be declared abstract 
or implement abstract method 'log(String, Date)' in 'Logging'`.

Использование методов по умолчанию

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

Давайте посмотрим, как создать реализацию метода по умолчанию:

public class DefaultMethods {

    public interface Logging {
        void log(String message);

        default void log(String message, Date date) {
            System.out.println(date.toString() + ": " + message);
        }
    }
}

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

Чтобы узнать больше о методах по умолчанию, обратитесь к документации.

Аннотации типов

Аннотации типов - еще одна функция, представленная в Java 8. Несмотря на то, что и раньше у нас были аннотации, теперь мы можем использовать их везде, где используется тип. Это означает, что мы можем использовать их при:

  • определении локальной переменной

  • вызове конструктора

  • приведении типов

  • в дженериках

  • в throw и многое другое

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

Определение локальной переменной

Давайте посмотрим, как сделать так, чтобы наша локальная переменная не получала значение null:

public class TypeAnnotations {

    public static void main(String[] args) {
        @NotNull String userName = args[0];
    }
}

Здесь мы используем аннотацию в определении локальной переменной. Обработчик аннотаций времени компиляции теперь может читать аннотацию @NotNull и выдавать ошибку, если переменной присваивается значение NULL.

Вызов конструктора

Мы хотим убедиться, что мы не можем создать пустой ArrayList:

public class TypeAnnotations {

    public static void main(String[] args) {
        List<String> request =
                new @NotEmpty ArrayList<>(Arrays.stream(args).collect(
                        Collectors.toList()));
    }
}

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

Generic тип

Пусть одно из наших требований - каждый электронный адрес должен быть в формате <name>@<company>.com. Если мы используем аннотации типов, мы можем сделать это легко:

public class TypeAnnotations {

    public static void main(String[] args) {
        List<@Email String> emails;
    }
}

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

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

Дополнительные сведения об аннотациях типов см. в документации.

Повторяющиеся аннотации

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

Повторяющиеся аннотации позволяют нам размещать несколько аннотаций в одном классе.

Создание повторяющейся аннотации

В этом примере мы собираемся создать повторяющуюся аннотацию под названием @Notify:

public class RepeatingAnnotations {
    
    @Repeatable(Notifications.class)
    public @interface Notify {
        String email();
    }

    public @interface Notifications {
        Notify[] value();
    }
}

Мы создаем обычную аннотацию @Notify, но добавляем к ней (мета) аннотацию @Repeatable. Кроме того, мы должны создать аннотацию «контейнер» Notifications, содержащую массив объектов Notify. Обработчик аннотаций теперь может получить доступ ко всем повторяющимся аннотациям Notify через аннотацию контейнера Noifications.

Обратите внимание, что это фиктивная аннотация только для демонстрационных целей. Эта аннотация не будет отправлять электронные письма без процессора аннотаций, который читает ее, а затем отправляет электронные письма.

Использование повторяющихся аннотаций

Мы можем добавить повторяющуюся аннотацию несколько раз к одной и той же конструкции:

@Notify(email = "admin@company.com")
@Notify(email = "owner@company.com")
public class UserNotAllowedForThisActionException
        extends RuntimeException {
    final String user;

    public UserNotAllowedForThisActionException(String user) {
        this.user = user;

    }
}

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

Чтобы узнать больше о повторяющихся аннотациях, обратитесь к документации.

Java 9

В Java 9 представлены следующие основные функции:

  • Система модулей Java

  • Try-with-resources

  • Diamond оператор для анонимных внутренних классов

  • Private методы в интерфейсах

Система модулей Java

Модуль - это группа пакетов, их зависимости и ресурсы. Он предоставляет более широкий набор функций, чем пакеты.

При создании нового модуля нам необходимо предоставить несколько атрибутов:

  • Имя

  • Зависимости

  • Публичные пакеты - по умолчанию все пакеты являются private модулями.

  • Предлагаемые сервисы

  • Потребляемые сервисы

  • Разрешения на рефлексию

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

Создание модулей внутри IntelliJ IDEA

Сначала рассмотрим простой пример. Мы создадим приложение Hello World, в котором мы напечатаем «Hello» из одного модуля и вызываем второй модуль, чтобы вывести «World!».

Поскольку я работаю в IntelliJ IDEA, нам нужно кое-что понять в первую очередь. IntelliJ IDEA имеет концепцию модулей. Поэтому каждый модуль Java должен соответствовать одному модулю IntelliJ.

Структура пакета
Структура пакета

У нас есть два Java модуля: hello.module и world.module. Они соответствуют IntelliJ модулям hello и world соответственно. Внутри каждого из них мы создали файл module-info.java. Этот файл определяет наш Java-модуль. Внутри мы объявляем, какие пакеты нам нужно экспортировать и от каких модулей мы зависим.

Определение нашего первого модуля

Мы используем модуль hello для печати слова: «Hello». Внутри мы вызываем метод внутри модуля world, который выведет «World!». Первое, что нам нужно сделать, это объявить внутри module-info.java экспорт пакета, содержащего наш класс World.class:

module world.module {
    exports com.reflectoring.io.app.world;
}

Мы используем ключевое слово module с именем модуля для ссылки на модуль.

Следующее ключевое слово, которое мы используем, - это exports. Он сообщает модульной системе, что мы делаем наш пакет com.reflectoring.io.app.world видимым за пределами нашего модуля.

Можно использовать еще несколько ключевых слов:

  • requires

  • requires transitive

  • exports to

  • uses

  • provides with

  • open

  • opens

  • opens to

Из них мы покажем только декларацию requires. Остальные можно найти в документации.

Определение нашего второго модуля

После того, как мы создали и экспортировали модуль world, мы можем приступить к созданию модуля hello:

module hello.module {
    requires world.module;
}

Мы определяем зависимости с помощью ключевого слова requires. Мы ссылаемся наш вновь созданный модуль hello.module. Пакеты, которые не экспортируются, по умолчанию являются private для модуля и не могут быть видимы извне модуля.

Чтобы узнать больше о модульной системе Java, обратитесь к документации.

Try-with-resources

Try-with-resources - это фича, которая позволяет нам объявлять новые автоматически закрываемые ресурсы в блоке try-catch. Объявление их внутри блока try-catch указывает JVM освободить их после выполнения кода. Единственное условие - декларированный ресурс реализует интерфейс Autoclosable.

Закрытие ресурса вручную

Мы хотим читать текст, используя BufferedReaderBufferedReader является закрываемым ресурсом, поэтому нам нужно убедиться, что он правильно закрыт после использования. До Java 8 мы делали это так:

public class TryWithResources {
    public static void main(String[] args) {
        BufferedReader br = new BufferedReader(
                new StringReader("Hello world example!"));
        try {
            System.out.println(br.readLine());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

В блоке finally мы бы вызывали close(). Блок finally гарантирует, что reader всегда правильно закрыт.

Закрытие ресурса с помощью try-with-resources

В Java 8 появилась фича try-with-resource, которая позволяет нам декларировать наш ресурс внутри блока try. Это гарантирует, что наш закрываемый объект будет закрыт без использования finally

Примечание переводчика. В оригинале ошибка. Оператор try-with-resources появился в Java 7. См. Java SE 7 Features and Enhancements.

В Java 9 разрешено использовать final переменные в качестве ресурсов в операторе try-with-resources. См. What’s New for the Java Language in JDK 9

Давайте посмотрим на пример использования BufferedReader для чтения строки:

public class TryWithResources {
    public static void main(String[] args) {
        final BufferedReader br3 = new BufferedReader(
                new StringReader("Hello world example3!"));
        try (BufferedReader reader = br3) {
            System.out.println(reader.readLine());
        } catch (IOException e) {
            System.out.println("Error happened!");
        }
    }
}

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

Чтобы узнать больше о try-with-resources, обратитесь к документации.

Diamond оператор для анонимных внутренних классов

До Java 9 мы не могли использовать ромбовидный оператор (<>) во внутреннем анонимном классе.

В нашем примере мы создадим абстрактный класс StringAppender. У класса есть только один метод, который добавляет две строки с разделителем - между ними. Мы будем использовать анонимный класс для реализации метода append():

public class DiamondOperator {

    StringAppender<String> appending = new StringAppender<>() {
        @Override
        public String append(String a, String b) {
            return new StringBuilder(a).append("-").append(b).toString();
        }
    };
    
    public abstract static class StringAppender<T> {
        public abstract T append(String a, String b);
    }
}

Мы используем ромбовидный оператор, чтобы опустить тип при вызове конструктора new StringAppender<>(). Если мы используем Java 8, в этом примере мы получим ошибку компилятора:

java: cannot infer type arguments for 
com.reflectoring.io.java9.DiamondOperator.StringAppender<T>

reason: '<>' with anonymous inner classes is not supported in -source 8
    (use -source 9 or higher to enable '<>' with anonymous inner classes)

В Java 9 эта ошибка компилятора больше не возникает.

Private методы в интерфейсах

Мы уже упоминали, как мы используем методы по умолчанию в интерфейсах.

Как разделить реализацию на несколько методов? При работе с классами мы можем добиться этого с помощью private методов. Может ли это быть выходом в нашем случае?

Что касается Java 9, да. Мы можем создавать private методы внутри интерфейсов.

Использование private метода в интерфейсе

В нашем примере мы хотим напечатать набор имен.

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

public class PrivateInterfaceMethods {

    public static void main(String[] args) {
        TestingNames names = new TestingNames();
        System.out.println(names.fetchInitialData());
    }

    public static class TestingNames implements NamesInterface {
        public TestingNames() {
        }
    }

    public interface NamesInterface {
        default List<String> fetchInitialData() {
            try (BufferedReader br = new BufferedReader(
                    new InputStreamReader(this.getClass()
                            .getResourceAsStream("/names.txt")))) {
                return readNames(br);
            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }

        private List<String> readNames(BufferedReader br)
                throws IOException {
            ArrayList<String> names = new ArrayList<>();
            String name;
            while ((name = br.readLine()) != null) {
                names.add(name);
            }
            return names;
        }
    }
}

Мы используем BufferedReader для чтения файла, содержащего имена по умолчанию, которыми мы делимся с клиентом. Чтобы инкапсулировать наш код и, возможно, сделать его повторно используемым в других методах, мы решили переместить код для чтения и сохранения имен в List в отдельный метод. Этот метод является private, и теперь мы можем использовать его где угодно в нашем интерфейсе.

Как уже упоминалось, основным преимуществом этой функции внутри Java 9 является лучшая инкапсуляция и возможность повторного использования кода.

Java 10

Вывод типа локальной переменной

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

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

Тип var позволяет нам опускать тип в левой части наших операторов.

Старый способ

Давайте рассмотрим пример. Мы хотим создать небольшую группу людей, поместить все в один список, а затем просмотреть этот список в цикле for и распечатать имя и фамилию:

public class LocalTypeVar {

    public void explicitTypes() {
        Person Roland = new Person("Roland", "Deschain");
        Person Susan = new Person("Susan", "Delgado");
        Person Eddie = new Person("Eddie", "Dean");
        Person Detta = new Person("Detta", "Walker");
        Person Jake = new Person("Jake", "Chambers");

        List<Person> persons =
                List.of(Roland, Susan, Eddie, Detta, Jake);

        for (Person person : persons) {
            System.out.println(person.name + " - " + person.lastname);
        }
    }
}

Это код, который мы можем видеть в большинстве случаев в Java. Мы используем явные типы, чтобы убедиться, что мы знаем, чего ожидает метод.

Неявный тип с var

Теперь мы рассмотрим тот же пример, но с использованием ключевого слова var, введенного в Java 10. Мы по-прежнему хотим создать несколько объектов Person и поместить их в список. После этого мы просмотрим этот список и распечатаем имя каждого человека:

public class LocalTypeVar {

    public void varTypes() {
        var Roland = new Person("Roland", "Deschain");
        var Susan = new Person("Susan", "Delgado");
        var Eddie = new Person("Eddie", "Dean");
        var Detta = new Person("Detta", "Walker");
        var Jake = new Person("Jake", "Chambers");

        var persons = List.of(Roland, Susan, Eddie, Detta, Jake);

        for (var person : persons) {
            System.out.println(person.name + " - " + person.lastname);
        }
    }
}

Мы видим несколько наиболее типичных примеров использования типа var для локальных переменных. Во-первых, мы используем их для определения локальных переменных. Это может быть отдельный объект или даже список с ромбовидным оператором.

Дополнительные сведения о выводе типа локальной переменной см. в документации.

Java 11

Вывод типа локальной переменной в лямбда-выражениях

В Java 11 внесены улучшения в ранее упомянутый вывод типа локальной переменной. Они позволяют нам использовать тип var внутри лямбда-выражения.

Мы снова создадим несколько Person, соберем их в список и отфильтруем записи, в имени которых нет буквы «а»:

public class LocalTypeVarLambda {

    public void explicitTypes() {
        var Roland = new Person("Roland", "Deschain");
        var Susan = new Person("Susan", "Delgado");
        var Eddie = new Person("Eddie", "Dean");
        var Detta = new Person("Detta", "Walker");
        var Jake = new Person("Jake", "Chambers");

        var filteredPersons =
                List.of(Roland, Susan, Eddie, Detta, Jake)
                        .stream()
                        .filter((var x) -> x.name.contains("a"))
                        .collect(Collectors.toList());
        System.out.println(filteredPersons);
    }
}

Внутри метода filter() мы используем var для вывода типа переменной вместо явного упоминания типа.

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

Java 14

Switch выражения

Switch выражения позволили нам опускать break внутри каждого case блока. Это помогает улучшить читаемость и понимание кода.

В этом разделе мы увидим несколько способов использования Switch выражений.

Старый способ c оператором Switch

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

Первое, что приходит в голову, - это построить его с помощью оператора switch-case:

public class SwitchExpression {

    public static void main(String[] args) {
        int days = 0;
        Month month = Month.APRIL;

        switch (month) {
            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER :
                days = 31;
                break;
            case FEBRUARY :
                days = 28;
                break;
            case APRIL, JUNE, SEPTEMBER, NOVEMBER :
                days = 30;
                break;
            default:
                throw new IllegalStateException();
        }
    }
}

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

Использование Switch выражений

Мы рассмотрим тот же пример, что и раньше. Пользователь хочет отправить месяц и получить количество дней в этом месяце:

public class SwitchExpression {

    public static void main(String[] args) {
        int days = 0;
        Month month = Month.APRIL;

        days = switch (month) {
            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> 31;
            case FEBRUARY -> 28;
            case APRIL, JUNE, SEPTEMBER, NOVEMBER -> 30;
            default -> throw new IllegalStateException();
        };
    }
}

В блоке case мы используем немного другие обозначения. Мы используем -> вместо двоеточия. Несмотря на то, что мы не указываем оператор break, мы все равно выйдем из оператора switch при первом выполненном условии.

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

Ключевое слово yield

Логика внутри case блока может быть немного сложнее, чем просто возврат значения. Например, мы хотим записать в лог, какой месяц нам отправил пользователь:

public class SwitchExpression {

    public static void main(String[] args) {
        int days = 0;
        Month month = Month.APRIL;

        days = switch (month) {
            case JANUARY, MARCH, MAY, JULY, AUGUST, OCTOBER, DECEMBER -> {
                System.out.println(month);
                yield 31;
            }
            case FEBRUARY -> {
                System.out.println(month);
                yield 28;
            }
            case APRIL, JUNE, SEPTEMBER, NOVEMBER -> {
                System.out.println(month);
                yield 30;
            }
            default -> throw new IllegalStateException();
        };
    }
}

В многострочном блоке кода мы должны использовать ключевое слово yield для возврата значения из case блока.

Чтобы узнать больше об использовании Switch выражений, обратитесь к документации.

Java 15

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

Текстовый блок усовершенствует форматирование строковых переменных. Начиная с Java 15, мы можем написать String, занимающую несколько строк, как обычный текст.

Пример без использования текстовых блоков

Мы хотим отправить HTML-документ по электронной почте. Мы сохраняем шаблон электронного письма в переменной:

public class TextBlocks {

    public static void main(String[] args) {
        System.out.println(
        "<!DOCTYPE html>\n" +
                "<html>\n" +
                "     <head>\n" +
                "        <title>Example</title>\n" +
                "    </head>\n" +
                "    <body>\n" +
                "        <p>This is an example of a simple HTML " +
                "page with one paragraph.</p>\n" +
                "    </body>\n" +
                "</html>\n");
    }
}

Мы форматируем нашу строку, как в примере выше. Нам нужно позаботиться о переходе текста на новую строку и соединить все строки в одну строку.

Пример использования текстовых блоков

Давайте рассмотрим тот же пример HTML-шаблона для электронной почты. Мы хотим отправить пример электронного письма с простым форматированием HTML. На этот раз мы будем использовать текстовый блок:

public class TextBlocks {
    
    public static void main(String[] args) {
        System.out.println(
        """
                <!DOCTYPE html>
                <html>
                    <head>
                        <title>Example</title>
                    </head>
                    <body>
                        <p>This is an example of a simple HTML 
                        page with one paragraph.</p>
                    </body>
                </html>      
                """
        );
    }
}

Мы использовали специальный синтаксис для открытия и закрытия кавычек: """. Это позволяет нам трактовать нашу строку как будто мы записываем ее в файл .txt.

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

Illegal text block start: missing new line after opening quotes.

Если мы хотим закончить нашу строку символом \n мы можем сделать это, поместив новую строку перед закрывающими кавычками """, как в примере выше.

Чтобы узнать больше о текстовых блоках, обратитесь к документации.

Java 16

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

Сопоставление с образцом instanceof позволяет нам преобразовать нашу переменную в строку и использовать ее внутри желаемого if-else блока без явного преобразования.

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

У нас есть вызываемый базовый класс Vehicle и два расширяющих его класса: Car и Bicycle. Мы опустили код для них - вы можете найти его в репозитории GitHub.

Наш алгоритм расчета цен зависит от экземпляра автомобиля:

public class PatternMatching {
    public static double priceOld(Vehicle v) {
        if (v instanceof Car) {
            Car c = (Car) v;
            return 10000 - c.kilomenters * 0.01 -
                    (Calendar.getInstance().get(Calendar.YEAR) -
                            c.year) * 100;
        } else if (v instanceof Bicycle) {
            Bicycle b = (Bicycle) v;
            return 1000 + b.wheelSize * 10;
        } else throw new IllegalArgumentException();
    }
}

Поскольку мы не используем сопоставление с образцом, нам нужно преобразовать транспортное средство в правильный тип внутри каждого if-else блока. Как мы видим, это типичный пример шаблонного кода, которым славится Java.

Использование сопоставления с образцом

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

public class PatternMatching {
    public static double price(Vehicle v) {
        if (v instanceof Car c) {
            return 10000 - c.kilomenters * 0.01 -
                    (Calendar.getInstance().get(Calendar.YEAR) -
                            c.year) * 100;
        } else if (v instanceof Bicycle b) {
            return 1000 + b.wheelSize * 10;
        } else throw new IllegalArgumentException();
    }
}

Следует отметить область видимости приведенной переменной. Ее видно только внутри оператора if.

Дополнительные сведения о сопоставлении с образцом в методе instanceof см. в документации.

Записи

Сколько POJO (Plain Old Java Objects) вы написали?

Что ж, могу ответить за себя: «Слишком много!».

У Java плохая репутация написания шаблонного кода. Lombok позволил нам перестать беспокоиться о геттерах, сеттерах и т. д. В Java 16 наконец-то появились Records (записи) для удаления большого количества шаблонного кода.

Класс записи - это не что иное, как обычный POJO, для которого большая часть кода генерируется из определения.

Обычное определение POJO

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

public class Vehicle {
    String code;
    String engineType;

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public String getEngineType() {
        return engineType;
    }

    public void setEngineType(String engineType) {
        this.engineType = engineType;
    }

    public Vehicle(String code, String engineType) {
        this.code = code;
        this.engineType = engineType;
    }

    @Override
    public boolean equals(Object o) ...

    @Override
    public int hashCode() ...

    @Override
    public String toString() ...
}

Он включает почти 50 строк кода для объекта, который содержит только два свойства. IDE сгенерировала этот код, но, тем не менее, он существует и должен поддерживаться.

Определение Record 

Определение записи (Record) vehicle с теми же двумя свойствами может быть выполнено всего в одной строке:

public record VehicleRecord(String code, String engineType) {}

В этой строке есть все те же геттеры, сеттеры, конструкторы и т. д., что и в приведенном выше примере. Следует отметить, что класс Record по умолчанию является final, и мы должны этому подчиняться. Это означает, что мы не можем расширить класс Record, но для нас доступно большинство других вещей.

Чтобы узнать больше о классах записи, обратитесь к документации.

Java 17

Sealed классы

Модификатор final в классе не позволяет никому расширить его. А как насчет того, чтобы расширить класс, но разрешить это только для некоторых классов?

Вернемся в автосалон. Мы так гордимся нашим алгоритмом расчета цен, что хотим его опубликовать. Однако мы не хотим, чтобы кто-либо использовал наше представление Vehicle. Оно справедливо только для нашего бизнеса. Здесь мы видим небольшую проблему. Нам нужно раскрыть класс, но также ограничить его.

Именно здесь в игру вступает Java 17 с запечатанными (Sealed) классами. Запечатанный класс позволяет нам сделать класс final для всех, кроме явно упомянутых классов.

public sealed class Vehicle permits Bicycle, Car {...}

Мы добавили модификатор sealed к нашему классу Vehicle, и нам пришлось добавить ключевое слово permits со списком классов, которым мы разрешаем расширять его. После этого изменения мы по-прежнему получаем ошибки от компилятора.

Здесь нам нужно сделать еще одну вещь.

Нам нужно добавить модификаторы finalsealed или non-sealed в классы, которые расширяют наш класс.

public final class Bicycle extends Vehicle {...}

Ограничения

Для работы запечатанного класса необходимо выполнить несколько ограничений:

  • Разрешенные подклассы должны быть доступны запечатанному классу во время компиляции.

  • Разрешенные подклассы должны напрямую расширять запечатанный класс

  • Разрешенные подклассы должны иметь один из следующих модификаторов:

    • final

    • sealed

    • non-sealed

  • Разрешенные подклассы должны находиться в одном Java модуле.

Более подробную информацию о запечатанных классах можно найти в документации.

Tags:
Hubs:
Total votes 20: ↑13 and ↓7+6
Comments12

Articles