web-dev-qa-db-fra.com

Flux Java 8+: vérifie si la liste est dans le bon ordre pour deux champs de mes instances d'objet

Le titre est peut-être un peu vague, mais voici ce que j'ai (dans le code privatisé):

Une classe avec certains champs, y compris BigDecimal et Date:

class MyObj{
  private Java.math.BigDecimal percentage;
  private Java.util.Date date;
  // Some more irrelevant fields

  // Getters and Setters
}

Dans une autre classe, j'ai une liste de ces objets (c'est-à-dire Java.util.List<MyObj> myList). Ce que je veux maintenant, c'est un flux Java 8 pour vérifier si la liste est dans le bon ordre pour les dates et les pourcentages pour mon validateur.

Par exemple, la liste suivante serait la vérité:

[ MyObj { percentage = 25, date = 01-01-2018 },
  MyObj { percentage = 50, date = 01-02-2018 },
  MyObj { percentage = 100, date = 15-04-2019 } ]

Mais cette liste serait falsey car le pourcentage n’est pas dans le bon ordre:

[ MyObj { percentage = 25, date = 01-01-2018 },
  MyObj { percentage = 20, date = 01-02-2018 },
  MyObj { percentage = 100, date = 15-04-2019 } ]

Et cette liste serait aussi falsey car les dates ne sont pas dans le bon ordre:

[ MyObj { percentage = 25, date = 10-03-2018 },
  MyObj { percentage = 50, date = 01-02-2018 },
  MyObj { percentage = 100, date = 15-04-2019 } ]

Une solution possible pourrait être de créer Pairscomme ceci puis d'utiliser un ! et .anyMatch vérifiant chaque Pair<MyObj> individuel. Mais je ne veux pas vraiment créer une classe Pair uniquement dans ce but, si possible.

Existe-t-il peut-être un moyen d'utiliser .reduce ou un moyen de survoler des paires de MyObj pour les vérifier? Quelle serait la meilleure approche ici pour vérifier si toutes les dates et tous les pourcentages de la MyObj de ma liste sont dans le bon ordre en utilisant le flux Java 8?

Une autre possibilité est peut-être de trier la liste par date, puis de vérifier si elles sont toutes en ordre de pourcentage, si cela est plus facile que de vérifier que les deux champs sont identiques. Le même problème avec la comparaison des paires de MyObj pour le pourcentage demeure, cependant.

