Класс Optional

Класс Optional придуман для того, чтобы победить проблемы, связанные со значением null. Наверно все сталкивались с NullPointerException. Если понять, в каких ситуациях уместно использовать Optional,  то это исключение будет возникать гораздо реже.

Введен класс java.util.Optional начиная с версии Java 8.

Если кратко, то в Optional<T> хранят объекты типа T, значение которых может быть равно null. Но прежде чем подробнее рассказывать об использовании Optional, расскажем, какие проблемы возникают из-за его отсутствия.

 Проблема с null

Давайте рассмотрим пример, демонстрирующий проблему. Допустим, у нас есть вложенная структура: Квартира (Apartment), Кухня (Kitchen), Холодильник (Fridge), Марка холодильника (brand). Марку холодильника мы получаем так:

String brand = apartment.getKitchen().getFridge().getBrand();

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

Так что любой из этих геттеров может провалиться и завершить аварийно нашу программу.

Допустим, кухни нет, тогда getKitchen() возвращает null – это распространенная практика. Но тогда следующий за ним геттер getFridge() вызывается на объекте null, и программа завершается с NullPointerException.

Особенно неприятно, если это случается на компьютере заказчика.

Такие ситуации обходятся проверками:

String brand = "UNKNOWN";
    if (apartment != null) {
        Kitchen kitchen = apartment.getKitchen();
	if (kitchen != null) {
		Fridge fridge = kitchen.getFridge();
		if (fridge != null) {
			brand = fridge.getBrand();
		}
	}
}

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

В общем, разработчики Oracle решили придумать другой способ отображать наличие или отсутствие значения объекта, нежели возвращать null. Для хранения объекта, чье значение может быть null, они предложили использовать класс Optional, то есть вместо

Kitchen kitchen;

писать:

Optional<Kitchen> kitchen;

Теперь пара слов о том, что вдохновило инженеров Oracle написать класс Optional.

Что есть аналогичное в других языках

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

  • В Groove есть конструкция с оператором “?.”  и с Elvis-оператором “?:”
String brand = 
    apartment?.getKitchen()?.getFridge()?.getBrand() ?: "UNKNOWN";
  • В Haskell есть тип Maybe.
  • В Scala есть тип Option[T] – очень похоже на наш Optional<T>

Optional<T> в Java 8

Итак, что же у нас в Java 8? Перейдем к примеру.

С Optional класс Apartment будет такой:

import java.util.Optional;

public class Apartment {
	private Kitchen kitchen;

	public Optional<Kitchen> getKitchen() {
		return Optional.ofNullable(kitchen);
	}

	public void setKitchen(Kitchen kitchen) {
		this.kitchen = kitchen;
	}
}

Класс Kitchen такой:

public class Kitchen {
	private Fridge fridge;

	public Optional<Fridge> getFridge() {
		return Optional.ofNullable(fridge);
	}

	public void setFridge(Fridge fridge) {
		this.fridge = fridge;
	}
}

И класс Fridge:

public class Fridge {
	private String brand;

	public String getBrand() {
		return brand;
	}

	public void setBrand(String brand) {
		this.brand = brand;
	}
}

Глядя на эти классы, сразу понятно, что kitchen и fridge могут иметь, а могут не иметь значений (иначе говоря, быть null). Это упрощает чтение кода.

Кроме того, если объект имеет тип Optional, то вам автоматически придется учитываете возможность нуля и пропустить проверку на null будет сложнее.

В итоге с Optional получение марки будет выглядеть так:

String brand = apartment.flatMap(Apartment::getKitchen)
			     .flatMap(Kitchen::getFridge)
				  .map(Fridge::getBrand)
				       .orElse("UNKNOWN");

А теперь начнем сначала и расскажем о создании Optional.

Создание Optional

Optional<T> – это обертка для объекта типа T. Мы создаем Optional, оборачивая обычный объект. Есть три метода обернуть объект:  обернуть можно null, ненулевой объект и есть метод для оборачивания любого (как нулевого, так и ненулевого) объекта.

Обернуть null:

Optional<Kitchen> kitchen = Optional.empty();

Этот метод возвращает Optional, который оборачивает нулевую кухню. Если же мы знаем, что кухня ненулевая, обернуть ее можно с помощью метода Optional.of():

Optional<Kitchen> kitchen = Optional.of(new Kitchen());

Обратите внимание, что если в этот метод передать null, будет выброшено исключение:

@Test(expected = NullPointerException.class)
public void givenNull_whenOf_thenException() {
    Optional.of(null);
}

Обычно мы оборачиваем объект, про который неизвестно, null ли он, например, как в геттерах выше. Для этого используется метод  Optional.ofNullable. Он не выбросит исключение, даже если передаваемый объект равен null:

Optional<Kitchen> kitchen1 = Optional.ofNullable(new Kitchen());
Optional<Kitchen> kitchen2 = Optional.ofNullable(null);

assertThat(kitchen1.isPresent(), is(true));
assertThat(kitchen2.isPresent(), is(false));

Обратите внимание, мы использовали в тесте метод isPresent(), он возвращает true, если Optional содержит ненулевое значение.

Теперь рассмотрим некоторые типовые ситуации, когда Optional полезен.

Проверка на null – isPresent()

