web-dev-qa-db-fra.com

Comment itérer des listes imbriquées avec des flux lambda?

J'essaie de refactoriser le code suivant en expressions lambda avec `stream, en particulier les boucles foreach imbriquées:

public static Result match (Response rsp) {
    Exception lastex = null;

    for (FirstNode firstNode : rsp.getFirstNodes()) {
        for (SndNode sndNode : firstNode.getSndNodes()) {
            try {
                if (sndNode.isValid())
                return parse(sndNode); //return the first match, retry if fails with ParseException
            } catch (ParseException e) {
                lastex = e;
            }
        }
    }

    //throw the exception if all elements failed
    if (lastex != null) {
        throw lastex;
    }

    return null;
}

Je commence par:

rsp.getFirstNodes().forEach().?? // how to iterate the nested 2ndNodes?
17
membersound

J'ai peur qu'en utilisant des streams et des lambdas, votre performance puisse en souffrir. Votre solution actuelle renvoie le premier nœud valide et analysable, mais il n'est pas possible d'interrompre une opération sur le flux telle que for-each ( source ).

De plus, comme vous pouvez avoir deux sorties différentes (résultat renvoyé ou exception levée), il ne sera pas possible de le faire avec une seule expression de ligne.

Voici ce que j'ai trouvé. Cela peut vous donner quelques idées:

public static Result match(Response rsp) throws Exception {
    Map<Boolean, List<Object>> collect = rsp.getFirstNodes().stream()
            .flatMap(firstNode -> firstNode.getSndNodes().stream()) // create stream of SndNodes
            .filter(SndNode::isValid) // filter so we only have valid nodes
            .map(node -> {
                // try to parse each node and return either the result or the exception
                try {
                    return parse(node);
                } catch (ParseException e) {
                    return e;
                }
            }) // at this point we have stream of objects which may be either Result or ParseException
            .collect(Collectors.partitioningBy(o -> o instanceof Result)); // split the stream into two lists - one containing Results, the other containing ParseExceptions

    if (!collect.get(true).isEmpty()) {
        return (Result) collect.get(true).get(0);
    }
    if (!collect.get(false).isEmpty()) {
        throw (Exception) collect.get(false).get(0); // throws first exception instead of last!
    }
    return null;
}

Comme mentionné au début, il existe un problème de performances possible car , cela tentera d'analyser chaque nœud valide .


MODIFIER:

Pour éviter d'analyser tous les nœuds, vous pouvez utiliser reduce, mais c'est un peu plus complexe et laid (et une classe supplémentaire est nécessaire). Cela montre également tous les ParseException au lieu du dernier.

private static class IntermediateResult {

    private final SndNode node;
    private final Result result;
    private final List<ParseException> exceptions;

    private IntermediateResult(SndNode node, Result result, List<ParseException> exceptions) {
        this.node = node;
        this.result = result;
        this.exceptions = exceptions;
    }

    private Result getResult() throws ParseException {
        if (result != null) {
            return result;
        }
        if (exceptions.isEmpty()) {
            return null;
        }
        // this will show all ParseExceptions instead of just last one
        ParseException exception = new ParseException(String.format("None of %s valid nodes could be parsed", exceptions.size()));
        exceptions.stream().forEach(exception::addSuppressed);
        throw exception;
    }

}

public static Result match(Response rsp) throws Exception {
    return Stream.concat(
                    Arrays.stream(new SndNode[] {null}), // adding null at the beginning of the stream to get an empty "aggregatedResult" at the beginning of the stream
                    rsp.getFirstNodes().stream()
                            .flatMap(firstNode -> firstNode.getSndNodes().stream())
                            .filter(SndNode::isValid)
            )
            .map(node -> new IntermediateResult(node, null, Collections.<ParseException>emptyList()))
            .reduce((aggregatedResult, next) -> {
                if (aggregatedResult.result != null) {
                    return aggregatedResult;
                }

                try {
                    return new IntermediateResult(null, parse(next.node), null);
                } catch (ParseException e) {
                    List<ParseException> exceptions = new ArrayList<>(aggregatedResult.exceptions);
                    exceptions.add(e);
                    return new IntermediateResult(null, null, Collections.unmodifiableList(exceptions));
                }
            })
            .get() // aggregatedResult after going through the whole stream, there will always be at least one because we added one at the beginning
            .getResult(); // return Result, null (if no valid nodes) or throw ParseException
}

EDIT2:

En général, il est également possible d'utiliser une évaluation paresseuse lors de l'utilisation d'opérateurs de terminal tels que findFirst(). Donc, avec un changement mineur des exigences (c'est-à-dire en retournant null au lieu de lancer une exception), il devrait être possible de faire quelque chose comme ci-dessous. Cependant, flatMap avec findFirst n'utilise pas d'évaluation paresseuse ( source ), donc ce code essaie d'analyser tous les nœuds.

