Java 8 Stream's .Min() and .Max(): Why Does This Compile

Java 8 stream's .min() and .max(): why does this compile?

Let me explain what is happening here, because it isn't obvious!

First, Stream.max() accepts an instance of Comparator so that items in the stream can be compared against each other to find the minimum or maximum, in some optimal order that you don't need to worry too much about.

So the question is, of course, why is Integer::max accepted? After all it's not a comparator!

The answer is in the way that the new lambda functionality works in Java 8. It relies on a concept which is informally known as "single abstract method" interfaces, or "SAM" interfaces. The idea is that any interface with one abstract method can be automatically implemented by any lambda - or method reference - whose method signature is a match for the one method on the interface. So examining the Comparator interface (simple version):

public Comparator<T> {
T compare(T o1, T o2);
}

If a method is looking for a Comparator<Integer>, then it's essentially looking for this signature:

int xxx(Integer o1, Integer o2);

I use "xxx" because the method name is not used for matching purposes.

Therefore, both Integer.min(int a, int b) and Integer.max(int a, int b) are close enough that autoboxing will allow this to appear as a Comparator<Integer> in a method context.

Why does Java8 Stream.min() and max() takes comparator as input?

The only way min() could work without a Comparator was if the element type T of the Stream<T> was bound to implement Comparable<T>.

However, the Stream interface should support both element types that implement Comparable and element types that don't. Therefore, you must pass a Comparator<T> to min().

Calculating Min and Max sums from a List of Integer using Java 8 streams

The overflow occurs because you are performing reduction on a stream of Integer, i.e. accumulator of the reduce() operation deals with Interger objects, and then you're assigning the overflown result of the calculation to a variable of type long.

LongSummaryStatistics & LongStream

Since only one value should be discarded in both cases (for min and max sum), we can approach this problem by calculating the total of all elements and subtracting the value of the lowest element to obtain the maximum sum and subtracting the value of the highest element to get the minimum sum.

There's no need to apply sorting and hard-code the value of 4 in the stream. If you don't want to develop bad habits, try to design your solutions to be clean and generalized.

We can get all three values (total, the lowest element, the highest element) by performing a single iteration over the given list. It can be done "manually" by tracking these values while iterating with a for loop, or we can make use of one of the summary statistics classes from the JDK to get this job done for us.

From the summary statistics object we can extract information about the total sum, minimum and maximum values, using its methods getSum(), getMin() and getMax(). To avoid int overflow we have to use LongSummaryStatistics. And we can obtain this object from a LongStream by applying summaryStatistics() as a terminal operation.

public static void miniMaxSum(List<Integer> arr) {
LongSummaryStatistics statistics = arr.stream()
.mapToLong(i -> i)
.summaryStatistics();

long max = statistics.getSum() - statistics.getMin();
long min = statistics.getSum() - statistics.getMax();

System.out.println(min + " " + max);
}

main()

public static void main(String[] args) {
miniMaxSum(List.of(256741038, 623958417, 467905213, 714532089, 938071625));
}

Output:

2063136757 2744467344

Concise way to get both min and max value of Java 8 stream

If this is a frequently needed feature, we better make a Collector to do the job. We'll need a Stats class to hold count, min, max, and factory methods to creat stats collector.

Stats<String> stats = stringStream.collect(Stats.collector())

fooStream.collect(Stats.collector(fooComparator))

(Maybe a better convenience method would be Stats.collect(stream))

I made an example Stats class -

https://gist.github.com/zhong-j-yu/ac5028573c986f7820b25ea2e74ed672

public class Stats<T>
{
int count;

final Comparator<? super T> comparator;
T min;
T max;

public Stats(Comparator<? super T> comparator)
{
this.comparator = comparator;
}

public int count(){ return count; }

public T min(){ return min; }
public T max(){ return max; }

public void accept(T val)
{
if(count==0)
min = max = val;
else if(comparator.compare(val, min)<0)
min = val;
else if(comparator.compare(val, max)>0)
max = val;

count++;
}

public Stats<T> combine(Stats<T> that)
{
if(this.count==0) return that;
if(that.count==0) return this;

this.count += that.count;
if(comparator.compare(that.min, this.min)<0)
this.min = that.min;
if(comparator.compare(that.max, this.max)>0)
this.max = that.max;

return this;
}

public static <T> Collector<T, Stats<T>, Stats<T>> collector(Comparator<? super T> comparator)
{
return Collector.of(
()->new Stats<>(comparator),
Stats::accept,
Stats::combine,
Collector.Characteristics.UNORDERED, Collector.Characteristics.IDENTITY_FINISH
);
}

public static <T extends Comparable<? super T>> Collector<T, Stats<T>, Stats<T>> collector()
{
return collector(Comparator.naturalOrder());
}
}

Java 8 stream's max(Math::max)

This is somehow very subtle, let's take two at a time:

 -2, -5 => Max between these two is "-2"

It is a negative result, since max method from a stream accepts a Comparator, which says:

returns a negative integer if the first argument is less than the second.

Thus according to your Comparator , you have just said that -5 > -2 or in simpler words:

Stream.of(-2, -5)
.max(Math::max)
.ifPresent(System.out::println); // will show -5

You can build your logic for the other numbers from here and understand why -16 is the result that you get.

To make this correct, you need:

 ...max(Comparator.naturalOrder())
....

Find the min and max number from a Collection using Java streams

You have to flatten the elements of all the Lists into a single Stream<Integer> in order for your code to work :

System.out.println(largeList.stream().flatMap(List::stream).max(Integer::compare).get());
System.out.println(largeList.stream().flatMap(List::stream).min(Integer::compare).get());

This is not very efficient, though, since you process the Lists twice in order to find both min and max, and you can get the same data (and more) in a single processing by using IntStream::summaryStatistics() :

IntSummaryStatistics stats = largeList.stream().
.flatMap(List::stream)
.mapToInt(Integer::intValue)
.summaryStatistics();
System.out.println(stats.getMin());
System.out.println(stats.getMax());

Java 8 streams, why does this compile part 2... Or what is a method reference, really?

The syntax of method references is defined in JLS #15.13. In particular it can be of the form:

Primary :: [TypeArguments] Identifier

Where Primary can be, among other things, a:

ClassInstanceCreationExpression

so yes, your syntax is correct. A few other interesting examples:

this::someInstanceMethod    // (...) -> this.someInstanceMethod(...)
"123"::equals // (s) -> "123".equals(s)
(b ? "123" : "456")::equals // where b is a boolean
array[1]::length // (String[] array) -> array[1].length()
String[]::new // i -> new String[i]
a.b()::c // (...) -> a.b().c(...)

By the way, since you mention static methods, it is interesting to note that you can't create a static method reference from an instance:

class Static { static void m() {} }
Static s = new Static();

s.m(); //compiles
someStream.forEach(s::m); //does not compile
someStream.forEach(Static::m); //that's ok


Related Topics



Leave a reply



Submit