Поток Java 8+: проверьте, что список находится в правильном порядке для двух полей моих объектов-экземпляров

Название может быть немного неопределенным, но вот что я имею (в приватизированном коде):

Класс с некоторыми полями, включая BigDecimal и Date:

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

  // Getters and Setters
}

В другом классе у меня есть список этих объектов (например, java.util.List<MyObj> myList). Теперь мне нужен поток Java 8, чтобы проверить, соответствует ли список правильному порядку как дат, так и процентов для моего валидатора.

Например, следующий список будет правдивым:

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

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

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

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

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

Одним из возможных решений может быть создание таких Pairs а затем использование ! и .anyMatch проверяя каждую отдельную Pair<MyObj>. Но я действительно не хочу создавать класс Pair только для этой цели, если это возможно.

Есть ли способ использовать .reduce или что-то, чтобы MyObj пары MyObj чтобы проверить их? Какой был бы лучший подход здесь, чтобы проверить, соответствуют ли все даты и проценты MyObj в моем списке в правильном порядке с использованием потока Java 8?

Другая возможность, возможно, сортировка списка по дате, а затем проверка, все ли они в порядке процента, если это проще, чем проверка обоих полей в одно и то же время. Тем не MyObj же проблема при сравнении пар MyObj для процента остается.

(PS: Я буду использовать его для com.vaadin.server.SerializablePredicate<MyObj> validator, и я предпочитаю Java 8 лямбда, потому что я также использовал некоторые для других валидаторов, так что это будет больше соответствовать остальным кода. Java-лямбда - это скорее предпочтение, чем требование в моем вопросе.)

Ответ 1

Ну, если вы хотите короткозамкнутую операцию, я не думаю, что существует простое решение с использованием stream-api... Я предлагаю более простой вариант, сначала определите метод, который в коротком замыкании скажет вам, отсортировано или нет, на основе некоторого параметра:

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

И называя это через:

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

Ответ 2

Я думаю, вы почти там, пытаясь использовать Stream.anyMatch. Вы можете сделать это так:

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

Мы можем использовать IntStream.range для перебора элементов списка с помощью индекса. Таким образом, мы можем ссылаться на любой элемент в списке, например, предыдущий, чтобы сравнить его.

EDIT добавляет более общую версию:

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

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

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

Или используя ссылку метода в классе ListNotOrdered:

isNotOrderedAccordingTo(myList1, ListNotOrdered::isNotOrdered)

Ответ 3

Поскольку вы упомянули, что не хотите создавать отдельный класс Pairs для этого, вы можете использовать встроенный класс для этой цели: AbstractMap.SimpleEntry.

Вы можете сделать BiPredicate который проверяет оба условия сравнения и использует их для сравнения всех пар.

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

Вышеприведенное решение с компаратором:

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

Ответ 4

Вот решение pairMap в 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);

Ответ 5

Да, вы можете использовать reduce для сравнения двух элементов (хотя это не самый лучший вариант). Вам просто нужно создать новый "пустой" элемент в результате, когда вы найдете элемент не в порядке, например:

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

Это будет true только если оба условия выполнены, в противном случае - false.

Конечно, вы могли бы сделать код лучше, включив некоторые вспомогательные методы в 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);

Имейте в виду, что это не короткое замыкание, т.е. Оно будет перемещаться по всему списку даже после того, как оно найдет неупорядоченный элемент. Единственный способ быстро выйти из потока при использовании reduce это выбросить RuntimeException тот момент, когда вы найдете первый неупорядоченный элемент, и который будет использовать исключения для управления потоком программы, что считается плохой практикой. Тем не менее, я хотел показать вам, что вы действительно можете использовать reduce для этой цели.

Для короткого замыкания, который завершится, как только он найдет первый элемент вне места, взгляните на ответ @LuCio.

Ответ 6

Я не думаю, что это проблема, которая должна быть решена с использованием потоков. Потоки применяют сопоставления и фильтрации к элементам коллекции независимо (возможно, даже распределяя обработку разных элементов на разные ядра ЦП), прежде чем снова собирать их в новую коллекцию или сводить их к какой-то накопленной стоимости. Ваша проблема связана с отношениями между различными элементами коллекции, что противоречит цели потока. Хотя могут быть решения, связанные с потоками, это будет похоже на то, чтобы забивать гвоздь в стену плоскогубцами. Классический цикл будет отлично для вас: найти первое появление элемента, нарушающего порядок, и вернуть желаемый результат! Таким образом, вам даже не нужно создавать пару.

Ответ 7

Аналогично @luis g. ответ, вы также можете использовать reduce в сочетании с Optional (пустые средства несортированными) и "минимальным" MyObj как личность:

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

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