Статьи

Java 8 Необязательные

При программировании мы все сталкивались с самой (в) известной исключительной ситуацией NullPointerException . И я считаю, что мы все согласимся с тем, что столкновение с NullPointerException также является проблемой. Просто чтобы держать читателей в курсе, знаменитый компьютерщик Тони Хоар представил нулевые ссылки, и он считает это ошибкой в ​​миллион долларов . Мы все знаем, это очень легко реализовать, но это также довольно непредсказуемо. И именно поэтому разработчики должны быть очень осторожными.

Обычный Путь

Давайте рассмотрим 3 простых POJO следующим образом.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Employee {
  private Car car;
 
  public Car getCar() {
    return car;
  }
}
 
public class Car {
  private Insurance insurance;
 
  public Insurance getInsurance() {
    return insurance;
  }
}
 
public class Insurance {
  private String name;
 
  public String getName() {
    return name;
  }
}

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

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

01
02
03
04
05
06
07
08
09
10
11
12
public String getInsuranceName(Employee employee) {
  if (employee != null) {
    Car car = employee.getCar();
    if (car != null) {
      Insurance insurance = car.getInsurance();
      if (insurance != null) {
        return insurance.getName();
      }
    }
  }
  return "UNKNOWN";
}

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

Другой Обычный Путь

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public String getInsuranceName(Employee employee) {
  if (employee == null) {
    return "UNKNOWN";
  }
  Car car = employee.getCar();
  if (car == null) {
    return "UNKNOWN";
  }
  Insurance insurance = car.getInsurance();
  if (insurance == null) {
    return "UNKNOWN";
  }
  return insurance.getName();
}

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

Почему NULL не хорошо?

  1. Это ухудшает читабельность исходного кода
  2. Семантически неправильно представлять что-либо без значения
  3. Это противоречит идеологии Java, поскольку Java скрывает указатели от разработчиков, за исключением случаев нулевых ссылок

Альтернативы NULL

Немногие языки, такие как Scala, Groovy убрали страшное использование пустых ссылок для обозначения отсутствия значения. Подобный код может быть написан на Groovy в очень сжатой форме.

1
def name = employee?.car?.insurance?.name

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

Java’s Endeavour

Теперь мы должны спросить, что может сделать Java-разработчик для достижения аналогичной вещи, которая предотвращает возможность возникновения исключения NullPointerException при сохранении читаемого и поддерживаемого исходного кода. Разработчики языка Java выбрали подобный подход, который уже реализован на языке Groovy или Scala, но с введением нового класса — Optional

По желанию

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public final class Optional<T> {
  public static<T> Optional<T> empty() {}
  public static <T> Optional<T> of(T value) {}
  public static <T> Optional<T> ofNullable(T value) {}
  public T get() {}
  public boolean isPresent() {}
  public void ifPresent(Consumer<? super T> consumer) {}
  public Optional<T> filter(Predicate<? super T> predicate) {}
  public<U> Optional<U> map(Function<? super T, ? extends U> mapper) {}
  public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) {}
  public T orElse(T other) {}
  public T orElseGet(Supplier<? extends T> other) {}
  public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {}
}

Этот класс в основном используется для обозначения отсутствия или наличия значения. Если вы считаете, что значение может присутствовать или не может присутствовать всегда, лучше использовать дополнительный тип. В нашем предыдущем примере сотрудник может содержать или не содержать автомобиль, и поэтому лучше возвращать Optional <Car>, а не просто возвращать Car .

Давайте посмотрим, как мы могли бы разработать наш предыдущий пример:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Employee {
  private Car car;
 
  public Optional<Car> getCar() {
    return Optional.ofNullable(car);
  }
}
 
public class Car {
  private Insurance insurance;
 
  public Optional<Insurance> getInsurance() {
    return Optional.ofNullable(insurance);
  }
}
 
public class Insurance {
  private String name;
 
  public String getName() {
    return name;
  }
}

