Разработка программ. Мои заметки.

January 31, 2017 at 22:15

NullPointerException в Java. Optional - что он такое есть?

6. Тип Optional. Знакомимся.

В Java 8 появился новый класс — Optional. Это несложный класс, присутствующий в стандартной Java библиотеке, который призван слегка обезопасить код, защитив его от NullPoiunterException. То есть, как-то способствовать уменьшению таких ситуаций при его грамотном использовании.

Класс Optional<тип хранимого значения> — это класс-контейнер, умеющий хранить внутри себя ровно одно значение объявленного типа — <тип хранимого значения>, которое может там быть, но может и не быть. Потому как это — опционально (Optional). Что бы узнать хранится ли сейчас внутри значение, существует специальный маркер-флажок. Используя этот маркер и методы класса, которые уже работают с учётом состояния флага, вы сможете уменьшить количество занудливых проверок на null без потери надёжности кода.

Помимо переменной, хранящей значение, есть несколько методов, позволяющих легко инициализировать объект при его создании и затем удобно извлекать хранимое значение. Или сразу использовать его как параметр в каком-то методе (помним о лямдах).

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

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

Также, обязательно, что бы и тот разработчик, который до этого заполнял Optional переменную, также не забыл вставить туда объект типа Optional. Ведь ничего, кроме совести, не позволяет ему оставить там null. Ибо это — обычная переменная, указывающая на объект.

Первая же шальная мысль, которая посещает доверчивого, но энергичного разработчика программ — а не объявить ли нам теперь все переменные, которые порой могут содержать null, как Optional? То есть, ну чуть ли не все ссылки на классы. Дерзкая мысль. Нешуточная революция в коде.

Чуть позже мы вернёмся к тому, где действительно лучше использовать Optional (как рекомендуют те, кто его добавил в платформу). Вопрос оказался далеко не простым. Но пока, что бы самостоятельно оценить технические возможности, просто пройдёмся по тем тривиальным шаблонам, которые мы привыкли использовать в коде, обрабатывая null и которые немного улучшает наличие Optional.

Я тут использую удачный пример из статьи с сайта Oracle. Статья написана Raoul-Gabriel Urma. Называется Tired of Null Pointer Exceptions? Consider Using Java SE 8’s Optional!. Устали от Null Pointer Exceptions? Попробуйте Java SE 8’s Optional!

Мне понравилось какие предметы были использованы в примере композиции объектов со звуковой картой. Немного позаимствую и примеры кода, но в целом статья, увы, даёт не самый хороший образец использования класса Optional. Во всяком случае, один из известных архитекторов языка (Brian Goetz) категорически не рекомендует такой подход. Но, для начала, на нём можно доходчиво показать какие методы имеет класс. А затем, рассмотреть как рациональнее ими пользоваться.

Итак, в статье рассматривается хорошо понятная любому разработчику конструкция: где-то, точно есть компьютер (сomputer), в нём, может быть есть звуковая карта (soundcard), которая, не исключено — имеет USB порт.


    public class Computer
    {
        private Soundcard soundcard;
        public getSoundcard() { ... }
        ...
    }
    
    class Soundcard
    {
        private USB usb;
        public getUSB() { ... }
        ...
    }
    
    class USB
    {
        public String getVersion() { ... }
    }
    

И вот мы хотим узнать версию этого порта. Которая, быть может, указана. Абсолютно тривиальный код:


    String version = computer.getSoundcard().getUSB().getVersion();

Коротко и ясно. Да.

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


    String version = "UNKNOWN";
    Soundcard soundcard = computer.getSoundcard();
    if( soundcard != null ) {
        USB usb = soundcard.getUSB();
        if( usb != null && usb.getVersion() != null ) {
            version = usb.getVersion();
        }
    }

И правильно делают. В этом коде также всё ясно, но не коротко. Увы. Попробуем применить Optional.

Вначале, рассмотрим все способы инициализации объекта типа Optional (иначе как его ввести в дело?):


    // Инициализируем заведомо пустым значением
    Optional<Soundcard> sc = Optional.empty();

    // Инициализируем заведомо НЕ пустым значением
    Optional<Soundcard> sc = Optional.of( new Soundcard() );

    // Инициализируем неизвестным значением (может null, а может и нет)
    Optional<Soundcard> sc = Optional.ofNullable( soundcard );  

