Статьи

Необязательно: Java 8 способ справиться с нулем

Те, кто программировал на Java или C / C ++ в течение какого-либо периода времени, будут знать, что одной из самых раздражающих вещей является попытка отладить сбой из-за доступа к нулевому объекту. Хотя концепция нуля необходима для работы языка программирования, она учитывает отклонения от нормального «счастливого» пути, включая обработку ошибок, но не способствует реализации решения. Тем не менее, мы должны тратить немало времени на работу и защиту от нулевых значений для создания надежного программного обеспечения. Сегодня мы рассмотрим, как Optional может улучшить наш код в целом, а затем кратко рассмотрим его API.

Ноль — это значение по умолчанию для неинициализированного поля члена класса или статического объекта, мы переназначаем нуль для освобождения памяти. Он также используется для значений дозорных, таких как указание отсутствия данных. Проблема в том, что когда мы пытаемся получить доступ к нулевому значению, мы получаем исключение. Затем мы пытаемся выяснить, было ли значение неинициализировано и, следовательно, является ошибкой какого-либо другого кода, или это было дозорным значением, которое наш код не обрабатывал должным образом. Иногда это приводит к неправильному исправлению или сглаживанию того, какое исправление сделать. Этот код, вероятно, будет выглядеть знакомо:

public class ImportantData
{
  private Data fileData; // Not constructor initialised

  ...

  // Call first before using csvData
  public void load(String fname)
  {
    try
    {
       fileData = loadCSVFromFile(fname);
    }
    catch (IOException e)
    {
      // Should at least have:
      // System.err.println("Can't load " + fname);
    }
  }

...
}

Это паттерн «Я пока не могу понять, как с этим справиться». Часто мы делаем такие вещи просто для того, чтобы запустить код, потому что обработка ошибки может быть не тривиальной, еще не определенной и / или мы делаем доказательство концепции. Такой код становится более вероятным в гибкой «всегда демонстрируемой» модели разработки. Когда мы пытаемся построить этот код, легко забыть пересмотреть ярлыки и трудно найти их снова, если мы не помечаем их последовательно. Хуже того, когда исключение перехватывается, но обработчик пуст, даже сообщения нет, поэтому мы получаем молчаливый сбой. Это дополнительно усугубляется правилом Java о необходимости отлавливать проверенные исключения, соблазняя нас в ярлык. Тестирование может даже не выявить проблему, потому что это исключительный случай и может потребоваться что-то еще, чтобы пойти не так, прежде чем мы получим ошибку.

Если fileData не должен быть нулевым, мы обязательно должны сделать проверку. Мы могли бы использовать assert, но это будет отключено в производстве. Если пространство или время не стоят на месте, всегда лучше защищаться. Лучше поймать проблему раньше, чем позже, а также не допустить, чтобы она продолжалась и испортила что-то еще. До Java 7 нам пришлось бы делать следующее:

try
{
   fileData = loadCSVFromFile(fname);
}
catch (IOException e)
{
  // Should at least have:
  // System.err.println("Can't load " + fname);
}

if (fileData == null)
{
  throw new NullPointerException("fileData can't be null!");
}

Это также поможет нам с тихим уловом IOException, так как fileData также будет нулевым.

В Java 7 мы можем пойти лучше и заменить нулевой тест встроенным:

Objects.requireNonNull(fileData, "fileData can't be null!");

Это короче, документирует наше намерение, что fileData не может быть нулевым, и предотвращает нулевой объект, вызывающий беспокойство позже в коде. Существует две версии requireNonNull, одна с сообщением, а другая без нее, которые точно переводят в более старый эквивалент Java.

Добавлена ​​Java 8 Необязательно, чтобы мы могли лучше работать с нулями и различать результат и неинициализированный / произошла ошибка. Давайте изменим код следующим образом:

public class ImportantData
{
  private Optional<Data> fileData; // Not constructor initialised

  ...

  // Call first before using csvData
  public void load(String fname)
  {
    // assume fileData is uninitialised at this point

    try
    {
       fileData = Optional.of(loadCSVFromFile(fname));
    }
    catch (IOException e)
    {
      // Should at least have:
      // System.err.println("Can't load " + fname);
    }

    Objects.requireNonNull(fileData, "fileData can't be null!");
  }

...
}

Теперь мы используем Optional, чтобы обернуть наш объект Data с помощью статического метода of of (необязательно). Optional можно инициализировать только статическими методами. Метод ‘of’ вызовет исключение NullPointerException, если мы попытаемся обернуть ноль. Мы могли бы также использовать это как бесплатную проверку безопасности, поскольку код тут же вылетает. Если мы хотим быть более устойчивыми позже, мы можем найти Optional.of, чтобы найти все места, которые мы должны проверять на наличие NullPointerException.

