Java 8 Stream API - выберите самый низкий ключ после группы

У меня есть поток объектов Foo.

class Foo {
    private int variableCount;
    public Foo(int vars) {
        this.variableCount = vars; 
    }
    public Integer getVariableCount() { 
      return variableCount; 
    }
}

Мне нужен список Foo, у которого все самые низкие переменные.

Например

new Foo(3), new Foo(3), new Foo(2), new Foo(1), new Foo(1)

Я хочу, чтобы поток возвращал последние 2 Foo

Я попытался сделать сбор с группировкой

.collect(Collectors.groupingBy((Foo foo) -> {
                    return foo.getVariableCount();
})

И это возвращает Map<Integer, List<Foo>> и я не уверен, как преобразовать это в то, что я хочу.

заранее спасибо

Ответ 1

Вот решение, которое:

  1. Только поток только один раз.
  2. Не создает карту или другую структуру, которая содержит все входные элементы (если только значения переменных не совпадают), сохраняя только те, которые в настоящее время являются минимальными.
  3. O (n) время, O (n). Вполне возможно, что все Foo имеют одинаковое количество переменных, и в этом случае это решение будет хранить все элементы, такие как другие решения. Но на практике, с различными, разнообразными значениями и более высокой мощностью, количество элементов в списке, вероятно, будет намного ниже.

отредактированный

Я улучшил свое решение в соответствии с предложениями в комментариях.

Я реализовал объект-аккумулятор, который для этого выполняет функции Collector.

/**
 * Accumulator object to hold the current min
 * and the list of Foos that are the min.
 */
class Accumulator {
    Integer min;
    List<Foo> foos;

    Accumulator() {
        min = Integer.MAX_VALUE;
        foos = new ArrayList<>();
    }

    void accumulate(Foo f) {
        if (f.getVariableCount() != null) {
            if (f.getVariableCount() < min) {
                min = f.getVariableCount();
                foos.clear();
                foos.add(f);
            } else if (f.getVariableCount() == min) {
                foos.add(f);
            }
        }
    }

    Accumulator combine(Accumulator other) {
        if (min < other.min) {
            return this;
        }
        else if (min > other.min) {
            return other;
        }
        else {
            foos.addAll(other.foos);
            return this;
        }
    }

    List<Foo> getFoos() { return foos; }
}

Тогда все, что нам нужно сделать, это collect, ссылаясь на методы аккумулятора для своих функций.

List<Foo> mins = foos.stream().collect(Collector.of(
    Accumulator::new,
    Accumulator::accumulate,
    Accumulator::combine,
    Accumulator::getFoos
    )
);

Тестирование с помощью

List<Foo> foos = Arrays.asList(new Foo(3), new Foo(3), new Foo(2), new Foo(1), new Foo(1), new Foo(4));

Выход (с подходящей toString определенной на Foo):

[Foo{1}, Foo{1}]

Ответ 2

Вы можете использовать отсортированную карту для группировки, а затем просто получить первую запись. Что-то вроде строк:

Collectors.groupingBy(
    Foo::getVariableCount,
    TreeMap::new,
    Collectors.toList())
.firstEntry()
.getValue()

Ответ 3

ЕСЛИ вы нормально потоки (итерации) дважды:

private static List<Foo> mins(List<Foo> foos) {
    return foos.stream()
            .map(Foo::getVariableCount)
            .min(Comparator.naturalOrder())
            .map(x -> foos.stream()
                          .filter(y -> y.getVariableCount() == x)
                          .collect(Collectors.toList()))
            .orElse(Collections.emptyList());
}

Ответ 4

Чтобы избежать создания карты, вы можете использовать два потока:

  • первый находит минимальное значение.
  • второй фильтрует элементы с этим значением.

Это могло бы дать:

List<Foo> foos = ...;
int min = foos.stream()
              .mapToInt(Foo::getVariableCount)
              .min()
              .orElseThrow(RuntimeException::new); // technical error

List<Foo> minFoos = foos.stream()
    .filter(f -> f.getVariableCount() == min)
    .collect(Collectors.toList());

Ответ 5

Чтобы избежать создания всей карты, а также избегая потоковой передачи дважды, я скопировал пользовательский сборник fooobar.com/questions/93157/... и изменил его для работы с min вместо max. Я даже не знал, что пользовательские коллекционеры были возможны, поэтому я благодарю @lexicore за то, что указал мне в этом направлении.

Это результирующая функция minAll

public static <T, A, D> Collector<T, ?, D> minAll(Comparator<? super T> comparator,
                                                  Collector<? super T, A, D> downstream) {
    Supplier<A> downstreamSupplier = downstream.supplier();
    BiConsumer<A, ? super T> downstreamAccumulator = downstream.accumulator();
    BinaryOperator<A> downstreamCombiner = downstream.combiner();
    class Container {
        A acc;
        T obj;
        boolean hasAny;

        Container(A acc) {
            this.acc = acc;
        }
    }
    Supplier<Container> supplier = () -> new Container(downstreamSupplier.get());
    BiConsumer<Container, T> accumulator = (acc, t) -> {
        if(!acc.hasAny) {
            downstreamAccumulator.accept(acc.acc, t);
            acc.obj = t;
            acc.hasAny = true;
        } else {
            int cmp = comparator.compare(t, acc.obj);
            if (cmp < 0) {
                acc.acc = downstreamSupplier.get();
                acc.obj = t;
            }
            if (cmp <= 0)
                downstreamAccumulator.accept(acc.acc, t);
        }
    };
    BinaryOperator<Container> combiner = (acc1, acc2) -> {
        if (!acc2.hasAny) {
            return acc1;
        }
        if (!acc1.hasAny) {
            return acc2;
        }
        int cmp = comparator.compare(acc1.obj, acc2.obj);
        if (cmp < 0) {
            return acc1;
        }
        if (cmp > 0) {
            return acc2;
        }
        acc1.acc = downstreamCombiner.apply(acc1.acc, acc2.acc);
        return acc1;
    };
    Function<Container, D> finisher = acc -> downstream.finisher().apply(acc.acc);
    return Collector.of(supplier, accumulator, combiner, finisher);
}

Ответ 6

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

    List<Foo> newlist = list.stream()
    .sorted( Comparator.comparing(Foo::getVariableCount) )
    .reduce( new ArrayList<>(), 
         (l, f) -> { 
             if ( l.isEmpty() || l.get(0).getVariableCount() == f.getVariableCount() ) l.add(f); 
             return l;
         }, 
         (l1, l2) -> {
             l1.addAll(l2); 
             return l1;
         } 
    );

Или использование сбора еще более компактно:

    List<Foo> newlist = list.stream()
    .sorted( Comparator.comparing(Foo::getVariableCount) )
    .collect( ArrayList::new, 
         (l, f) -> if ( l.isEmpty() || l.get(0).getVariableCount() == f.getVariableCount() ) l.add(f),
         List::addAll
    );

Ответ 7

Вы можете использовать разумный collect в отсортированном списке, а в накопителе добавить логику, чтобы добавить только первый элемент в пустой список или добавить любой другой Foo, имеющий счетчик переменных, такой же, как и для первого элемента списка.

Полный рабочий пример ниже:

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

class Foo {
    private int variableCount;

    public Foo(int vars) {
        this.variableCount = vars;
    }

    public Integer getVariableCount() {
        return variableCount;
    }

    public static void main(String[] args) {
        List<Foo> list = Arrays.asList(
                new Foo(2),
                new Foo(2),
                new Foo(3),
                new Foo(3),
                new Foo(1),
                new Foo(1)
        );

        System.out.println(list.stream()
                .sorted(Comparator.comparing(Foo::getVariableCount))
                .collect(() -> new ArrayList<Foo>(),
                        (ArrayList<Foo> arrayList, Foo e) -> {
                            if (arrayList.isEmpty()
                                    || arrayList.get(0).getVariableCount() == e.getVariableCount()) {
                                arrayList.add(e);
                            }
                        },
                        (ArrayList<Foo> foos, ArrayList<Foo> foo) -> foos.addAll(foo)
                )

        );
    }

    @Override
    public String toString() {
        return "Foo{" +
                "variableCount=" + variableCount +
                '}';
    }
}

Кроме того, вы можете сначала найти минимальный variableCount в одном потоке и использовать этот внутренний фильтр другого потока.

    list.sort(Comparator.comparing(Foo::getVariableCount));
    int min = list.get(0).getVariableCount();
    list.stream().filter(foo -> foo.getVariableCount() == min)
            .collect(Collectors.toList());

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

Ура!