private static class ParsedNode {
    private final Result result;

    private ParsedNode(Result result) {
        this.result = result;
    }
}

public static Result match(Response rsp) throws Exception {
    return rsp.getFirstNodes().stream()
            .flatMap(firstNode -> firstNode.getSndNodes().stream())
            .filter(SndNode::isValid)
            .map(node -> {
                try {
                    // will parse all nodes because of flatMap
                    return new ParsedNode(parse(node));
                } catch (ParseException e ) {
                    return new ParsedNode(null);
                }
            })
            .filter(parsedNode -> parsedNode.result != null)
            .findFirst().orElse(new ParsedNode(null)).result;
}
9
Jaroslaw Pawlak

Regardez flatMap:

flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
Renvoie un flux constitué des résultats du remplacement de chaque élément de ce flux par le contenu d'un flux mappé produit en appliquant la fonction de mappage fournie à chaque élément.

Exemple de code en supposant que isValid() ne lance pas

Optional<SndNode> sndNode = rsp.getFirstNodes()
  .stream()
  .flatMap(firstNode -> firstNode.getSndNodes().stream())  //This is the key line for merging the nested streams
  .filter(sndNode -> sndNode.isValid())
  .findFirst();

if (sndNode.isPresent()) {
    try {
        parse(sndNode.get());
    } catch (ParseException e) {
        lastex = e;
    }
}
18
Arkadiy

Essayez d'utiliser map qui transforme la source d'origine.

   rsp.getFirstNodes().stream().map(FirstNode::getSndNodes)
               .filter(sndNode-> sndNode.isValid())
               .forEach(sndNode->{
   // No do the sndNode parsing operation Here.
   })
2
Masudul

Vous pouvez parcourir les boucles imbriquées comme ci-dessous

allAssessmentsForJob.getBody().stream().forEach(assessment -> {
        jobAssessments.stream().forEach(jobAssessment -> {
            if (assessment.getId() == jobAssessment.getAssessmentId()) {
                jobAssessment.setAssessment(assessment);
            }
        });
    });
1
Sunny Gupta

Un peu en retard mais voici une approche lisible:

   Result = rsp.getFirstNodes()
        .stream()
        .flatMap(firstNode -> firstNode.getSndNodes.stream())
        .filter(secondNode::isValid))
        .findFirst()
        .map(node -> this.parseNode(node)).orElse(null);

Explication: vous obtenez tous les firstNodes et stream() up. Sur chaque premier nœud, vous apportez n SndNodes. Vous vérifiez chaque SndNodes pour voir trouver le premier celui qui est valide. S'il n'y a pas de SndNode valide, nous obtiendrons un null. S'il y en a un, il sera analysé dans un Result

la parseMethod () ne change pas de l'original:

public Result parseNode(SndNode node){
        try {
        ...
        ... // attempt to parsed node 
    } catch (ParseException e) {
        throw new ParseException;
    }   
} 
1
arthur

Vous pouvez utiliser ce fait que StreamSupport fournit une méthode stream qui prend une Spliterator et Iterable a une méthode spliterator.

Vous avez alors juste besoin d'un mécanisme pour aplatir votre structure en Iterable - quelque chose comme ça.

class IterableIterable<T> implements Iterable<T> {

    private final Iterable<? extends Iterable<T>> i;

    public IterableIterable(Iterable<? extends Iterable<T>> i) {
        this.i = i;
    }

    @Override
    public Iterator<T> iterator() {
        return new IIT();
    }

    private class IIT implements Iterator<T> {

        // Pull an iterator.
        final Iterator<? extends Iterable<T>> iit = i.iterator();
        // The current Iterator<T>
        Iterator<T> it = null;
        // The current T.
        T next = null;

        @Override
        public boolean hasNext() {
            boolean finished = false;
            while (next == null && !finished) {
                if (it == null || !it.hasNext()) {
                    if (iit.hasNext()) {
                        it = iit.next().iterator();
                    } else {
                        finished = true;
                    }
                }
                if (it != null && it.hasNext()) {
                    next = it.next();
                }
            }
            return next != null;
        }

        @Override
        public T next() {
            T n = next;
            next = null;
            return n;
        }
    }

}

public void test() {
    List<List<String>> list = new ArrayList<>();
    List<String> first = new ArrayList<>();
    first.add("First One");
    first.add("First Two");
    List<String> second = new ArrayList<>();
    second.add("Second One");
    second.add("Second Two");
    list.add(first);
    list.add(second);
    // Check it works.
    IterableIterable<String> l = new IterableIterable<>(list);
    for (String s : l) {
        System.out.println(s);
    }
    // Stream it like this.
    Stream<String> stream = StreamSupport.stream(l.spliterator(), false);
}

Vous pouvez désormais diffuser directement depuis votre Iterable.

Les premières recherches suggèrent que cela devrait être fait avec flatMap mais peu importe.

0
OldCurmudgeon