Когда мы, наконец, закончили с fileData и нам нужно передать его сборщику мусора, мы не сможем просто изменить содержимое Optional (поскольку его нельзя переназначить), нам нужно изменить ссылки на fileData. Мы могли бы рассмотреть специальный объект-страж, чтобы указать, что он освобожден, а не использовать ноль, который может быть ошибочно [при отладке] никогда не инициализирован (то есть загрузка никогда не вызывалась).

Предположим, что для loadCSVFromFIle допустимо возвращать ноль, возможно, для обозначения пустого файла. Без обобщения этого параметра мы не можем различить пустой файл, файл не найден, или файл был поврежден, или загрузка никогда не вызывалась. Если мы не обработаем эти исключения должным образом, у нас не будет возможности позже узнать причину, по которой fileData имеет значение null, и нужно ли с ней работать или ее нужно обрабатывать ранее. Таким образом, мы не документируем наши намерения, часто оставляя кого-то еще, чтобы понять, что мы имели в виду. Это может привести к неправильному исправлению.

Необязательный помогает с этой проблемой, но чтобы обнулить нули, мы должны заменить

...
       fileData = Optional.of(loadCSVFromFile(fname));
...

с

...
       fileData = Optional.ofNullable(loadCSVFromFile(fname));
...

Поскольку передача пустого значения в методе Optional ‘of’ вызывает исключение NullPointerException, мы должны использовать ofNullable, который также переносит нулевые значения. Под капотом возвращается Optional.empty (), если ему передается значение null. Теперь мы можем определить разницу между неинициализированным fileData (из-за исключений или загрузки, не вызываемой) и случая, когда в файле отсутствуют какие-либо данные.

Примечание. В примере предполагалось, что мы не можем изменить loadCSVFromFile, но если бы мы могли, мы бы вернули Optional из этого, а не переносили его позже. Это также избавит пользователя API от необходимости решать, переносить ли с ‘of’ или ofNullable.

