Избегаем исключения Null Pointer Exception в Java с помощью Optional

В 1964 году британский ученый-компьютерщик Тони Хоар изобрел ссылки на нулевые указатели (Null Pointer References).

Исключение Null Pointer Exception составляет львиную долю всех исключений, которые возникают в продакшне. Оно было реализовано во многих языках программирования, включая C, C++, C#, JavaScript, Java и другие.

Потеря денег, времени и человеческих ресурсов, которые уходят на его исправление, побудила Хоара назвать его “ошибкой на миллиард долларов”.

Java  —  один из языков программирования, где реализованы ссылки на нулевые указатели. Если вы разрабатывали на Java, то наверняка их встречали. Неважно, новичок ли вы в Java или за плечами у вас десять лет опыта  —  всегда есть вероятность, что вы столкнетесь с ошибкой Null Pointer Exception.

Optional в Java

Optional  —  это API, предоставленный в Java 8. При правильном применении он способен решить проблему Null Pointer Exception.

API Optional реализует функциональное программирование и использует функциональный интерфейс.

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

Пустой Optional

Пустой элемент optional  —  это основной способ избежать исключения Null Pointer Exception при использовании API Optional.

В потоке Optional любой null будет преобразован в пустой Optional. Пустой элемент Optional больше не будет обрабатываться. Вот как мы можем избежать исключения NullPointerException.

Подробнее о том, как ведет себя пустой Optional, мы поговорим позже.

Создадим объект Optional

Существует три способа инициализации объекта Optional:

  • Optional.of(T)
  • Optional.ofNullable(T)
  • Optional.empty()

Optional.of

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

@Test
    public void initializeOptional_optionalOf() {
        Optional<String> helloWorldOptional = Optional.of("Hello, world");
        assert helloWorldOptional.isPresent();
        assert "Hello, world".equals(helloWorldOptional.get());
    }

Будьте осторожны, когда передаете значение Optional.of. Помните, что Optional.of не принимает в качестве параметра нулевые значения. Если вы попытаетесь передать нулевое значение, оно вызовет исключение NullPointerException.

@Test
    public void initializeOptional_optionalOf_null() {
        try {
            Optional.of(null);
        } catch (Exception e) {
            assert e instanceof NullPointerException;
        }
    }

Optional.ofNullable

Optional.ofNullable аналогичен Optional.of. Он принимает любой тип. Разница в том, что с помощью Optional.ofNullable параметру возможно передать нулевое значение.

@Test
    public void initializeOptional_optionalOfNullable() {
        Optional<String> helloWorldOptional = Optional.ofNullable("Hello, world");
        assert helloWorldOptional.isPresent();
        assert "Hello, world".equals(helloWorldOptional.get());
    }

Когда Optional.ofNullable инициализируется с объектом null, он возвращает пустой Optional.

@Test
    public void initializeOptional_optionalOfNullable_null() {
        Optional<String> helloWorldOptional = Optional.ofNullable(null);
        assert !helloWorldOptional.isPresent();
        try {
            helloWorldOptional.get();
        } catch (Exception e) {
            assert e instanceof NoSuchElementException;
        }
    }

Optional.empty

Пустой Optional можно инициализировать через Optional.empty().

@Test
    public void initializeOptional_optionalEmpty() {
        Optional<String> helloWorldOptional = Optional.empty();
        assert !helloWorldOptional.isPresent();
    }

Доступ к Optional.

Есть несколько способов получить значение Optional.

get

Простой метод. Метод get вернет значение Optional, если оно присутствует, и вызовет исключение NoSuchElementException, если значения не существует.

