简体   繁体   中英

Returning List of values with max property using Comparator

What is the best way to get List<MyCustomObject> from List<MyCustomObject> that have maximum value of some property?

I can write my own Comparator :

Comparator<MyCustomObject> cmp = Comparator.comparing(MyCustomObject::getIntField);

Then use it in a stream :

Optional<MyCustomObject> max = list.stream().max(cmp);

But I'm getting only one element. Is there a simple way to return all MyCustomObject that have maximum IntField and not just the first one?

You only know the maximum value of the relevant property after you iterate over all the elements of the List , so one way of find the elements having the max value is to group the elements by that property into a sorted Map and get the last value:

List<MyCustomObject> max = list.stream()
                               .collect(Collectors.groupingBy (MyCustomObject::getIntField,
                                                               TreeMap::new,
                                                               Collectors.toList ()))
                               .lastEntry ()
                               .getValue ();

However, this performs more work then you actually need, and costs O(NlogN) due to the sorting. If you don't mind splitting the problem into two steps (first finding the maximum value and then collecting the elements having a property with that value), you'll have better running time ( O(N) ).

Since I don't have your custom object, I couldn't test the above code, but I tested similar code that takes a Stream of String s and returns all the String s having the max length:

List<String> max = Stream.of ("ddd","aa","EEEE","a","BB","CCC1")
                         .collect(Collectors.groupingBy (String::length,
                                                         TreeMap::new,
                                                         Collectors.toList ()))
                         .lastEntry ()
                         .getValue ();
System.out.println (max);

This returns:

[EEEE, CCC1]

To avoid two runs, you can provide your own Collector to collect the stream.

Let's use

Sample data class

static class MyCustomObject {
    private int intField;

    MyCustomObject(int field) {
        intField = field;
    }

    public int getIntField() {
        return intField;
    }

    @Override
    public String toString() {
        return Integer.toString(intField);
    }
}

What you do to create your own Collector is use one of the factory methods, Collector#of . We'll use the more complex one .

This is what it will look like:

Collector<MyCustomObject, Intermediate, List<MyCustomObject>> collector

with MyCustomObject the objects you're collecting, Intermediate a class that will store the current maximum and the list of MyCustomObject s that had that maximum, and List<MyCustomObject>> the desired end result of objects with that maximum.

Intermediate

Here's the intermediate class:

// simple enough
class Intermediate {
    Integer val = null;
    List<MyCustomObject> objects = new ArrayList<>();
}

which will keep the maximum and corresponding objects. It will be supplied with

Supplier<Intermediate> supplier = () -> new Intermediate();

(or short Intermediate::new).

Accumulator

The accumulator needs to accumulate a new MyCustomObject into an existing Intermediate . This is where the logic calculating the max value comes in.

BiConsumer<Intermediate, MyCustomObject> accumulator = (i, c) -> {
    System.out.printf("accumulating %d into %d%n", c.intField, i.value);
    if (i.value != null) {
        if (c.intField > i.value.intValue()) {
            // new max found
            System.out.println("new max " + c.intField);
            i.value = c.intField;
            i.objects.clear();
        } else if (c.intField < i.value) {
            // smaller than previous max: ignore
            return;
        }
    } else {
        i.value = c.intField;
    }
    i.objects.add(c);
};

Combiner

The combiner is used to combine two Intermediate values. This is used for parallel streams. If you do the simple test run below you won't trigger it.

BinaryOperator<Intermediate> combiner = (i1, i2) -> {
    System.out.printf("combining %d and %d%n", i1.value, i2.value);
    Intermediate result = new Intermediate();
    result.value = Math.max(i1.value, i2.value);
    if (i1.value.intValue() == result.value.intValue()) {
        result.objects.addAll(i1.objects);
    }
    if (i2.value.intValue() == result.value.intValue()) {
        result.objects.addAll(i2.objects);
    }
    return result;
};

Finisher

Finally, we need to extract the List<MyCustomObject> we really want from the final Intermediate using the finisher

Function<Intermediate, List<MyCustomObject>> finisher = i -> i.objects;

This all comes together for the Collector

Collector<MyCustomObject, Intermediate, List<MyCustomObject>> collector =
    Collector.of(supplier, accumulator, combiner, finisher);

And for a simple test run

List<MyCustomObject> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
    for (int j = 0; j < 3; j++) {
        list.add(new MyCustomObject(i));
    }
}
Collections.shuffle(list);

System.out.println(list.stream().collect(collector));

Output

[9, 9, 9]

We do only iterate once, so it should be O(n) as the solution with two runs; I'm not entirely sure about this though because of all the adding to lists that happens in the intermediate steps.

See it tied together

For an actual Comparator version, you'd have to also adjust the Intermediate object; then it's probably best to use a MyCustomObject in the Intermediate to use for the comparison as well.

Here is a version for this , including a refactoring of the accumulator into the Intermediate class.

In the end, it boils down to this factory method:

public static <T> Collector<T, ?, List<T>> max(Comparator<T> compare) {
    class Intermediate {
        T value = null;
        List<T> objects = new ArrayList<>();

        void add(T c) {
            if (objects.isEmpty()) {
                value = c;
            } else {
                int compareResult = compare.compare(c, objects.get(0));
                if (compareResult > 0) {
                    // new max found
                    System.out.println("new max " + c + ", dropping " + objects.size() + " objects");
                    value = c;
                    objects.clear();
                } else if (compareResult < 0) {
                    return;
                }
            }
            objects.add(c);
        }
    }
    BinaryOperator<Intermediate> combiner = (i1, i2) -> {
        Optional<T> max = Stream.of(i1, i2).filter(Objects::nonNull).filter(i -> !i.objects.isEmpty())
                .map(i -> i.objects.get(0)).max(compare);
        Intermediate r = max.map(m -> {
            Intermediate result = new Intermediate();
            result.value = max.get();
            if (i1 != null && i1.value != null && compare.compare(i1.value, m) == 0) {
                result.objects.addAll(i1.objects);
            }
            if (i2 != null && i2.value != null && compare.compare(i2.value, m) == 0) {
                result.objects.addAll(i2.objects);
            }
            return result;
        }).orElse(null);
        System.out.printf("combining %s and %s - result %s%n", i1, i2, r);
        return r;
    };
    return Collector.of(Intermediate::new, Intermediate::add, combiner, i -> i.objects);
}

I offer this simple solution. First retrieve max element from the given list objs . And then retrieve all elements, that equal to max.

public static <T> List<T> getAllMax(List<T> objs, Comparator<T> comp) {
    T max = objs.stream().max(comp).get();
    return objs.stream().filter(e -> comp.compare(e, max) == 0).collect(Collectors.toList());
}

We loop given list only twice without extra memory allocation. Therefore we have O(n) complexity.

Code snippet for this problem:

List<MyCustomObject> maxList = new ArrayList<>();
MyCustomObject max = list.stream().max(cmp).orElse(null);
if (null != max) {
   maxList.add(max);
   list.remove(max);
   while (list.stream().max(cmp).orElse(null) != null) {
     maxList.add(max);
     list.remove(max);
  }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM