Класс Optional

Иногда возникают условия, при которых метод не может вернуть нормальное значение. До появления класса Optional программисты либо выбрасывали в этом случае исключение, либо возвращали null. Но гораздо красивее в это случае вернуть 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.

Другие особенности Java 8: Лямбда-выражения, Default method.

 

Класс Optional: 2 комментария

  1. А ничего, что flatmap принимает функциональный интерфейс function и передаваемая конструкция Apartment::getKitchen не совпадает по сигнатуре с методом apply интерфейса function?как ссылка на метод в таком случае передается?

    1. Все совпадает, Apartment::getKitchen равносильно а->a.getKitchen(), где а — текущий объект типа Apartment. таким образом в Function T — Apartment, R — Optional )
      Просто есть разные типы method references, иногда левая часть лямбда выражения — вызывающий объект, а иногда аргумент (а иногда оба). У нас вызывающий объект в левой части (это unbound method reference). Еще пример unbound method reference String::toUpperCase. Подробнее см тут.
      Если бы getKithen был статическим, то это был бы static method reference, и переносился бы аргумент.

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

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