$ sudo teach IT
МОДУЛЬ 10 · УРОК 6

Optional

Решаем проблему NullPointerException с помощью Optional. Безопасная работа с nullable-значениями без проверок на null

~25 минут Для новичков Java

Проблема: NullPointerException

NullPointerException (NPE) — самое частое исключение в Java. Оно возникает, когда вы пытаетесь обратиться к методу или полю объекта, который равен null.

public class Main {
    public static void main(String[] args) {
        String name = null;
        System.out.println(name.length()); // NullPointerException!
    }
}

Для защиты от NPE приходится писать много проверок на null:

public class Main {
    public static void main(String[] args) {
        String name = getName();

        if (name != null) {
            System.out.println(name.length());
        } else {
            System.out.println("Имя не найдено");
        }
    }

    static String getName() {
        return null;
    }
}

Аналогия: Представьте, что вы заказываете пиццу. Доставщик может принести пиццу, а может сказать «извините, пиццы нет». Без Optional вам приходится каждый раз проверять: «А пицца есть? А точно есть? А может, она протухла?». Optional — это как уверенный ответ: «Пицца точно есть, и вот она» или «Пиццы нет, но вот что я могу предложить вместо неё».

Optional — это контейнер, который может содержать значение (не null) или быть пустым. Он заставляет вас явно обработать оба варианта, избегая случайных NPE.

Создание Optional

Есть три способа создать Optional:

Optional.of(value) — создаёт Optional с гарантированно ненулевым значением. Если передать null, выбросит NullPointerException:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        System.out.println(name.isPresent()); // true

        Optional<String> empty = Optional.of(null); // NullPointerException!
    }
}

Optional.ofNullable(value) — создаёт Optional, который может содержать null. Если значение null — Optional будет пустым:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name1 = Optional.ofNullable("Анна");
        System.out.println(name1.isPresent()); // true

        Optional<String> name2 = Optional.ofNullable(null);
        System.out.println(name2.isPresent()); // false
    }
}

Optional.empty() — создаёт пустой Optional без значения:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> empty = Optional.empty();
        System.out.println(empty.isPresent()); // false
        System.out.println(empty.isEmpty());  // true
    }
}

Правило: Используйте Optional.of() когда уверены, что значение не null. Используйте Optional.ofNullable() когда значение может быть null. Никогда не храните null в Optional.

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

Optional предоставляет несколько способов проверить, есть ли значение:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        Optional<String> empty = Optional.empty();

        System.out.println(name.isPresent());   // true
        System.out.println(name.isEmpty());     // false

        System.out.println(empty.isPresent());  // false
        System.out.println(empty.isEmpty());    // true
    }
}

Но проверка isPresent() + get() — это всё ещё «старый» стиль. Гораздо удобнее использовать методы, которые мы рассмотрим дальше.

Получение значения

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

get() — получает значение напрямую. Если Optional пуст, выбросит NoSuchElementException:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        System.out.println(name.get()); // Анна

        Optional<String> empty = Optional.empty();
        System.out.println(empty.get()); // NoSuchElementException!
    }
}

orElse(defaultValue) — возвращает значение или значение по умолчанию, если Optional пуст:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        Optional<String> empty = Optional.empty();

        System.out.println(name.orElse("Неизвестно"));   // Анна
        System.out.println(empty.orElse("Неизвестно"));  // Неизвестно
    }
}

orElseGet(supplier) — возвращает значение или вычисляет его с помощью Supplier, если Optional пуст. Полезно, когда значение по умолчанию дорогое в вычислении:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> empty = Optional.empty();

        String result = empty.orElseGet(() -> {
            System.out.println("Вычисляю значение...");
            return "Вычислено";
        });

        System.out.println(result); // Вычисляю значение... Вычислено
    }
}

orElseThrow() — возвращает значение или выбрасывает исключение, если Optional пуст:

import java.util.Optional;
import java.util.NoSuchElementException;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        Optional<String> empty = Optional.empty();

        System.out.println(name.orElseThrow()); // Анна
        System.out.println(empty.orElseThrow()); // NoSuchElementException!
    }
}

ifPresent: выполнение при наличии значения

Метод ifPresent() выполняет действие, только если значение присутствует. Это безопасная альтернатива проверке if (value != null):

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        Optional<String> empty = Optional.empty();

        name.ifPresent(n -> System.out.println("Имя: " + n)); // Имя: Анна
        empty.ifPresent(n -> System.out.println("Имя: " + n)); // Ничего не выведет
    }
}

ifPresentOrElse() — выполняет одно действие при наличии значения и другое при его отсутствии:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");
        Optional<String> empty = Optional.empty();

        name.ifPresentOrElse(
            n -> System.out.println("Найдено: " + n),
            () -> System.out.println("Не найдено")
        );

        empty.ifPresentOrElse(
            n -> System.out.println("Найдено: " + n),
            () -> System.out.println("Не найдено")
        );
    }
}

Преобразование: map, filter, flatMap

Optional поддерживает те же операции, что и Stream: map, filter и flatMap. Это позволяет строить цепочки обработки.

map() — преобразует значение, если оно есть:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");

        Optional<Integer> length = name.map(String::length);
        System.out.println(length.orElse(0)); // 4

        Optional<String> upper = name.map(String::toUpperCase);
        System.out.println(upper.orElse("")); // АННА

        Optional<String> empty = Optional.empty();
        Optional<Integer> emptyLength = empty.map(String::length);
        System.out.println(emptyLength.orElse(0)); // 0
    }
}