Часто в старом коде встречаются такие проверки:

Kitchen kitchen =...
if (kitchen != null) {
    System.out.println(kitchen);
}

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

Optional<Kitchen> maybeKitchen = Optional.ofNullable(new Kitchen());
maybeKitchen.ifPresent(System.out::println);

Значение или действие по умолчанию – orElse(), orElseThrow()

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

Kitchen kitchen = new Kitchen();
Kitchen anotherKitchen = kitchen != null ? kitchen : new Kitchen();

С Optional это делается так:

Optional<Kitchen> maybeKitchen = Optional.ofNullable(new Kitchen());
Kitchen anotherKitchen = maybeKitchen.orElse(new Kitchen());

Допустим, надо извлечь и присвоить значение из Optional<Kitchen>, если оно не null,  либо выбросить исключение, если оно null.

Вот здесь происходит присваивание, поскольку maybeKitchen содержит не null:

Optional<Kitchen> maybeKitchen = Optional.ofNullable(new Kitchen());
Kitchen kitchen = maybeKitchen.orElseThrow(IllegalArgumentException::new);

А здесь будет выброшено исключение:

Optional<Kitchen> maybeKitchen = Optional.ofNullable(null);
Kitchen kitchen = maybeKitchen.orElseThrow(IllegalArgumentException::new);

Сложная проверка – filter()

Ранее мы проверяли наличие значения с помощью ifPresent() и делали что-то. Допустим, надо выполнить более сложную проверку. Давайте напечатаем марку холодильника, причем только в том случае, если она равна “LG”.

Без Optional:

Fridge fridge=new Fridge();
fridge.setBrand("LG");
if(fridge!=null && fridge.getBrand().equals("LG")){
    System.out.println(fridge.getBrand());
}

С Optional:

Optional<Fridge> maybeFridge = Optional.ofNullable(fridge);
maybeFridge.filter(f->f.getBrand().equals("LG"))
              .ifPresent(f->System.out.println(f.getBrand()));

Метод filter() возвращает тот Optional, на котором вызван, если выполняется условие. Иначе он возвращает Optional.empty().

Извлечение и преобразование значений с помощью map()

Вышеприведенную задачу можно также решить с помощью метода map():

Optional<String> brand = maybeFridge.map(Fridge::getBrand);

brand.filter(b -> b.equals("LG")).ifPresent(b -> System.out.println(b));

В map() передается функция преобразования, которая применяется к значению в Optional, если оно не null. Иначе возвращается Optional.empty(). Исключений не выбрасывается, независимо от того, null ли холодильник или нет.

Во второй строке  мы делаем фильтрацию и печатаем значение.

flatMap() для цепочки Optional

Вернемся к нашей задаче – получить по цепочке кухню, холодильник и марку. Марку из холодильника мы получили в последнем примере с помощью map(). При этом избавились от проблемы NullPointerException.

На первый взгляд кажется, что аналогично можно извлечь кухню и холодильник. Но это не совсем так. Геттеры кухни и холодильника возвращают не просто значение обычного типа, как getBrand(), а типы Optional<T>. При вызове map() мы получим вложенный Optional:

Apartment apartment = new Apartment();
Optional<Apartment> maybeApartment = Optional.ofNullable(apartment);
		
Optional<Optional<Kitchen>> mbKitchen=maybeApartment.map(Apartment::getKitchen);

Чтобы убрать вложенный Optional, придуман метод flatMap(). С его помощью мы можем вызвать цепочку геттеров:

@Test
public void whenFlatMap_thenOk() {

    Apartment apartment = new Apartment();
    Kitchen kitchen=new Kitchen();
    Fridge fridge = new Fridge();
		
    fridge.setBrand("LG");
    kitchen.setFridge(fridge);
    apartment.setKitchen(kitchen);

    Optional<Apartment> maybeApartment = Optional.ofNullable(apartment);
		
    // здесь цепочка	
    String brand = maybeApartment.flatMap(Apartment::getKitchen)
        .flatMap(Kitchen::getFridge)
            .map(Fridge::getBrand)
		.orElse("UNKNOWN");
		
   assertThat(brand, equalTo("LG"));
}

На последнем звене цепочки мы вызываем map(), потому что getBrand() возвращает не Optional, а обычное значение – это было проделано уже в предыдущем разделе.

Для кухни же и квартиры мы используем flatMap().

Обратите внимание, что если нашу структуру значениями не заполнять, то никакого NullPointerException не будет (хотя в в старом коде исключение возникало). Здесь мы создаем Apartment, но никакого холодильника и кухни ей не назначаем, и все методы, тем не менее,  вызываются безопасно:

@Test
public void whenFlatMap_thenNoException() {

    Apartment apartment = new Apartment();
    Optional<Apartment> maybeApartment = Optional.ofNullable(apartment);
		
	
    String brand = maybeApartment.flatMap(Apartment::getKitchen)
                      .flatMap(Kitchen::getFridge)
                            .map(Fridge::getBrand)
				.orElse("UNKNOWN");
		

    assertThat(brand, equalTo("UNKNOWN"));

Итог

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

При чтении кода с Optional сразу понятно,  где значения не обязательны, и код гораздо короче.

Код вышеприведенных примеров доступен на GitHub.

 

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *