it-swarm.com.ru

Условная обработка потоков Java 8

Я заинтересован в разделении потока на два или более подпотока и обработке элементов различными способами. Например, (большой) текстовый файл может содержать строки типа A и строки типа B, и в этом случае я хотел бы сделать что-то вроде:

File.lines(path)
.filter(line -> isTypeA(line))
.forEachTrue(line -> processTypeA(line))
.forEachFalse(line -> processTypeB(line))

Предыдущее - моя попытка абстрагировать ситуацию. На самом деле у меня есть очень большой текстовый файл, в котором каждая строка проверяется на соответствие регулярному выражению; если линия проходит, то она обрабатывается, тогда как если она отклонена, то я хочу обновить счетчик. Эта дальнейшая обработка отклоненных строк - вот почему я не просто использую filter.

Есть ли какой-нибудь разумный способ сделать это с потоками, или мне придется вернуться к циклам? (Я хотел бы, чтобы это также выполнялось параллельно, поэтому потоки - мой первый выбор).

17
gdiazc

Потоки Java 8 не были предназначены для поддержки такого рода операций. Из JDK :

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

Если вы можете сохранить его в памяти, вы можете использовать Collectors.partitioningBy, если у вас есть только два типа, и используйте Map<Boolean, List>. В противном случае используйте Collectors.groupingBy

16
Cosu

Просто протестируйте каждый элемент и действуйте соответственно.

lines.forEach(line -> {
    if (isTypeA(line)) processTypeA(line);
    else processTypeB(line);
});

Это поведение может быть скрыто во вспомогательном методе:

public static <T> Consumer<T> branch(Predicate<? super T> test, 
                                     Consumer<? super T> t, 
                                     Consumer<? super T> f) {
    return o -> {
        if (test.test(o)) t.accept(o);
        else f.accept(o);
    };
}

Тогда использование будет выглядеть так:

lines.forEach(branch(this::isTypeA, this::processTypeA, this::processTypeB));

Тангенциальная нота

Метод Files.lines() не закрывает основной файл, поэтому вы должны использовать его следующим образом:

try (Stream<String> lines = Files.lines(path, encoding)) {
  lines.forEach(...);
}

Переменные типа Stream выдают мне красный флажок, поэтому я предпочитаю управлять BufferedReader напрямую:

try (BufferedReader lines = Files.newBufferedReader(path, encoding)) {
    lines.lines().forEach(...);
}
11
erickson

Хотя побочные эффекты в поведенческих параметрах не приветствуются, они не запрещены, если нет помех, поэтому самое простое, хотя и не самое чистое решение - это подсчитать прямо в фильтре:

AtomicInteger rejected=new AtomicInteger();
Files.lines(path)
    .filter(line -> {
        boolean accepted=isTypeA(line);
        if(!accepted) rejected.incrementAndGet();
        return accepted;
})
// chain processing of matched lines

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

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