Я не обсуждал статическую фабрику метода Nullable (..), а просто рассматривал его как служебный метод-обертку, который переносит значение независимо от его ссылки.

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

Опциональное создание

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

  1. of (..) : Это позволяет создать экземпляр Optional, заключающий в себя ненулевое значение
  2. empty () : это создает пустой необязательный
  3. ofNullable (..) : Это позволяет создать необязательный экземпляр, заключающий в себе любое значение (нулевое или ненулевое)

Дополнительное извлечение и преобразование

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

  1. get () Возвращает содержащееся значение или генерирует исключение NoSuchElementException, если необязательный экземпляр пуст

Но как мы должны это использовать?

1
2
3
4
Car car = employee.getCar();
if (employee != null) {
  car = employee.getCar();
}

Это то, что мы в основном делаем, чтобы уклониться от NullPointerException . Теперь с Java 8 Необязательно , мы можем написать то же самое следующим образом:

1
2
3
4
Optional<Car> car = employee.getCar();
if (!car.isEmpty()) {
  Car car = car.get();
}

Но вы считаете это улучшением по сравнению с неприятными нулевыми проверками?

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

Давайте рассмотрим предыдущий метод:

1
2
3
public String getInsuranceName(Employee employee) {
  return employee.getCar().getInsurance().getName();
}

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

Если мы включим public String Optional в определение хорошего API, это можно было бы сделать более кратким способом:

