The max and min in an array in Java come from a single scan that keeps the largest and the smallest value seen so far; Arrays.stream(array).max() and min() run the same scan, and summaryStatistics() returns both values from one pass. There is no single Math.max() call that accepts a whole array.
A weather app shows the daily high and low, a shop shows the price range of a category, and a monitoring dashboard flags the slowest response of the hour. Each of these reads the extremes of a set of values, often together.
The following example reads the warmest and the coldest of five temperature readings.
int[] temps = {18, 24, 9, 31, 15};
int max = Arrays.stream(temps).max().orElseThrow(); // 31
int min = Arrays.stream(temps).min().orElseThrow(); // 9
IntSummaryStatistics stats = Arrays.stream(temps).summaryStatistics();
int high = stats.getMax(); // 31
int low = stats.getMin(); // 9
Notice the orElseThrow() calls, which exist because an empty array has no maximum. Below, we write the scan by hand, compare the JDK methods, handle objects, indexes and double values, and explain why sorting and recursion are poor choices here.
1. One Pass for the Largest and Smallest Values
The scan starts with the first element as both the current maximum and the current minimum, and compares every remaining element with them. Starting from the first element instead of Integer.MIN_VALUE and Integer.MAX_VALUE keeps the code correct for any element type, and it makes the empty-array case visible instead of returning a made-up value.

