Как выполнить итерацию потока, даже если создаются исключения?

stream.map(obj -> doMap(obj)).collect(Collectors.toList());

private String doMap(Object obj) {
    if (objectIsInvalid) {
    throw new ParseException("Object could not be parsed");
    }
}

проблема: как я могу выбросить исключение и заставить итерацию потока знать, что она не должна нарушать всю итерацию, а продолжать следующий элемент (и в конечном итоге регистрировать неудачные объекты)?

5 ответов


без исключения вы можете работать с Optionals:

stream.map(obj -> doMap(obj))
      .filter(obj -> obj.isPresent())
      .collect(Collectors.toList());

private Optional<String> doMap(Object obj) {
   if (objectIsInvalid) {
    return Optional.empty();
   }
}

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

Предположим, ваша функция картографа выглядит так:

String doMap(Object obj) {
    if (isInvalid(obj)) {
        throw new IllegalArgumentException("info about obj");
    } else {
        return obj.toString();
    }
}

возвращает результат, если объект допустим, но создает исключение, если объект недопустим. К сожалению, если вы вставляете это непосредственно в конвейер, любая ошибка остановит выполнение конвейера. То, что вы хотите, - это что-то вроде типа "либо", который может содержать либо значение, либо индикатор ошибки (который был бы исключение в Java).

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

во-первых, дали stream объектов для обработки, мы вызываем функцию отображения обернутые в вызов supplyAsync:

 CompletableFuture<String>[] cfArray = 
        stream.map(obj -> CompletableFuture.supplyAsync(() -> doMap(obj), Runnable::run))
              .toArray(n -> (CompletableFuture<String>[])new CompletableFuture<?>[n]);

(к сожалению, создание универсального массива дает непроверенное предупреждение, которое должно быть подавлены.)

странная конструкция

 CompletableFuture.supplyAsync(supplier, Runnable::run)

запускает поставщика "асинхронно" на предоставленном исполнителе Runnable::run, который просто запускает задачу сразу в этом потоке. Другими словами, он запускает поставщика синхронно.

фокус в том, что CompletableFuture экземпляр, возвращенный из этого вызова, содержит либо значение от поставщика, если оно возвращено нормально, либо содержит исключение, если поставщик бросил его. (Я игнорирую отмену здесь.) Затем мы собираем CompletableFuture экземпляры в массив. Почему массив? Это настройка для следующей части:

CompletableFuture.allOf(cfArray).join();

обычно это ожидает завершения массива CFs. Поскольку они были запущены синхронно, они уже должны быть завершены. Что важно для этого случая, так это то, что join() бросит CompletionException если любой из CFs в массиве завершилось исключительно. Если соединение завершается нормально, мы можем просто собрать возвращаемые значения. Если соединение выбрасывает исключение, мы можем либо распространить его, либо поймать его и обработать исключения, хранящиеся в CFs в массиве. Например,

try {
    CompletableFuture.allOf(cfArray).join();
    // no errors
    return Arrays.stream(cfArray)
                 .map(CompletableFuture::join)
                 .collect(toList());
} catch (CompletionException ce) {
    long errcount =
        Arrays.stream(cfArray)
              .filter(CompletableFuture::isCompletedExceptionally)
              .count();
    System.out.println("errcount = " + errcount);
    return Collections.emptyList();
}

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


Если вы хотите, чтобы средство спокойно игнорировало те элементы, которые вызывают исключение при сопоставлении, вы можете определить вспомогательный метод, который возвращает успех или ничего, как поток из 0 или 1 элементов, а затем flatMap ваш поток с этим методом.

import static java.util.stream.Collectors.toList;

import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;

public class Fish {
    private int weight;
    public Fish(int weight) { this.weight = weight; }
    public String size() {
        if (weight < 10) {
            return "small";
        } else if (weight < 50) {
            return "medium";
        } else if (weight < 100) {
            return "large";
        } else {
            throw new RuntimeException("I don't believe you");
        }   
    }

    public static <T> Stream<T> successOrNothing(Supplier<T> supplier) {
      try {
        return Stream.of(supplier.get());
      } catch (Exception e) {
        return Stream.empty();
      }
    }

    public static void main(String[] args) {
        Stream<Fish> fishes = Stream.of(new Fish(1000), new Fish(2));
        List<String> sizes = fishes
          .flatMap(fish -> successOrNothing(fish::size))
          .collect(toList());
        System.out.println(sizes);
    }
}

каждый элемент в потоке сопоставляется с 1 элементом (если вызов возвращен успешно) или 0 (если это не так). Это может быть проще или проще, чем "изменить метод для возврата null или необязательно.пустой() вместо того, чтобы бросать" шаблон, особенно когда речь идет о методе, контракт которого уже определен, и вы не хотите меняться.

предостережение: это решение по-прежнему молча игнорирует исключения; это неуместно во многих ситуациях.


Я бы сделал следующее...

stream.map(doMapLenient())
      .filter(Objects::nonNull)
      .collect(Collectors.toList());

private String doMap(Object obj) {
    if (objectIsInvalid(obj)) {
    throw new ParseException("Object could not be parsed");
    }
    return "Streams are cool";
}

private Function<Object, String> doMapLenient() {
  return obj -> {
     try {
       return doMap(obj);   
   } catch (ParseExcepion ex) {
       return null; 
   }
}

здесь вы можете добавить красиво также некоторые вход в catch part


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

Stream<Object> stream = ... // throws unchecked exceptions;
Iterator<Object> iterator = stream.iterator();
while (iterator.hasNext()) {
    try {
        Object obj = it.next();
        // process
    } catch (ParseException ex) {
        // handle
    }
}