18

Given a list of items with properties, I am trying to get the last item to appear with a maximum value of said property.

For example, for the following list of objects:

t i A: 3 D: 7 * F: 4 C: 5 X: 7 * M: 6 

I can get one of the Things with the highest i:

Thing t = items.stream() .max(Comparator.comparingLong(Thing::getI)) .orElse(null); 

However, this will get me Thing t = D. Is there a clean and elegant way of getting the last item, i.e. X in this case?

One possible solution is using the reduce function. However, the property is calculated on the fly and it would look more like:

Thing t = items.stream() .reduce((left, right) -> { long leftValue = valueFunction.apply(left); long rightValue = valueFunction.apply(right); return leftValue > rightValue ? left : right; }) .orElse(null); 

The valueFunction now needs to be called nearly twice as often.

Other obvious roundabout solutions are:

  1. Store the object in a Tuple with its index
  2. Store the object in a Tuple with its computed value
  3. Reverse the list beforehand
  4. Don't use Streams
4
  • 2
    "The valueFunction now needs to be called nearly twice as often." Note that even when using max, the getI method will be called again and again for every comparison, not just once per element. In your example, it's called 11 times, including 6 times for D. How about you just cache the calculated value directly in the Thing instance? Commented Feb 5, 2019 at 16:14
  • @tobias_k I realised that after posting my question. My practical solution was to use a standard for each. Commented Feb 5, 2019 at 17:35
  • 3
    If your stream goes parallel or async, there can be a complete loss of the idea of "in order". Be sure to think about this and consider if your stream can contain a field or value indicating the order, similar to a tuple of (value, orderIndex) so that the max operation is a simple, parallelizable comparison of the value and orderIndex fields. Commented Feb 5, 2019 at 19:19
  • @ErikE if the stream has a defined encounter order, the reduce based solution will work in parallel, as long as the reduction function fulfills the associativity constraint. Which is the case here. Commented Feb 7, 2019 at 12:04

9 Answers 9

10

Remove the equals option (don't return 0 if the compared numbers are equal, return -1 instead) from the comparator (ie. write your own comparator that doesn't include an equals option):

Thing t = items.stream() .max((a, b) -> a.getI() > b.getI() ? 1 : -1) .orElse(null); 
Sign up to request clarification or add additional context in comments.

8 Comments

I'm not sure that java guarantees the comparator is called "in order", i.e. the later items always is the second argument (thinking about parallel execution). That being said, it makes sense to place a little bit of trust in this implementation detail.
I've accepted this because it's the most elegant solution and, as far as I can tell, as efficient as a Comparator (which is what I wanted). If I don't want to compute the value every time I have to settle for a for each or a tuple. Thank you.
I do believe this is not guaranteed to work as you expect the moment that the stream goes parallel/asynch.
What does this have to do with equals? The only trick seems to be to invert the tie-breaking from "a unless b is greater" to "b unless a is greater", which is exactly the same as OP's reduce did, but IMHO less readable, as you return +/-1 instead of the actual object to retain. Also, this, too, will call getI more often than necessary (just as often as reduce).
This may happen to work, but still violates the Comparator contract, which mandates symmetry. Further, it doesn't solve the problem of invoking getI() multiple times per element, hence, is no improvement over a clean reduce((a, b) -> a.getI() > b.getI()? a: b).
|
5

Conceptually, you seem to be possibly looking for something like thenComparing using the index of the elements in the list:

Thing t = items.stream() .max(Comparator.comparingLong(Thing::getI).thenComparing(items::indexOf)) .orElse(null); 

7 Comments

Assuming: that for two objects if attribute I is equal that doesn't mean the objects are equal as well. In which case the indexOf would return same value always.
This is nice, but the problem I am trying to solve was actually originally caused by the equals method returning true for different objects ;-)
You're right. Unfortunately, I didn't write the equals.
Doesn't this give the whole thing O(n²) complexity? Also, would not work for a pure stream, with not backing list (not a requirement, but anyway). Something like Python's enumerate would come in handy here.
@Marco13 in the worst case, all elements are "new maximum" values in respect to the previous elements, i.e. when the input elements happen to be in ascending order. So O(n²) is the correct time complexity of this solution.
|
3

To avoid the multiple applications of valueFunction in your reduce solution, simply explicitly calculate the result and put it in a tuple:

Item lastMax = items.stream() .map(item -> new AbstractMap.SimpleEntry<Item, Long>(item, valueFunction.apply(item))) .reduce((l, r) -> l.getValue() > r.getValue() ? l : r ) .map(Map.Entry::getKey) .orElse(null); 

Comments

1

Stream is not necessary bad if you do things in two steps :

1) Find the i value that has more occurrences in the Iterable (as you did)
2) Search the last element for this i value by starting from the end of items:

