Статьи

Остерегайтесь findFirst () и findAny ()

После фильтрации Java 8 Stream обычно используют findFirst() или findAny() чтобы получить элемент, который пережил фильтр. Но это не может сделать то, что вы действительно имели в виду, и могут появиться тонкие ошибки.

Так что же не так с findFirst() и findAny() ?

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

Простой пример выглядит так:

1
2
3
4
5
public Optional<Customer> findCustomer(String customerId) {
    return customers.stream()
            .filter(customer -> customer.getId().equals(customerId))
            .findFirst();
}

Конечно, это просто модная версия старого доброго цикла for-each:

1
2
3
4
5
6
public Optional<Customer> findCustomer(String customerId) {
    for (Customer customer : customers)
        if (customer.getId().equals(customerId))
            return Optional.of(customer);
    return Optional.empty();
}

Но оба варианта содержат одну и ту же потенциальную ошибку: они основаны на неявном предположении, что с любым данным идентификатором может быть только один клиент.

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

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

Но во многих случаях я вижу в дикой природе, это не так. Возможно, клиенты были загружены только из внешнего источника, который не дает никаких гарантий относительно уникальности их идентификаторов. Возможно, существующая ошибка позволила двум книгам с одинаковым ISBN. Может быть, поисковый термин позволяет удивительно много непредвиденных совпадений (кто-нибудь говорил регулярные выражения?).

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

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

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

Так что, конечно, в findFirst() и findAny() нет ничего плохого. Но их легко использовать таким образом, что это приводит к ошибкам в моделируемой доменной логике.

Сбой Быстро

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public Optional<Customer> findOnlyCustomer(String customerId) {
    boolean foundCustomer = false;
    Customer resultCustomer = null;
    for (Customer customer : customers)
        if (customer.getId().equals(customerId))
            if (!foundCustomer) {
                foundCustomer = true;
                resultCustomer = customer;
            } else {
                throw new DuplicateCustomerException();
            }
  
    return foundCustomer
            ? Optional.of(resultCustomer)
            : Optional.empty();
}

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

Выполняет сокращение элементов этого потока, используя ассоциативную функцию накопления, и возвращает необязательный параметр, описывающий уменьшенное значение, если оно есть. Это эквивалентно:

Stream.reduce

01
02
03
04
05
06
07
08
09
10
11
boolean foundAny = false;
T result = null;
for (T element : this stream) {
    if (!foundAny) {
        foundAny = true;
        result = element;
    }
    else
        result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();

но не обязательно выполнять последовательно.

Разве это не похоже на наш цикл выше ?! Сумасшедшее совпадение …

Поэтому все, что нам нужно, это аккумулятор, который выбрасывает желаемое исключение, как только оно вызывается:

1
2
3
4
5
6
7
public Optional<Customer> findOnlyCustomerWithId_manualException(String customerId) {
    return customers.stream()
            .filter(customer -> customer.getId().equals(customerId))
            .reduce((element, otherElement) -> {
                throw new DuplicateCustomerException();
            });
}

Это выглядит немного странно, но делает то, что мы хотим. Чтобы сделать его более читабельным, мы должны поместить его в служебный класс Stream и дать ему хорошее имя:

01
02
03
04
05
06
07
08
09
10
public static <T> BinaryOperator<T> toOnlyElement() {
    return toOnlyElementThrowing(IllegalArgumentException::new);
}
  
public static <T, E extends RuntimeException> BinaryOperator<T>
toOnlyElementThrowing(Supplier<E> exception) {
    return (element, otherElement) -> {
        throw exception.get();
    };
}

Теперь мы можем назвать это следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
// if a generic exception is fine
public Optional<Customer> findOnlyCustomer(String customerId) {
    return customers.stream()
            .filter(customer -> customer.getId().equals(customerId))
            .reduce(toOnlyElement());
}
  
// if we want a specific exception
public Optional<Customer> findOnlyCustomer(String customerId) {
    return customers.stream()
            .filter(customer -> customer.getId().equals(customerId))
            .reduce(toOnlyElementThrowing(DuplicateCustomerException::new));
}

Как это для намерения раскрытия кода?

Это материализует весь поток.

Следует отметить, что, в отличие от findFirst() и findAny() , это, конечно, не короткая операция, которая материализует весь поток. То есть, если действительно есть только один элемент. Обработка, конечно, останавливается, как только встречается второй элемент.

отражение

Мы видели, как findFirst() и findAny() недостаточно для выражения предположения о том, что в потоке остается не более одного элемента. Если мы хотим выразить это предположение и убедиться, что код быстро завершается с ошибкой, если он нарушен, нам нужно reduce(toOnlyElement()) .

Спасибо Борису Терзичу за то, что он осознал это несоответствие намерений.

Ссылка: Остерегайтесь findFirst () и findAny () от нашего партнера по JCG Николая Парлога в блоге CodeFx .