Java 8 Stream API - 选择分组后的最低密钥

时间:2018-02-27 17:10:09

标签: java java-8 java-stream

我有一个Foo对象流。

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

我想要一个Foo的列表,它们都具有最低的variableCount。

例如

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

我只希望流返回最后2 Foo s

我已尝试通过

进行分组收集
.collect(Collectors.groupingBy((Foo foo) -> {
                    return foo.getVariableCount();
})

这会返回Map<Integer, List<Foo>>,我不知道如何将其转换为我想要的内容。

提前致谢

7 个答案:

答案 0 :(得分:14)

您可以使用有序地图进行分组,然后只需获取第一个条目。 一些事情:

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

答案 1 :(得分:10)

以下是一个解决方案:

  1. 仅对列表进行一次流式处理。
  2. 不构建包含所有输入项的地图或其他结构(除非变量计数完全相同),只保留当前最小的那些。
  3. 是O(n)时间,O(n)空间。完全有可能所有Foo具有相同的变量计数,在这种情况下,此解决方案将存储所有项目,如其他解决方案。但在实践中,凭借不同的,不同的价值和更高的基数,列表中的项目数量可能会低得多。
  4. <强>被修改

    我根据评论中的建议改进了我的解决方案。

    我实现了一个累加器对象,它为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 :(得分:6)

如果你可以流式传输(迭代)两次:

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

答案 3 :(得分:1)

为避免创建地图,您可以使用两个流:

  • 第一个找到最小值。
  • 第二个过滤具有此值的元素。

它可以给:

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

答案 4 :(得分:1)

为了避免创建整个地图并避免两次流式传输,我从这里复制了一个自定义收集器https://stackoverflow.com/a/30497254/1264846并将其修改为使用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);
}

答案 5 :(得分:1)

以下是一个流和自定义减速器的替代方案。我们的想法是首先排序,然后只收集具有第一个最小值的元素:

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

或者使用collect更紧凑:

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

答案 6 :(得分:1)

你可以在排序列表上明智地使用collect,在累加器中添加逻辑,只将第一个元素添加到空列表中,或者添加任何其他具有与列表第一个元素相同的变量计数的Foo。 / p>

以下完整的工作示例: -

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

我认为在任何情况下都需要排序或找到后来可以在谓词中使用的最小数字的方法。即使您使用地图对值进行分组。

干杯!