filter() — оставляет значение, если оно удовлетворяет условию:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");

        Optional<String> longName = name.filter(n -> n.length() > 5);
        System.out.println(longName.isPresent()); // false

        Optional<String> shortName = name.filter(n -> n.length() > 2);
        System.out.println(shortName.isPresent()); // true
    }
}

flatMap() — используется, когда функция преобразования возвращает Optional:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");

        Optional<Integer> result = name.flatMap(n -> {
            if (n.length() > 3) {
                return Optional.of(n.length());
            }
            return Optional.empty();
        });

        System.out.println(result.orElse(0)); // 4
    }
}

Цепочки операций с Optional

Главное преимущество Optional — возможность строить цепочки операций для безопасной обработки nullable-значений:

import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        Optional<String> name = Optional.of("Анна");

        String result = name
            .filter(n -> n.length() > 3)
            .map(String::toUpperCase)
            .orElse("Неизвестно");

        System.out.println(result); // АННА

        Optional<String> short = Optional.of("Б");

        String result2 = short
            .filter(n -> n.length() > 3)
            .map(String::toUpperCase)
            .orElse("Неизвестно");

        System.out.println(result2); // Неизвестно
    }
}

Практический пример: безопасный поиск

Давайте создадим пример с поиском пользователя, который возвращает Optional вместо null:

import java.util.HashMap;
import java.util.Optional;

public class UserFinder {
    static HashMap<String, String> users = new HashMap<>();

    static {
        users.put("anna", "Анна Иванова");
        users.put("boris", "Борис Петров");
        users.put("vika", "Вика Сидорова");
    }

    public static Optional<String> findUser(String login) {
        return Optional.ofNullable(users.get(login));
    }

    public static void main(String[] args) {
        findUser("anna")
            .ifPresent(name -> System.out.println("Найден: " + name));

        findUser("dima")
            .ifPresentOrElse(
                name -> System.out.println("Найден: " + name),
                () -> System.out.println("Пользователь не найден")
            );

        String name = findUser("boris")
            .map(String::toUpperCase)
            .orElse("НЕИЗВЕСТНО");

        System.out.println("Имя: " + name);
    }
}

Optional и Stream API

Optional отлично работает со Stream API. Многие терминальные операции Stream возвращают Optional:

import java.util.ArrayList;
import java.util.Optional;

public class Main {
    public static void main(String[] args) {
        ArrayList<Integer> numbers = new ArrayList<>();
        numbers.add(1);
        numbers.add(2);
        numbers.add(3);
        numbers.add(4);
        numbers.add(5);

        Optional<Integer> max = numbers.stream()
            .max(Integer::compareTo);
        System.out.println("Максимум: " + max.orElse(0)); // 5

        Optional<Integer> first = numbers.stream()
            .filter(n -> n > 10)
            .findFirst();
        System.out.println("Больше 10: " + first.orElse(-1)); // -1

        Optional<Integer> found = numbers.stream()
            .filter(n -> n == 3)
            .findFirst();
        found.ifPresent(n -> System.out.println("Найдено: " + n));
    }
}

Когда использовать Optional

Optional полезен в следующих ситуациях:

  • Возвращаемое значение метода может отсутствовать (поиск, получение по ключу)
  • Результат вычисления зависит от условий
  • Вы хотите избежать NullPointerException
  • Вы хотите явно показать, что значение может отсутствовать

Когда НЕ использовать Optional:

  • Как параметр метода (лучше использовать перегрузку или null)
  • В полях класса (лучше использовать null или инициализацию)
  • В коллекциях (коллекции не должны содержать Optional)
  • Когда null — допустимое значение бизнес-логики
import java.util.Optional;

public class User {
    private String name;
    private Optional<String> email;

    public User(String name, String email) {
        this.name = name;
        this.email = Optional.ofNullable(email);
    }

    public String getName() {
        return name;
    }

    public Optional<String> getEmail() {
        return email;
    }
}

class Main {
    public static void main(String[] args) {
        User user1 = new User("Анна", "anna@mail.ru");
        User user2 = new User("Борис", null);

        user1.getEmail()
            .ifPresent(e -> System.out.println("Email Анны: " + e));

        user2.getEmail()
            .ifPresentOrElse(
                e -> System.out.println("Email Бориса: " + e),
                () -> System.out.println("У Бориса нет email")
            );
    }
}

Итоги урока

  • Optional — контейнер, который может содержать значение или быть пустым
  • Optional.of() — создаёт Optional с гарантированно ненулевым значением
  • Optional.ofNullable() — создаёт Optional, который может быть пустым
  • Optional.empty() — создаёт пустой Optional
  • isPresent()/isEmpty() — проверка наличия значения
  • get() — получение значения (опасно без проверки!)
  • orElse(value) — значение или значение по умолчанию
  • orElseGet(supplier) — значение или вычисление по умолчанию
  • orElseThrow() — значение или исключение
  • ifPresent(consumer) — действие при наличии значения
  • map() — преобразование значения
  • filter() — фильтрация по условию
  • flatMap() — преобразование, возвращающее Optional
  • Optional безопаснее проверок на null и делает код выразительнее

Тест по Optional

5 вопросов

Optional

Premium