В течение многих лет защитное программирование казалось единственным выходом. Теперь, после многих лет борьбы с проверкой нуля, мы можем воспользоваться классом Optional, который был представлен в Java 8.
Я собираюсь показать вам простой тест класса Optional и то, что с ним можно сделать. Вы можете пойти гораздо дальше.
В этом примере мы собираемся просто отобразить количество фильмов, которые данный пользователь посмотрел/продал в текущем месяце.
Вот основные классы:
public final class RentHistory {
private Integer totalInThisMonth;
private Integer totalFromTheBeginning;
public RentHistory() {
}
public RentHistory(Integer totalInThisMonth, Integer totalFromTheBeginning) {
this.totalInThisMonth = totalInThisMonth;
this.totalFromTheBeginning = totalFromTheBeginning;
}
public RentHistory(Integer totalInThisMonth) {
this.totalInThisMonth = totalInThisMonth;
}
public Integer getTotalInCurrentMonth() {
return totalInThisMonth;
}
public Integer getTotalFromTheBeginning() {
return totalFromTheBeginning;
}
}
public final class User {
private RentHistory rentHistory;
public User() {
}
public User(RentHistory rentHistory) {
this.rentHistory = rentHistory;
}
public RentHistory getRentHistory() {
return rentHistory;
}
}
Теперь мы хотим отобразить пользователю количество фильмов, которые он взял напрокат в этом месяце. Поэтому код будет выглядеть следующим образом:
public final class Test {
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println("Number of movies user has rented in this month is: " + getTotal(user.getRentHistory()));
}
private Integer getTotal(RentHistory history) {
return history.getTotalInCurrentMonth();
}
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(4)));
}
}
Пользователь получит следующее: Количество фильмов, взятых пользователем напрокат в этом месяце, составляет: 4
Теперь что произойдет, если мы создадим Rent history с помощью конструктора по умолчанию, то есть без количества фильмов, которые он посмотрел в текущем месяце:
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory()));
}
Мы получим очень красивый текст, который говорит: Количество фильмов, которые пользователь взял напрокат в этом месяце: null
Наверняка это не то, что мы хотим видеть в пользовательском интерфейсе. Что можно с этим сделать, может быть следующее:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println("Number of movies user has rented in this month is: " + getTotal(user.getRentHistory()));
}
private Integer getTotal(RentHistory history) {
return history.getTotalInCurrentMonth() == null ? 0: history.getTotalInCurrentMonth();
}
Это стандартный оборонительный подход, который мы использовали, чтобы избежать получения null — добавляем условие, проверяем на наличие null и выполняем действие. Теперь с помощью Optional мы можем сделать следующее:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
Optional<Integer> total = getTotal(user.getRentHistory());
if (total.isPresent()) {
System.out.println("Number of movies user has rented in this month is: " + total.get());
} else {
System.out.println("User has not rented any movie in the current month");
}
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
Видите, что это дало нам…. да, ровно ничего! Вы никогда не должны менять использование обычного условия на использование isPresent! Это точно не тот способ, которым следует использовать Optional. Нужно было сделать вот что:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
Помните, когда вы имеете дело с нулями, Optional.map — это то, что вы должны попробовать.
Теперь давайте поговорим о другой ситуации. Что произойдет, если мы передадим пользователя, у которого вообще нет RentHistory, давайте проверим.
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User());
}
Сейчас мы получаем следующее: Исключение в потоке «main» java.lang.NullPointerException at getTotal(Test.java:15)
Теперь мы можем сделать примерно следующее:
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
if (history == null){
return Optional.empty();
}
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
Это окончательное решение, которое мы действительно хотим, добавление проверки нуля, опять же, защитное программирование. Нет, мы не должны оставлять это так, мы должны сделать некоторый рефактор! Прежде всего, мы изменим класс User следующим образом:
public final class User {
private RentHistory rentHistory;
public User() {
}
public User(RentHistory rentHistory) {
this.rentHistory = rentHistory;
}
public Optional<RentHistory> getRentHistory() {
return Optional.ofNullable(rentHistory);
}
}
Вместо того, чтобы возвращать сущности, мы будем возвращать их опцию! Теперь изменения в классе Test.
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user rent in this month is: " + total)
.orElse("User has not rent any movie in the current month"));
}
private Optional<Integer> getTotal(Optional<RentHistory> history) {
if (!history.isPresent()){
return Optional.empty();
}
return Optional.ofNullable(history.get().getTotalInCurrentMonth());
}
Optional не предназначен для передачи в качестве аргумента! Это точно не тот путь, которым нужно идти. Мы собираемся провести рефакторинг! InteliJ уже говорит нам, что я могу внести изменения и ввести метод map, не стоит этого делать!
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(getTotal(user.getRentHistory())
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(Optional<RentHistory> history) {
return history.map(RentHistory::getTotalInCurrentMonth);
}
Выглядит немного лучше, но мы все еще передаем Optional в качестве аргумента, а это плохо! Теперь мы собираемся изменить способ. Сначала мы будем использовать пользователя, а затем, если у пользователя есть его RentHistory, мы будем использовать ее в методе getTotal. Таким образом, теперь код будет выглядеть следующим образом:
public final class Test {
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(user.getRentHistory()
.map(this::getTotal)
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User());
}
}
Приведенное выше решение почти идеально! Оно хорошо работает при неправильном вводе, но когда все в порядке, как сейчас…
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(6)));
}
…он выдает ответ: _Количество фильмов, взятых пользователем напрокат в этом месяце, составляет: Необязательно[6]
_
Это так, потому что мы оборачиваем Optional с Optional. Что необходимо сделать, так это использовать flatMap:
public final class Test {
public void displayNumberOfMoviesRentInCurrentMonth(User user) {
System.out.println(user.getRentHistory()
.flatMap(this::getTotal)
.map(total -> "Number of movies user has rented in this month is: " + total)
.orElse("User has not rented any movie in the current month"));
}
private Optional<Integer> getTotal(RentHistory history) {
return Optional.ofNullable(history.getTotalInCurrentMonth());
}
public static void main(String[] args) {
Test test = new Test();
test.displayNumberOfMoviesRentInCurrentMonth(new User(new RentHistory(6)));
}
}
Это будет окончательный подход к данному вопросу. Помните, что всегда, имея дело с нулями, старайтесь использовать Optional. Возможно, вначале это будет не так интуитивно понятно, но со временем вы будете все больше и больше привыкать к такому подходу.
Конечно, это всего лишь простой пример, простой тестовый пример, который показывает, насколько мощным является Optional. Я не говорю, что вы должны всегда использовать Optional в каждом случае, но вы должны попробовать!
Вы можете представить, как это можно использовать для чего-то более сложного и комплексного, а не только для простого отображения текста.
Этот пост основан на одном из докладов, который провел господин Виктор Рентеа. Я очень рекомендую посмотреть на этого человека в действии!
Вот git-репозиторий для этого простого примера.