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

问题描述 投票:16回答:7

我有一个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个Foos

我试过用分组进行收集

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

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

提前致谢

java java-8 java-stream
7个回答
10
投票

这是一个解决方案:

  1. 只列出一次列表。
  2. 不构建包含所有输入项的映射或其他结构(除非变量计数完全相同),只保留当前最小的那些。
  3. 是O(n)时间,O(n)空间。所有Foos完全有可能具有相同的变量计数,在这种情况下,此解决方案将存储所有项目,如其他解决方案。但在实践中,由于具有不同的,不同的值和更高的基数,列表中的项目数量可能会低得多。

编辑

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

我实现了一个累加器对象,它为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}]

14
投票

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

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

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

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

1
投票

您可以在排序列表上明智地使用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());

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

干杯!


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

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());
© www.soinside.com 2019 - 2024. All rights reserved.