Опционально позволяет нам легче работать с нулевыми объектами, так как есть полезные вспомогательные функции, уменьшающие количество элементов ‘if (object! = Null) {…. «Это может засорять код, делая его трудным для понимания.

Давайте теперь посмотрим на API Optional. Обратите внимание, что существуют также специальные Optionals: OptionalInt, OptionalDouble и OptionalLong, чьи API очень похожи. Сначала мы начнем с создания (обертывания объектов) и их распаковки:

public static void main(String[] args)
{
    Optional<String> opt = Optional.of("hello");
    System.out.println("Test1: " + opt.get());

    try
    {
        Optional.of(null);
    }
    catch (NullPointerException e)
    {
        System.out.println(
           "Test2: Can't wrap a null object with of");
    }

    Optional<String> optNull = Optional.ofNullable(null);

    try
    {
        System.out.println(optNull.get());
    }
    catch (NoSuchElementException e)
    {
        System.out.println(
           "Test3: Can't unwrap a null object with get");
    }

    Optional<String> optEmpty = Optional.empty();

    try
    {
        System.out.println(optEmpty.get());
    }
    catch (NoSuchElementException e)
    {
        System.out.println(
           "Test4: Can't unwrap an empty Optional with get");
    }
}

Есть четыре теста выше:

1, первый показывает перенос объекта, который мы делаем, вызывая статический метод ‘of’ с объектом, который мы хотим обернуть, а затем извлекаем с помощью get (getAs в специализированных Optionals).
2, второе показывает, что мы не можем обернуть нулевой объект с помощью ‘of’, и если мы попробуем, мы получим исключение NullPointerException. Таким образом, ‘of’ следует использовать, когда мы уверены, что нулевое значение невозможно, или мы хотим вызвать исключение NullPointerException, если оно есть. Если ноль допустим, мы должны использовать вместо Nullable.
3. & 4. Третий и четвертый — фактически один и тот же случай, так как когда ofNullable оборачивает нуль, возвращается Optional.empty (). Мы также можем вызвать пустой метод напрямую. Эти тесты показывают, что если метод get используется для разворачивания Optional.empty (), он выдаст исключение NoSuchElementException.

Следует отметить, что специализированные версии (например, OptionalInt) не имеют ofNullable, хотя мы все еще можем выполнить тест и вручную получить OptionalInt.empty (), если мы захотим. Соответственно, этот API работает с int, а не с Integer.

Так как нам может потребоваться проверить, является ли Optional пустым или нет, мы можем использовать для этого тест isPresent (). API явно заявляет, что мы никогда не должны делать проверку == против Optional.empty (), так как нельзя гарантировать, что он будет одиночным.

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

public class OptionalTest2
{
        public static void main(String[] args)
        {
                Optional<String> opt = Optional.of("found");
                System.out.println(opt.isPresent());
                System.out.println(opt.orElse("not found"));

                Optional<String> optNull = Optional.ofNullable(null);
                System.out.println(optNull.isPresent());
                System.out.println(optNull.orElse("default"));

                Optional<String> optEmpty = Optional.empty();
                System.out.println(optEmpty.isPresent());
                System.out.println(optEmpty.orElse("default"));
        }
}

В дополнение к коду, предоставляющему значение по умолчанию, явно использующее orElse, мы можем вызвать orElseGet, чтобы получить значение от поставщика. Существует также orElseThrow, в котором переданный поставщик предоставит соответствующее исключение, а также метод ifPresent, передающий значение поставщику, только если Optional переносит значение. Следующий пример демонстрирует это:

public class OptionalTest3
{
    private static class MySupplier implements Supplier<String>
    {
        @Override
        public String get()
        {
            return "Supplier returned this";
        }
    }

    private static class MyExceptionSupplier implements
            Supplier<IllegalArgumentException>
    {
        @Override
        public IllegalArgumentException get()
        {
            return new IllegalArgumentException();
        }
    }

    private static class MyConsumer implements Consumer<String>
    {
        @Override
        public void accept(String t)
        {
            System.out.println("Consumed: " + t);
        }
    }

    public static void main(String[] args)
    {
        Optional<String> opt = Optional.of("found");
        System.out.println(opt.orElseGet(new MySupplier()));
        System.out.println(opt.orElseThrow(
                                         new MyExceptionSupplier()));
        opt.ifPresent(new MyConsumer());

        Optional<String> optNull = Optional.ofNullable(null);
        System.out.println(optNull.orElseGet(new MySupplier()));

        try
        {
            System.out.println(optNull.orElseThrow(
                                         new MyExceptionSupplier()));
        }
        catch (IllegalArgumentException e)
        {
            System.out.println("Exception caught");
        }

        // This one won't use the consumer
        optNull.ifPresent(new MyConsumer());
    }
}

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

Обратите внимание, что в последнем примере, если мы упаковываем слово Integer вместо String, мы не можем использовать IntSupplier или IntConsumer. Это связано с тем, что для orElseGet и ifPresent of Optional требуется тип, который расширяется или является супер целым числом соответственно (включая Integer или курс). IntSupplier и IntConsumer не расширяют поставщика и потребителя, поэтому мы не можем заменить их. Тем не менее, специализированный OptionalInt принимает IntSupplier и IntConsumer.

Есть несколько полезных функциональных методов (которые на удивление не были добавлены в классы-обертки или Number): filter, map и flatMap. FlatMap обрабатывает случай, когда функция отображения уже возвращает Optional и поэтому не переносит его снова. Наоборот, карта Optional обернет все, что возвращает функция отображения.

Фильтр возвращает Optional.empty (), если предикат не совпадает. Если Optional уже пуст, предикат не проверяется, хотя это не должно нас беспокоить, поскольку предикаты должны быть просто логическими тестами и не иметь побочных эффектов.

Вот быстрый пробег через:

public static void main(String args[])
{
    Optional<String> hiMsg = Optional.of("hi");

    Optional<String> hiThereMsg = hiMsg.map(x -> x + " there!");

    System.out.println(hiMsg.get()); // Original

    System.out.println(hiThereMsg.get()); // Mapped

    System.out.println(hiThereMsg.filter(x -> x.equals("hi there!"))
                 .orElse("Bye!"));

    // Filter test fails returning Optional.empty()
    System.out.println(hiThereMsg.filter(x -> x.equals("yo there!"))
                .orElse("Bye!"));

    // The Optional gets wrapped
    Optional<Optional<String>> byeMessage = hiThereMsg
                                 .map(x -> Optional.of("Bye bye!"));

    // No extra wrapping
    Optional<String> byeMessage2 = hiThereMsg
                             .flatMap(x -> Optional.of("Bye bye!"));

    System.out.println(byeMessage.get().get());
    System.out.println(byeMessage2.get());

    // This would be an error since the
    // mapping has to return Optional
    // hiThereMsg.flatMap(x -> "Bye bye!");

    // We can change the wrapped type
    Optional<Integer> five = hiThereMsg.map(x -> 5);
    System.out.println(five.get());

    Optional<Integer> six = hiThereMsg.flatMap(x -> Optional.of(6));
    System.out.println(six.get());
}

Наконец, предупреждение из самой документации Java: «Это класс, основанный на значениях; использование чувствительных к идентификации операций (включая равенство ссылок ==), хэш-кода идентификации или синхронизации в экземплярах Optional может привести к непредсказуемым результатам, и его следует избегать ». Короче говоря, не пытайтесь использовать ==, hashCode или синхронизированы по желанию. Можно использовать нормальные .equals, но если вы ожидаете совпадения, то объект, с которым вы сравниваете, также должен быть необязательным. Если оба варианта являются Optional.empty (), это считается совпадением.

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