Thing t = items.stream() .max(Comparator.comparingLong(Thing::getI)) .mapping(firstMaxThing -> return IntStream.rangeClosed(1, items.size()) .mapToObj(i -> items.get(items.size()-i)) .filter(item -> item.getI() == firstMaxThing.getI()) .findFirst().get(); // here get() cannot fail as *max()* returned something. ) .orElse(null) 

Comments

1

The valueFunction now needs to be called nearly twice as often.

Note that even when using max, the getI method will be called again and again for every comparison, not just once per element. In your example, it's called 11 times, including 6 times for D, and for longer lists, too, it seems to be called on average twice per element.

How about you just cache the calculated value directly in the Thing instance? If this is not possible, you could use an external Map and use calculateIfAbsent to calculate the value only once for each Thing and then use your approach using reduce.

Map<Thing, Long> cache = new HashMap<>(); Thing x = items.stream() .reduce((left, right) -> { long leftValue = cache.computeIfAbsent(left, Thing::getI); long rightValue = cache.computeIfAbsent(right, Thing::getI); return leftValue > rightValue ? left : right; }) .orElse(null); 

Or a bit cleaner, calculating all the values beforehand:

Map<Thing, Long> cache = items.stream() .collect(Collectors.toMap(x -> x, Thing::getI)); Thing x = items.stream() .reduce((left, right) -> cache.get(left) > cache.get(right) ? left : right) .orElse(null); 

1 Comment

BTW, you can easily check how often the comparator callback is called by adding some print statement or incrementing a counter in getI or in the callback itself.
1

Here's an implementation of your "roundabout solution" of "Store the object in a Tuple with its computed value" to avoid calculating i more than once per Thing. It's actually fairly straightforward, especially now that Java has record classes:

private static Thing getLastMaxThing(List<Thing> items) { record ThingAndI(Thing thing, long i) { } // Not a musical about a Siamese king return items.stream() .map(t -> new ThingAndI(t, t.getI())) .reduce((t1, t2) -> t1.i() > t2.i() ? t1 : t2) .map(ThingAndI::thing) .orElseThrow(); } 

2 Comments

Much easier these days with Java records. And the comment made me laugh ;-)
A Simpsons reference? Bart's evil twin? en.wikipedia.org/wiki/Treehouse_of_Horror_VII
0

You can still use the reduction to get this thing done. If t1 is larger, then only it will keep t1. In all the other cases it will keep t2. If either t2 is larger or t1 and t2 are the same, then it will eventually return t2 adhering to your requirement.

Thing t = items.stream(). reduce((t1, t2) -> t1.getI() > t2.getI() ? t1 : t2) .orElse(null); 

8 Comments

This is the same solution as I presented in my question.
@Druckles Technically, the answer you accepted is also the same as your solution, just less obviously so.
@tobias_k "Less obvious" is everything in programming.
@Druckles Am I getting you correctly? Your programming goal is to write as obfuscated code as possible?
@Druckles I don't see any way, how to interpret your sentence "'Less obvious' is everything in programming" in a different way than that you like non-obvious things, like the accepted answer, which only makes it less obvious that it still does the same as the question's code, including having the same problems, plus adding another one, using a formally incorrect comparator, which makes it less obvious why it still happens to do the desired thing. "less obvious" is obfuscation. Actually, "is everything" does not only imply that you like it but that it is the most important thing to you.
|
0

Your current implementation using reduce looks good, unless your value-extractor function is costly.

Considering later you may want to reuse the logic for different object types & fields, I would extract the logic itself in separate generic method/methods:

public static <T, K, V> Function<T, Map.Entry<K, V>> toEntry(Function<T, K> keyFunc, Function<T, V> valueFunc){ return t -> new AbstractMap.SimpleEntry<>(keyFunc.apply(t), valueFunc.apply(t)); } public static <ITEM, FIELD extends Comparable<FIELD>> Optional<ITEM> maxBy(Function<ITEM, FIELD> extractor, Collection<ITEM> items) { return items.stream() .map(toEntry(identity(), extractor)) .max(comparing(Map.Entry::getValue)) .map(Map.Entry::getKey); } 

The code snippets above can be used like this:

Thing maxThing = maxBy(Thing::getField, things).orElse(null); AnotherThing maxAnotherThing = maxBy(AnotherThing::getAnotherField, anotherThings).orElse(null); 

Comments

0

One solution would be to do an initial pass of the list to get a maximum value, then do a pass over the list from the end to get the final maximum value. The reversed method added in Java 21 can be used to efficiently stream over the list in reverse.

Thing min = items.stream() .max(Comparator.comparingLong(Thing::getI)) .orElseThrow(); Thing lastMin = items.reversed().stream() .filter(t -> t.getI() == min.getI()) .findFirst() .orElseThrow(); 

At worst, this requires two passes over the list. At best, if a maximum value is near the end of the list, this requires only a little more than a single pass over the list.

If you want to reduce the number of calls to getI (as your comments indicate you want to do):

long maxI = items.stream().mapToLong(Thing::getI).max().orElseThrow(); Thing lastMin = items.reversed().stream() .filter(t -> t.getI() == maxI) .findFirst() .orElseThrow(); 

This implementation calls getI at most twice per object in the list.


One might be tempted to use your initial solution, but reversing the list first:

Thing t = items.reversed().stream() .max(Comparator.comparingLong(Thing::getI)) .orElse(null); 

While this works in practice for current stream implementations, the max method on Stream makes no guarantees as to which element is returned if there's a max, so I personally would advise against relying on it as it could break in different versions of Java.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.