1
2
3
4
5
6
public String getInsuranceName(Optional<Employee> employee) {
  return employee.flatMap(Employee::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("UNKNOWN");
}

Разве это не очень хороший и чистый подход? Я знаю, что это сбивает с толку некоторых программистов, которые еще не знакомы с Java Streams API. Я настоятельно рекомендую иметь быстрое понимание потоков Java 8, чтобы понять всю прелесть дополнительных функций.

Другим примером может быть получение страхового имени, если имя человека начинается с «P»

1
2
3
4
5
6
7
public String getInsuranceName(Optional<Employee> employee) {
  return employee.filter(e-> e.getName().startsWith("P"))
                 .flatMap(Employee::getCar)
                 .flatMap(Car::getInsurance)
                 .map(Insurance::getName)
                 .orElse("UNKNOWN");
}

Практика проектирования

Теперь я хотел бы поделиться некоторыми идеями о разработке ранее обсуждавшихся POJO.

Практика проектирования API 1

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Employee {
  private Optional<Car> car;
 
  public Optional<Car> getCar() {
    return car;
  }
}
 
public class Car {
  private Optional<Insurance> insurance;
 
  public Insurance getInsurance() {
    return insurance;
  }
}
 
public class Insurance {
  private String name;
 
  public String getName() {
    return name;
  }
}

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

Практика разработки API 2

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Employee {
  private Car car;
 
  public Optional<Car> getCar() {
    return Optional.ofNullable(car);
  }
}
 
public class Car {
  private Insurance insurance;
 
  public Optional<Insurance> getInsurance() {
    return Optional.ofNullable(insurance);
  }
}
 
public class Insurance {
  private String name;
 
  public String getName() {
    return name;
  }
}

Это также довольно интуитивно понятно, но в нем отсутствует идея о четком показе отсутствия экземпляра члена. Чтобы понять любую систему, разработчики всегда должны сначала понять объектную модель, а понимание объектной модели требует от нас понимания объектов предметной области. В этом сценарии сотрудник — это объект домена, в котором есть автомобиль, как если бы он был обязательным для сотрудника. Но на самом деле работник может иметь или не иметь автомобиль. Мы можем достичь этого, когда получим или получим его значение ( getCar () ), и тогда мы сможем заметить его вероятность отсутствия содержимого, так как метод возвращает значение Optional .

Что использовать?

Это зависит исключительно от разработчиков. Лично я предпочитаю первый подход, так как он понятен при понимании модели предметной области, тогда как второй подход имеет преимущества в области разделения. Так как Optional не реализует Serializable , он не сериализуем в нашем первом подходе. Если мы используем DTO, мы можем адаптировать нашу реализацию ко второму подходу.

Необязательный в методе или аргументе конструктора

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

С другой стороны, мы должны заплатить цену загрязнения базы кода с помощью Optional s. Это единственное желание разработчика — использовать его осторожно. Лично я предпочитаю не использовать Optional в аргументах метода, в то время как при необходимости мы можем по-прежнему заключать его в экземпляр Optional и выполнять над ним необходимые операции.

Необязательный в методе Возвращаемый тип

Архитектор языка Java Брайан Гетц также советует возвращать Optional в методах, если есть возможность вернуть null. Мы уже видели это в нашей Практике проектирования API 2.

Бросить исключение из методов или вернуть необязательно

С годами разработчики Java следуют обычному способу создания исключений, чтобы обозначить ошибочную ситуацию при вызове метода.

01
02
03
04
05
06
07
08
09
10
11
public static InputStream getInputStream(final String path) {
        checkNotNull(path, "Path cannot be null");
        final URL url = fileSystem.getEntry(path);
        InputStream xmlStream;
        try {
            xmlStream = url.openStream();
            return xmlStream;
        } catch (final IOException ex) {
            throw new RuntimeException(ex);
        }
}

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

01
02
03
04
05
06
07
08
09
10
11
public static Optional<InputStream> getInputStream(final String path) {
        checkNotNull(path, "Path cannot be null");
        final URL url = fileSystem.getEntry(path);
        InputStream xmlStream;
        try {
            xmlStream = url.openStream();
            return Optional.of(xmlStream);
        } catch (final IOException ex) {
            return Optional.empty();
        }
}

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

Необязательный тип возврата в приватных методах

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

Для лучшего понимания я сформулировал пример следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private void process(final String data) {
        try {
            final ItemList nList = doc.getChildNodes();
 
            for (int temp = 0; temp < nList.getLength(); temp++) {
                final Node nNode = nList.item(temp);
                final String key = nNode.getName();
                final String value = nNode.getValue();
                values.put(getAttribute(key).orElseThrow(IllegalArgumentException::new), value);
            }
        } catch (final Exception ex) {
            logger.error("{}", ex.getMessage(), ex);
        }
}
 
private Optional<Attribute> getAttribute(final String key) {
        return Arrays
                      .stream(Attribute.values())
                      .filter(x -> x.value()
                                    .filter(y -> y.equalsIgnoreCase(key))
                                    .isPresent())
                      .findFirst();
}
 
public static enum Attribute {
 
    A ("Sample1"),
    B ("Sample2"),
    C ("Sample3");
     
    private String value;
     
    private Attribute(String value) {
        this.value = value;
    }
     
    public Optional<String> value() {
        return Optional.ofNullable(value);
    }
 
}

Я мог бы написать второй метод более обычным способом:

1
2
3
4
5
6
7
8
9
private Attribute getAttribute(final String key) {
        for (final Attribute attribute : Attribute.values()) {
            Optional<String> value = attribute.value();
            if (value.isPresent() && value.get().equalsIgnoreCase(key)) {
                return attribute;
            }
        }
        throw new IllegalArgumentException();
}

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

В качестве первого примера рассмотрим код, необходимый для реализации метода для вывода списка файлов по указанному пути в Java.

01
02
03
04
05
06
07
08
09
10
11
public static List<String> listFiles(String file) {
 
    List<String> files;
    try {
        files = Files.list(Paths.get(path));
    } catch (IOException e) {
        files = Arrays.asList("Could not list");
    }
 
    return files;
}

Мы могли бы получить более сжатый код следующим образом:

1
2
3
4
5
6
public static List<String> listFiles(String path) {
         
    return Files.list(Paths.get(path))
                .filter(Files::isRegularFile)
                .collect(toList());
}

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

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

Ссылка: Java 8 Дополнительно от нашего партнера JCG Амит Мондал в блоге Java Разное .