(PS: Je vais l’utiliser pour un com.vaadin.server.SerializablePredicate<MyObj> validator, et je préfère un lambda Java 8 car j’en ai également utilisé pour les autres validateurs, ce qui correspondrait mieux au reste du code. Le lambda Java 8 est plus une préférence que l'exigence dans ma question cependant.)

17
Kevin Cruijssen

Eh bien, si vous voulez une opération de court-circuit, je ne pense pas qu’une solution facile utilisant stream-api existe ... Je vous en propose une plus simple. Commencez par définir une méthode qui vous indiquera si votre liste est triés ou non, en fonction de certains paramètres:

 private static <T, R extends Comparable<? super R>> boolean isSorted(List<T> list, Function<T, R> f) {
    Comparator<T> comp = Comparator.comparing(f);
    for (int i = 0; i < list.size() - 1; ++i) {
        T left = list.get(i);
        T right = list.get(i + 1);
        if (comp.compare(left, right) >= 0) {
            return false;
        }
    }

    return true;
}

Et en l'appelant via:

 System.out.println(
          isSorted(myList, MyObj::getPercentage) && 
          isSorted(myList, MyObj::getDate));
17
Eugene

Je pense que vous êtes presque là en essayant d'utiliser Stream.anyMatch. Vous pouvez le faire comme ceci:

private static boolean isNotOrdered(List<MyObj> myList) {
    return IntStream.range(1, myList.size()).anyMatch(i -> isNotOrdered(myList.get(i - 1), myList.get(i)));

}

private static boolean isNotOrdered(MyObj before, MyObj after) {
    return before.getPercentage().compareTo(after.getPercentage()) > 0 ||
            before.getDate().compareTo(after.getDate()) > 0;
}

Nous pouvons utiliser IntStream.range pour parcourir les éléments de la liste en utilisant un index. De cette façon, nous pouvons faire référence à n’importe quel élément de la liste, par exemple. le précédent pour le comparer.

EDITajout d'une version plus générique:

private static boolean isNotOrderedAccordingTo(List<MyObj> myList, BiPredicate<MyObj, MyObj> predicate) {
    return IntStream.range(1, myList.size()).anyMatch(i-> predicate.test(myList.get(i - 1), myList.get(i)));
}

Ceci peut être appelé comme suit en utilisant le prédicat ci-dessus:

isNotOrderedAccordingTo(myList1, (before, after) -> isNotOrdered(before, after));

Ou en utilisant une référence de méthode dans une classe ListNotOrdered:

isNotOrderedAccordingTo(myList1, ListNotOrdered::isNotOrdered)
4
LuCio

Voici la solution par pairMap dans StreamEx

StreamEx.of(1, 2, 3, 5).pairMap((a, b) -> a <= b).allMatch(e -> e); // true

StreamEx.of(1, 2, 5, 3).pairMap((a, b) -> a <= b).allMatch(e -> e); // false

// your example:
StreamEx.of(myList)
   .pairMap((a, b) -> a.getPercentage().compareTo(b.getPercentage()) <= 0 && !a.getDate().after(b.getDate()))
   .allMatch(e -> e);
2
user_3380739

Puisque vous avez indiqué que vous ne souhaitiez pas créer une classe séparée Pairs pour cela, vous pouvez utiliser une classe intégrée à cette fin: AbstractMap.SimpleEntry.

Vous pouvez créer une variable BiPredicate qui vérifie vos conditions de comparaison et vous permet de comparer toutes les paires.

BiPredicate<MyObj,MyObj> isIncorrectOrder = (o1,o2) -> {
    boolean wrongOrder = o1.getDate().after(o2.getDate());
    return wrongOrder ? wrongOrder : o1.getPercentage().compareTo(o2.getPercentage()) > 0;
};

boolean isNotSorted =  IntStream.range(1,myObjs.size())
        .anyMatch(i -> isIncorrectOrder.test(myObjs.get(i-1),myObjs.get(i)));

La solution ci-dessus avec comparateur:

Comparator<MyObj> comparator = (o1, o2) -> {
    boolean wrongOrder = o1.getDate().after(o2.getDate());
    return wrongOrder ? 1 : o1.getPercentage().compareTo(o2.getPercentage());
};

Predicate<AbstractMap.SimpleEntry<MyObj,MyObj>> isIncorrectOrder = pair ->  comparator.compare(pair.getKey(),pair.getValue()) > 0;

boolean isNotSorted =  IntStream.range(1,myObjs.size())
         .mapToObj(i -> new AbstractMap.SimpleEntry<>(myObjs.get(i-1),myObjs.get(i)))
         .anyMatch(isIncorrectOrder);
2
Pankaj Singhal

Oui, vous pouvez utiliser reduce pour comparer deux éléments (bien que ce ne soit pas la meilleure option). Vous devez simplement créer un nouvel élément "vide" comme résultat lorsque vous trouvez un article hors d'usage, comme ceci:

    boolean fullyOrdered = myList.stream()
            .reduce((a, b) -> {
                if ((a.percentage == null && a.date == null) || // previous item already failed 
                        a.percentage.compareTo(b.percentage) > 0 || // pct out of order
                        a.date.after(b.date)) { // date out of order
                    return new MyObj(null, null); // return new empty MyObj
                } else {
                    return b;
                }
            })
            .filter(a -> a.percentage != null || a.date != null)
            .isPresent();

    System.out.println("fullyOrdered = " + fullyOrdered);

Ceci imprimera true uniquement si vos deux conditions sont remplies, false sinon.

Bien sûr, vous pourriez rendre le code plus agréable en incluant des méthodes auxiliaires dans MyObj:

class MyObj {
    // ...
    public MyObj() {}
    public static MyObj EMPTY = new MyObj();
    public boolean isEmpty() {
        return percentage == null && date == null;
    }
    public boolean comesAfter(MyObj other) {
        return this.percentage.compareTo(other.percentage) > 0 ||
               this.date.after(other.date);
    }
}
// ...
        boolean fullyOrdered = myList.stream()
                .reduce((a, b) -> (a.isEmpty() || a.comesAfter(b)) ? MyObj.EMPTY : b)
                .filter(b -> !b.isEmpty())
                .isPresent();

        System.out.println("fullyOrdered = " + fullyOrdered);

Sachez qu’il ne s’agit pas d’un court-circuit, c’est-à-dire qu’il parcourra toute la liste même après avoir trouvé un élément non commandé. La seule façon de sortir du flux de manière précoce en utilisant reduce serait de lancer une RuntimeException dès que vous trouverez le premier élément non ordonné, ce qui consisterait à utiliser des exceptions pour contrôler le déroulement du programme, ce qui est considéré comme une mauvaise pratique. Néanmoins, je voulais vous montrer que vous pouvez effectivement utiliser reduce à cette fin.

Pour une approche de court-circuit qui se terminera dès qu’il trouvera le premier élément déplacé, jetez un coup d’œil à la réponse de @ LuCio.

0
walen

Je ne pense pas que ce soit un problème qui devrait être résolu en utilisant des flux. Les flux appliquent indépendamment des mappages et des filtrages aux éléments d'une collection (probablement même en répartissant le traitement de différents éléments sur différents cœurs de processeur) avant de les rassembler dans une nouvelle collection ou de les réduire à une sorte de valeur accumulée. Votre problème implique des relations entre différents éléments d'une collection qui contredisent le but d'un flux. Bien qu'il puisse y avoir des solutions impliquant des ruisseaux, ce serait comme enfoncer un clou dans le mur avec une pince. Une boucle classique vous conviendra parfaitement: trouvez la première occurrence d’un élément qui casse l’ordre et renvoyez le résultat souhaité! Ainsi, vous n'auriez même pas besoin de créer une paire.

0
Amadán

Comme pour la réponse de @luis g., Vous pouvez également utiliser reduce en combinaison avec Optional (vide signifie non trié) et une "minimale" MyObj comme identité:

 boolean isSorted = list.stream()
            .map(Optional::of)
            .reduce(Optional.of(new MyObj(BigDecimal.ZERO, Date.from(Instant.Epoch))),
                    (left, right) -> left.flatMap(l -> right.map(r -> l.date.compareTo(r.date)<= 0 && l.percentage.compareTo(r.percentage) <= 0 ? r : null)))
            .isPresent();

Notez que la fonction d'accumulation (BinaryOperator) devrait être associative, ce qu'elle n'est pas dans ce cas. En outre, il ne s'agit pas non plus d'un court-circuit.

0
sfiss