Итого, имеем три статических метода для инициализации, для трёх случаев: пустое значение, непустое значение, неизвестное значение:


    Optional.empty();
    Optional.of(...);
    Optional.ofNullable( ... );

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


    public class Computer
    {
        private Optional<Soundcard> soundcard = Optional.of( new Soundcard() );  
        public Optional<Soundcard> getSoundcard() { ... }
        ...
    }
   
    public class Soundcard
    {
        private Optional<USB> usb = Optional.of( new USB() );
        public Optional<USB> getUSB() { ... }
        ...
    }
   
    public class USB
    {
        public String getVersion(){ return "3.0" }
        ...
    }     

Всё выглядит чинно. Всякое поле теперь завёрнуто в “броню” Optional. И доступ к телу исключительно через эту безопасную прокладку. Как мы помним, объект computer точно существует. Теперь, что бы получить какую-то информацию о звуковой карте, можно поступить следующим образом:

Вместо:


    if( computer.getSoundcard() != null ) {
        System.out.println( soundcard );
    }

Можно написать так (опять же предполагаю, что про лямбды Вы слыхали):


    computer.getSoundcard().ifPresent( System.out::println );

А можно проверить есть ли значение и достать его (если оно есть):


    Optional<Soundcard> maybeSoundcard = computer.getSoundcard();
    if( maybeSoundcard.isPresent() ) {
        System.out.println( maybeSoundcard.get() );
    }

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

Часто нужно вернуть некое значение по умолчанию, если нужного значения не существует. Это можно сделать так:


    Optional<Soundcard> maybeSoundcard = computer.getSoundcard();
    Soundcard soundcard = maybeSoundcard.orElse( new Soundcard( "default" ) );

Или сгенерировать исключительную ситуацию:


    Optional<Soundcard> maybeSoundcard = computer.getSoundcard();
    Soundcard soundcard = maybeSoundCard.orElseThrow( IllegalStateException::new );

Понятно, что самое интересное, это сразу обратиться к полю, расположенному ещё на уровень ниже. Или на два уровня. Другими словами — безопасно реализовать нашу мечту вида: computer.getSoundcard().getUSB().getVersion();

Это было бы заметным сокращением кода.

Для этого у класса Optional существуют два метода: map(…) и flatMap(…).

Тут начинается самое интересное. Эти два метода, на первый взгляд, несложны в понимании. Они оба принимают на вход лямду (другими словами, указатель на метод класса). И возвращают результат работы этого метода.

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

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

Разумеется, можно склеивать в цепочку результаты работы этих методов, вызывая методы возвращаемых объектов. Важно только за порядком следить. И что ещё важно, если некие промежуточные объекты типа Optional будут пустыми, вы не получите никаких NullPointerException (разумеется, если вы не забыли создать сами объекты Optional). Вы получите исключительную ситуацию вида “No value present” для того поля, которое пусто (чей Optional был пуст). Чуть ниже я напишу как можно правильно использовать эти методы, но ещё раз должен предупредить, что использовать Optional что бы доступаться к полям модели - категорически не нужно.

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

Итак, вы хотите получить версию вашего USB порта. Который в звуковой карте, которая в компьютере. И как мы помним, компьютер точно есть (проверять не нужно).

Нужно сделать вот так:


    public String getVersion()
    {
        return computer.getSoundcard()
            .flatMap( Soundcard::getUSB )
            .map( USB::getVersion )
            .orElse( "UNKNOWN" );
    }

На этом месте, пытливый ум спросит писателя, откуда взялись эти интересные имена методов: map и flatMap. Какой Кафка и с какой целью их придумал и что они означают? Исключительно хороший вопрос. Я даже полагаю, что большинство читателей знают ответ. А для других я коротко поясню — они пришли из мира функционального программирования. Это тот непростой и интересный мир где “живут” функторы и монады. Мы тут сейчас не будем его трогать, ибо мало не покажется.

Следующей заметкой мы рассмотрим как всё-таки правильно использовать Optional и какие он имеет ограничения.

Автор — Владимир Рыбов