We return both values in a small record, so the caller reads the result by name.
record Range(int min, int max) {}
static Range minMax(int[] values) {
if (values == null || values.length == 0) {
throw new IllegalArgumentException("values must not be empty");
}
int min = values[0];
int max = values[0];
for (int i = 1; i < values.length; i++) {
if (values[i] > max) {
max = values[i];
} else if (values[i] < min) {
min = values[i];
}
}
return new Range(min, max);
}
int[] temps = {18, 24, 9, 31, 15};
Range today = minMax(temps); // Range[min=9, max=31]
Range single = minMax(new int[]{7}); // Range[min=7, max=7]
Range none = minMax(new int[0]); // IllegalArgumentException: values must not be empty
The else if is safe, because a value larger than the current maximum cannot be smaller than the current minimum. The loop makes at most 2(n – 1) comparisons and uses O(1) extra memory. A variant that compares the elements in pairs needs only about 3n/2 comparisons, which matters only in very hot loops.
2. IntStream max(), min() and summaryStatistics()
For primitive arrays, max() and min() on an IntStream return an OptionalInt, which is empty when the array is empty. On an empty result, getAsInt() and orElseThrow() throw a NoSuchElementException, so for data that can be empty we pick a fallback with orElse().
int[] noReadings = {};
OptionalInt maybeMax = Arrays.stream(noReadings).max(); // OptionalInt.empty
int fallback = Arrays.stream(noReadings).max().orElse(0); // 0
int boom = Arrays.stream(noReadings).max().getAsInt(); // NoSuchElementException: No value present
Calling max() and min() separately reads the array twice. The IntSummaryStatistics object collects the count, sum, average, minimum and maximum in one pass, and LongSummaryStatistics and DoubleSummaryStatistics do the same for the other two stream types. The sum and average of an array tutorial uses the same object for totals.
On an empty array, summaryStatistics() does not fail; it reports Integer.MAX_VALUE as the minimum and Integer.MIN_VALUE as the maximum, so we check getCount() before reading them.
IntSummaryStatistics empty = Arrays.stream(new int[0]).summaryStatistics();
int emptyMin = empty.getMin(); // 2147483647
int emptyMax = empty.getMax(); // -2147483648
long count = empty.getCount(); // 0
3. Object Arrays with a Comparator
For objects, the question is which field decides the order. The maximum needs an ordering, either the natural order from Comparable or a Comparator; equals() plays no part in it.
record Reading(String city, double celsius) {}
Reading[] readings = {new Reading("Oslo", 4.5), new Reading("Rome", 21.0), new Reading("Pune", 33.5)};
Reading hottest = Arrays.stream(readings).max(Comparator.comparingDouble(Reading::celsius)).orElseThrow(); // Reading[city=Pune, celsius=33.5]
Reading coldest = Collections.min(Arrays.asList(readings), Comparator.comparingDouble(Reading::celsius)); // Reading[city=Oslo, celsius=4.5]
Wrapper arrays such as Integer[] and String[] have a natural order, so Collections.max() works without a comparator. For an int[], we box the values first, because Arrays.asList() on a primitive array returns a list with one element, the array itself.
Integer[] boxed = {18, 24, 9, 31, 15};
Integer top = Collections.max(Arrays.asList(boxed)); // 31
String[] cities = {"Rome", "Oslo", "Pune"};
String first = Collections.min(Arrays.asList(cities)); // "Oslo"
int[] temps = {18, 24, 9, 31, 15};
Integer viaBoxed = Collections.min(Arrays.stream(temps).boxed().toList()); // 9
4. Finding the Index of the Max or Min
A chart that highlights the hottest hour needs the position of the maximum, not only its value. The JDK has no indexOfMax() method, so we stream the indexes and compare the values at those indexes.
int[] hourly = {18, 24, 9, 31, 15};
int maxAt = IntStream.range(0, hourly.length).reduce((i, j) -> hourly[j] > hourly[i] ? j : i).orElse(-1); // 3
int minAt = IntStream.range(0, hourly.length).reduce((i, j) -> hourly[j] < hourly[i] ? j : i).orElse(-1); // 2
The strict comparison keeps the first index when two elements tie. With >= instead of >, the result is the last index of the maximum, and an empty array returns -1 in both versions.
5. double Arrays and NaN
A sensor that fails to read a value often stores Double.NaN. Every comparison with NaN returns false, so our loop skips it unless it sits at index 0, whereas Math.max() and DoubleStream.max() return NaN as soon as one element is NaN.
double[] sensor = {21.5, Double.NaN, 25.0};
double streamMax = Arrays.stream(sensor).max().orElseThrow(); // NaN
double cleanMax = Arrays.stream(sensor).filter(d -> !Double.isNaN(d)).max().orElseThrow(); // 25.0
double mathMax = Math.max(21.5, Double.NaN); // NaN
We filter NaN values out before the search, so the result does not depend on where the bad reading is. The same methods also treat -0.0 as smaller than 0.0, which the plain < operator does not.
6. Why Not Sort or Recurse
Sorting puts the minimum at index 0 and the maximum at the last index, but it costs O(n log n) instead of O(n), and Arrays.sort() reorders the caller’s array. It makes sense only when we need the sorted array anyway.
int[] temps = {18, 24, 9, 31, 15};
int[] sorted = temps.clone();
Arrays.sort(sorted);
int sortedMin = sorted[0]; // 9
int sortedMax = sorted[sorted.length - 1]; // 31
A recursive maximum looks elegant but is not faster. It makes the same n comparisons, adds a method call per element, and a version that recurses once per element throws a StackOverflowError on large arrays. With the default thread stack size, an array of 10,000 to 20,000 elements is often enough for that error.
| Approach | Time | Empty array | Changes the array |
|---|---|---|---|
| Loop from the first element | O(n), one pass for both | Our check throws IllegalArgumentException | No |
| max() / min() on a stream | O(n) each | Empty OptionalInt | No |
| summaryStatistics() | O(n), one pass for both | MAX_VALUE / MIN_VALUE, count 0 | No |
| Collections.max() / min() | O(n) each | NoSuchElementException | No |
| Sorting | O(n log n) | ArrayIndexOutOfBoundsException on index 0 | Yes, unless we sort a copy |
7. Max and Min in an Array FAQs
After the basic program, developers look for variations such as the second largest value or a 2D array.
7.1. Can Math.max() take an array in Java?
No. Math.max() accepts two values and no more. For an array, we use Arrays.stream(array).max(), or Math.max() inside a loop or a reduce(Math::max) call.
7.2. How do we find the second largest number in an array?
We track two values in one loop, the largest and the second largest, or we take the top two values with a heap or a sorted stream. The top N items tutorial covers both, including how duplicates count.
7.3. How do we find the max and min of a 2D array?
We flatten the rows into one stream with flatMapToInt() and call summaryStatistics().
int[][] grid = {{18, 24}, {9, 31, 15}};
IntSummaryStatistics all = Arrays.stream(grid).flatMapToInt(Arrays::stream).summaryStatistics();
int gridMax = all.getMax(); // 31
int gridMin = all.getMin(); // 9
7.4. How do we find the max of an ArrayList?
We call Collections.max(list) for elements with a natural order, or Collections.max(list, comparator) for others. Both throw NoSuchElementException on an empty list, while list.stream().max(comparator) returns an empty Optional.
8. Conclusion
Finding the max and min of an array is a single pass that keeps two running values. For primitive arrays, Arrays.stream(array).max() and min() do the scan for us and return an OptionalInt, and summaryStatistics() gives both values in one pass but reports MAX_VALUE and MIN_VALUE for an empty array.
Object arrays need a Comparator or a natural order, not equals(). For double data, we filter out NaN first, and we leave sorting and recursion for problems that need them.
9. References
- IntStream (Java SE 25 API)
- IntSummaryStatistics (Java SE 25 API)
- Collections.max() (Java SE 25 API)
- Math.max() (Java SE 25 API)
Happy Learning !!