Search code examples
javajava-8java-stream

Select all elements with the lowest key after group by using the Stream API with Java 8


I have a Stream of Foo objects.

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

I want a list of all Foos that all have the lowest variableCount.

Example

Given:

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

I only want the stream to return the last 2 Foos, since they have the lowest value.


I've tried doing a collect with grouping by:

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

That returns a Map<Integer, List<Foo>>, but I'm not sure how to transform that into what I want.


Solution

  • Here is a solution that:

    1. Only streams the list once.
    2. Doesn't build a map or other structure that contains all of the input items (unless the variable counts are all the same), only keeping those that are currently the minimum.
    3. Is O(n) time, O(n) space. It's entirely possible that all Foos have the same variable count, in which case this solution would store all items like other solutions. But in practice, with different, varied values and higher cardinality, the number of items in the list is likely to be much lower.

    Edited

    I've improved my solution according to the suggestions in the comments.

    I implemented an accumulator object, which supplies functions to the Collector for this.

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

    Then all we have to do is collect, referencing the accumulator's methods for its functions.

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

    Testing with

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

    The output is (with a suitable toString defined on Foo):

    [Foo{1}, Foo{1}]