web-dev-qa-db-fra.com

Traitement conditionnel des flux Java 8

Je suis intéressé par la séparation d'un flux en deux sous-flux ou plus, et le traitement des éléments de différentes manières. Par exemple, un fichier texte (volumineux) peut contenir des lignes de type A et des lignes de type B. Dans ce cas, j'aimerais procéder de la manière suivante:

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

La précédente est ma tentative de résumer la situation. En réalité, j'ai un très gros fichier texte où chaque ligne est testée par rapport à une regex; si la ligne passe, elle est traitée alors que si elle est rejetée, je souhaite mettre à jour un compteur. Ce traitement ultérieur sur les chaînes rejetées est la raison pour laquelle je n'utilise pas simplement filter.

Existe-t-il un moyen raisonnable de faire cela avec les flux, ou devrais-je me replier sur des boucles? (Je voudrais aussi que cela fonctionne en parallèle, donc les flux sont mon premier choix).

17
gdiazc

Les flux Java 8 n'ont pas été conçus pour prendre en charge ce type d'opération. De la jdk :

Un flux ne doit être exploité (en invoquant une opération de flux intermédiaire ou terminal) qu'une seule fois. Cela exclut, par exemple, les flux "fourchus", où la même source alimente deux pipelines ou plus, ou plusieurs traversées du même flux.

Si vous pouvez le stocker en mémoire, vous pouvez utiliser Collectors.partitioningBy si vous n'avez que deux types et passer avec un Map<Boolean, List>. Sinon, utilisez Collectors.groupingBy

16
Cosu

Testez simplement chaque élément et agissez en conséquence.

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

Ce comportement pourrait être caché dans une méthode d'assistance:

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);
    };
}

Ensuite, l'utilisation ressemblerait à ceci:

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

Note tangentielle

La méthode Files.lines() ne ferme pas le fichier sous-jacent, vous devez donc l'utiliser comme ceci:

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

Les variables de type Stream jettent un peu le drapeau rouge pour moi, alors je préfère gérer directement un BufferedReader

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

Bien que les effets secondaires des paramètres comportementaux soient découragés, ils ne sont pas interdits, aussi longtemps qu’il n’ya pas d’interférence, la solution la plus simple mais non la plus propre consiste à compter directement dans le filtre:

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

Tant que vous traitez tous les éléments, le résultat sera cohérent. Si vous utilisez une opération de terminal en court-circuit (dans un flux parallèle), le résultat deviendra imprévisible.

La mise à jour d'une variable atomique n'est peut-être pas la solution la plus efficace, mais dans le contexte des lignes de traitement d'un fichier, le temps système sera probablement négligeable.

Si vous souhaitez une solution propre et conviviale pour le parallèle, une approche générale consiste à implémenter une variable Collector pouvant combiner le traitement de deux opérations de collecte en fonction d'une condition. Cela nécessite que vous puissiez exprimer l'opération en aval en tant que collecteur, mais la plupart des opérations de flux peuvent être exprimées en tant que collecteur (et la tendance va vers la possibilité d'exprimer toutes les opérations de cette façon, c'est-à-dire que Java 9 ajoutera l'élément manquant filtering et flatMapping .

Vous aurez besoin d’un type de paire pour conserver deux résultats. Supposons donc un croquis comme

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

la mise en œuvre de collecteur de combinaison ressemblera

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)));
}

et peut être utilisé, par exemple, pour rassembler les éléments correspondants dans une liste et compter les non-correspondants, comme ceci:

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;

Le collecteur est compatible avec les parallèles et permet des collecteurs délégués arbitrairement complexes, mais notez qu'avec la mise en œuvre actuelle, le flux renvoyé par Files.lines risque de ne pas fonctionner aussi bien avec un traitement en parallèle, par rapport à politique de taille dans son séparateur » . Des améliorations sont prévues pour la version Java 9.

5
Holger

La façon dont je traiterais avec ceci ne consiste pas à diviser cela du tout, mais plutôt à écrire

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

Les détails varient en fonction de ce que vous voulez faire et de la manière dont vous envisagez de le faire.

2
Louis Wasserman

Voici une approche (qui ignore les précautions à prendre pour forcer le traitement conditionnel dans un flux) qui englobe un prédicat et un consommateur dans un seul prédicat avec effet secondaire:

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;
    }

}

Puis filtrez le flux:

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

Les éléments restants dans le flux n'ont pas encore été traités.

Par exemple, un parcours de système de fichiers typique utilisant une itération externe ressemble à

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 );
    }
}

Avec les flux et l'itération interne, cela devient

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

J'aimerais que Stream mette en œuvre directement la méthode de traitement. Alors nous pourrions avoir

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

Pensées?

1
tom

Eh bien, vous pouvez simplement faire

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

Pas très fonctionnel, mais il le fait de la même manière que votre exemple. Bien sûr, si elles sont parallèles, les fonctions Counter.increment() et processTypeA() doivent être thread-safe.

1
JB Nizet

Il semble qu'en réalité, vous souhaitiez traiter chaque ligne, mais différemment en fonction de certaines conditions (types).

Je pense que c'est une façon plus ou moins fonctionnelle de l'implémenter:

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