Если вы хотите получить чистое, параллельное дружественное решение, один общий подход заключается в реализации Collector, который может комбинировать обработку двух операций сбора в зависимости от условия. Это требует, чтобы вы могли выразить последующую операцию как коллектор, но большинство потоковых операций может быть выражено как коллектор (и наблюдается тенденция к возможности выражать все операции таким образом, т.е. Java 9 добавит отсутствующие в настоящее время filtering и flatMapping .

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

class Pair<A,B> {
    final A a;
    final B b;
    Pair(A a, B b) {
        this.a=a;
        this.b=b;
    }
}

реализация объединяющего коллектора будет выглядеть

public static <T, A1, A2, R1, R2> Collector<T, ?, Pair<R1,R2>> conditional(
        Predicate<? super T> predicate,
        Collector<T, A1, R1> whenTrue, Collector<T, A2, R2> whenFalse) {
    Supplier<A1> s1=whenTrue.supplier();
    Supplier<A2> s2=whenFalse.supplier();
    BiConsumer<A1, T> a1=whenTrue.accumulator();
    BiConsumer<A2, T> a2=whenFalse.accumulator();
    BinaryOperator<A1> c1=whenTrue.combiner();
    BinaryOperator<A2> c2=whenFalse.combiner();
    Function<A1,R1> f1=whenTrue.finisher();
    Function<A2,R2> f2=whenFalse.finisher();
    return Collector.of(
        ()->new Pair<>(s1.get(), s2.get()),
        (p,t)->{
            if(predicate.test(t)) a1.accept(p.a, t); else a2.accept(p.b, t);
        },
        (p1,p2)->new Pair<>(c1.apply(p1.a, p2.a), c2.apply(p1.b, p2.b)),
        p -> new Pair<>(f1.apply(p.a), f2.apply(p.b)));
}

и может использоваться, например, для сбора совпадающих элементов в список и подсчета несоответствующих, например так:

Pair<List<String>, Long> p = Files.lines(path)
  .collect(conditional(line -> isTypeA(line), Collectors.toList(), Collectors.counting()));
List<String> matching=p.a;
long nonMatching=p.b;

Сборщик дружественен к параллелям и допускает произвольно сложные сборщики делегатов, но учтите, что в текущей реализации поток, возвращаемый Files.lines, может не так хорошо работать при параллельной обработке, по сравнению с «Reader # lines () плохо распараллеливается из-за неконфигурируемого пакета размерная политика в ее разделителе » . Улучшения запланированы на выпуск Java 9.

5
Holger

То, как я с этим справлюсь, - это вовсе не разделить это, а написать

Files.lines(path)
   .map(line -> {
      if (condition(line)) {
        return doThingA(line);
      } else {
        return doThingB(line);
      }
   })...

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

2
Louis Wasserman

Вот подход (который игнорирует предостережения о принудительной обработке условной обработки в потоке), который заключает предикат и потребителя в один предикат с побочным эффектом:

public static class StreamProc {

    public static <T> Predicate<T> process( Predicate<T> condition, Consumer<T> operation ) {
        Predicate<T> p = t -> { operation.accept(t); return false; };
        return (t) -> condition.test(t) ? p.test(t) : true;
    }

}

Затем отфильтруйте поток:

someStream
    .filter( StreamProc.process( cond1, op1 ) )
    .filter( StreamProc.process( cond2, op2 ) )
    ...
    .collect( ... )

Элементы, оставшиеся в потоке, еще не обработаны.

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

File[] files = dir.listFiles();
for ( File f : files ) {
    if ( f.isDirectory() ) {
        this.processDir( f );
    } else if ( f.isFile() ) {
        this.processFile( f );
    } else {
        this.processErr( f );
    }
}

С потоками и внутренней итерацией это становится

Arrays.stream( dir.listFiles() )
    .filter( StreamProc.process( f -> f.isDirectory(), this::processDir ) )
    .filter( StreamProc.process( f -> f.isFile(), this::processFile ) )
    .forEach( f -> this::processErr );

Я бы хотел, чтобы Stream реализовал метод процесса напрямую. Тогда мы могли бы иметь

Arrays.stream( dir.listFiles() )
    .process( f -> f.isDirectory(), this::processDir ) )
    .process( f -> f.isFile(), this::processFile ) )
    .forEach( f -> this::processErr );

Мысли?

1
tom

Ну, вы можете просто сделать

Counter counter = new Counter();
File.lines(path)
    .forEach(line -> {
        if (isTypeA(line)) {
            processTypeA(line);
        }
        else {
            counter.increment();
        }
    });

Не очень функциональный стиль, но он делает это так же, как ваш пример. Конечно, если параллельно, и Counter.increment(), и processTypeA() должны быть поточно-ориентированными.

1
JB Nizet

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

Я думаю, что это более или менее функциональный способ его реализации:

public static void main(String[] args) {
    Arrays.stream(new int[] {1,2,3,4}).map(i -> processor(i).get()).forEach(System.out::println);
}

static Supplier<Integer> processor(int i) {
    return tellType(i) ? () -> processTypeA(i) : () -> processTypeB(i);
}

static boolean tellType(int i) {
    return i % 2 == 0;
}

static int processTypeA(int i) {
    return i * 100;
}

static int processTypeB(int i) {
    return i * 10;
}
0
Oleg Mikheev