@Test
    public void get_test() {
        Optional<String> helloWorldOptional = Optional.of("Hello, World");
        assert "Hello, World".equals(helloWorldOptional.get());
    }

    @Test
    public void get_null_test() {
        Optional<String> helloWorldOptional = Optional.empty();
        try {
            helloWorldOptional.get();
        } catch (Exception e) {
            assert e instanceof NoSuchElementException;
        }

orElse

Если вы предпочитаете значение по умолчанию в случае, когда Optional пуст, вы можете воспользоваться методом orElse.

@Test
    public void orElse_test() {
        Optional<String> helloWorldOptional = Optional.of("Hello, World");
        assert "Hello, World".equals(helloWorldOptional.orElse("default"));
    }

    @Test
    public void orELseNull_test() {
        Optional<String> helloWorldOptional = Optional.empty();
        assert "default".equals(helloWorldOptional.orElse("default"));

orElseGet

orElseGet очень похож на метод orElse. Только orElseGet принимает в качестве параметра Supplier<T>.

@Test
    public void orElseGet_test() {
        Optional<String> helloWorldOptional = Optional.of("Hello, World");
        assert "Hello, World".equals(helloWorldOptional.orElseGet(() ->"default"));
    }

    @Test
    public void orELseGet_Null_test() {
        Optional<String> helloWorldOptional = Optional.empty();
        assert "default".equals(helloWorldOptional.orElseGet(() ->"default"));
    }

orElseThrow

orElseThrow вернет значение Optional или выдаст исключение, если значение Optional пустое.

@Test
    public void orElseThrow_test() {
        Optional<String> helloWorldOptional = Optional.of("Hello, World");
        assert "Hello, World".equals(helloWorldOptional.orElseThrow(NullPointerException::new));
    }

    @Test
    public void orELseThrow_Null_test() {
        Optional<String> helloWorldOptional = Optional.empty();
        try {
            helloWorldOptional.orElseThrow(NullPointerException::new);
        } catch (Exception e) {
            assert e instanceof NullPointerException;
        }
    }

Обработка Optional

Есть много способов обработки и преобразования Optional. В этом разделе мы рассмотрим наиболее распространенные методы.

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

map

Метод map чаще других применяется при обработке объекта Optional. В качестве параметра он принимает Function<? super T, ? extends U> и возвращает Optional<U>. Это означает, что в вашей функции может быть любой тип параметра, а возвращаемое значение будет обернуто в Optional внутри метода map.

@Test
    public void processingOptional_map_test() {
        Optional<String> stringOptional = Optional.of("Hello, World")
                .map(a -> a + ", Hello");

        assert stringOptional.isPresent();
        assert "Hello, World, Hello".equals(stringOptional.get());
    }

Если вы попытаетесь вернуть нулевое значение в функции <? super T,? extends U>, метод map вернет пустой Optional.

@Test
    public void processingOptional_map_empty_test() {
        Optional<String> stringOptional = Optional.of("Hello, World")
                .map(a -> null);

        assert !stringOptional.isPresent();
    }

Пустой Optional не будет обработан map. Это подтверждается следующим тестом:

@Test
    public void processingOptional_map_empty_notProcessed_test() {
        AtomicBoolean atomicBoolean = new AtomicBoolean(false);
        Optional<String> stringOptional = Optional.of("Hello, World")
                .map(a -> null)
                .map(a -> {
                    atomicBoolean.set(true);
                    return "won't be processed";
                });

        assert !stringOptional.isPresent();
        assert atomicBoolean.get() == false;
    }

flatMap

Этот метод похож на map, но flatMap не будет переносить возвращаемое значение функции в Optional. Метод flatMap принимает в качестве параметра Function<? super T, ? extends Optional<? extends U>>. Это означает, что вам нужно будет определить функцию, которая принимает любой тип и возвращает Optional.

Как правило, метод flatMap пригождается, когда ваш код вызывает другой метод, возвращающий объект Optional.

@Test
    public void processingOptional_flatmap_test() {
        Optional<String> stringOptional = Optional.of("Hello, World")
                .flatMap(this::getString);

        assert "Hello, World, Hello".equals(stringOptional.get());
    }

    @Test
    public void processingOptional_flatmap_randomString_test() {
        Optional<String> stringOptional = Optional.of(UUID.randomUUID().toString())
                .flatMap(this::getString);

        assert !stringOptional.isPresent();
    }

    public Optional<String> getString(String s) {
        if ("Hello, World".equals(s)) {
            return Optional.of("Hello, World, Hello");
        }
        return Optional.empty();
    }

filter

В предыдущем примере с flatMap мы использовали декларативный стиль для дифференциации возвращаемого значения метода getString. Но можно переписать это в функциональном стиле с помощью метода filter.

@Test
    public void processingOptional_filter_test() {
        Optional<String> stringOptional = Optional.of("Hello, World")
                .filter(helloWorldString -> "Hello, World".equals(helloWorldString))
                .map(helloWorldString -> helloWorldString + ", Hello");

        assert "Hello, World, Hello".equals(stringOptional.get());
    }

    @Test
    public void processingOptional_filter_randomString_test() {
        Optional<String> stringOptional = Optional.of(UUID.randomUUID().toString())
                .filter(helloWorldString -> "Hello, World".equals(helloWorldString))
                .map(helloWorldString -> helloWorldString + ", Hello");

        assert !stringOptional.isPresent();
    }

ifPresent

Метод ifPresent принимает Consumer, который будет выполняться только в том случае, если Optional не пуст.

@Test
    public void processingOptional_ifPresent_test() {
        AtomicBoolean atomicBoolean = new AtomicBoolean(false);
        Optional.of("Hello, World")
            .ifPresent(helloWorldString -> atomicBoolean.set(true));
        assert atomicBoolean.get();
    }

    @Test
    public void processingOptional_ifPresent_empty_test() {
        AtomicBoolean atomicBoolean = new AtomicBoolean(false);
        Optional.empty()
                .ifPresent(helloWorldString -> atomicBoolean.set(true));
        assert !atomicBoolean.get();
    }

Чего следует избегать

Если вы хотите использовать Optional в своем коде, вам стоит избегать некоторых критичных вещей.

Не создавайте метод, который принимает Optional

Создание метода, который принимает Optional в качестве параметра, может привести к возникновению той самой проблемы, которую он предположительно решает: NullPointerException.

Если пользователь метода с параметром Optional не знает об этом, он может передать методу null вместо Optional.empty(). Обработка null приведет к исключению NullPointerException.

@Test
    public void optionalAsParameter_test() {
        try {
            isPhoneNumberPresent(null);
        } catch (Exception e) {
            assert e instanceof NullPointerException;
        }
    }

    public boolean isPhoneNumberPresent(Optional<String> phoneNumber) {
        return phoneNumber.isPresent();
    }

Получение значения без проверки

Если вы задействуете Optional, то лучше по возможности избегать метода get. Если по какой-то причине он вам все-таки нужен, убедитесь, что вы сначала проверили его с помощью метода isPresent, потому что если применить get на пустом Optional, он вызовет исключение NoSuchMethodException.

@Test
    public void getWithIsPresent_test() {
        Optional<String> helloWorldOptional = Optional.ofNullable(null);
        if (helloWorldOptional.isPresent()) {
            System.out.println(helloWorldOptional.get());
        }
    }

    @Test
    public void getWithoutIsPresent_error_test() {
        Optional<String> helloWorldOptional = Optional.ofNullable(null);
        try {
            System.out.println(helloWorldOptional.get());
        } catch (Exception e) {
            assert e instanceof NoSuchElementException;
        }
    }

Заключение

Спасибо, что дочитали до конца! Optional  —  мощная функция, о которой стоит знать каждому Java-разработчику. Если вы станете правильно применять функции optional от начала до конца, то вряд ли еще когда-либо столкнетесь с исключением NullPointerException.

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

Репозиторий с примерами из этой статьи вы можете найти здесь: https://github.com/brilianfird/java-optional

Читайте также:

Читайте нас в Telegram, VK и Яндекс.Дзен


Перевод статьи Brilian Firdaus: “Avoiding the Null Pointer Exception With Optional in Java”

Предыдущая статьяПодписки, чеки и StoreKit в iOS 14
Следующая статья5 полезных советов для загрузки HTML-файлов