Master

Advanced Streams and Generics

Master generics bounds and complex stream pipelines.

What you will see

4

About 2 min of reading

Try It

Push streams and generics further. Write a bounded-generic method that sums any number list, and count primes across a parallel stream that splits work across cores.

1

Write a generic method bounded to Number so it can sum any numeric list. Call it with an Integer list; inside, every element is a Number and you can call doubleValue on it.

import java.util.Arrays; import java.util.List; class Main { public static void main(String[] args) { List values = Arrays.asList(1, 2, 3); System.out.println(sum(values)); } public static double sum(List values) { double total = 0; for (Number n : values) { total += n.doubleValue(); } return total; } }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
6.0
T extends Number constrains the type at compile time. Inside, the parameter is Number, so doubleValue works on any Integer, Double, and so on.
2

Count prime numbers with a parallel stream. parallelStream splits the work across the common fork/join pool, then count folds the matches into a single total.

import java.util.Arrays; import java.util.List; class Main { public static void main(String[] args) { List numbers = Arrays.asList(2, 3, 4, 5, 6, 7, 8, 9, 10); long count = numbers.parallelStream().filter(Main::isPrime).count(); System.out.println(count); } static boolean isPrime(int n) { if (n < 2) return false; for (int i = 2; i <= n / 2; i++) { if (n % i == 0) return false; } return true; } }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
4
parallelStream is great for large, independent work, but small streams pay more overhead than they save. Measure before trusting the speedup.

Data Flow

Bounded generics constrain types at compile time; streams pipeline operations that reduce lazily to a result.
1 Type parameter constrained by bounds
↓
2 Stream pulls from source
↓
3 Intermediate ops stage lazily
↓
4 Terminal op reduces to a result
Why this works
PECS principle

"Producer Extends, Consumer Super": use ? extends T when a collection only produces T values, and ? super T when it only consumes them:

void addDoubles(List<? extends Number> src) {
    // src produces Numbers (producer)
}

void takeNumbers(List<? super Integer> dst) {
    dst.add(5);   // dst consumes Integers (consumer)
}

PECS keeps generics flexible while staying type-safe.

Stream pitfalls

Streams mutate nothing by themselves; terminal operations drive them. Avoid side effects in stream lambdas, they can run out of order in parallel streams. Prefer collect for aggregation and reserve forEach for terminal display only. Immutable collectors like toList return new collections rather than mutating inputs.

Examples

Bounded generics

Constraining type parameters

public <T extends Number> double sum(List<T> values) {
    double total = 0;
    for (Number n : values) {
        total += n.doubleValue();
    }
    return total;
}

T extends Number constrains the type parameter to Number subclasses. Inside, you can call Number methods safely.

Custom collectors

Grouping data

Map<String, List<Integer>> byLength = words.stream()
    .collect(Collectors.groupingBy(String::length,
        Collectors.mapping(String::length, Collectors.toList())));

groupingBy partitions elements by a classifier into a map of lists. Combine with downstream collectors for rich aggregation.

Parallel streams

Automatic parallelism

long count = numbers.parallelStream()
    .filter(n -> isPrime(n))
    .count();

parallelStream() splits work across the common fork/join pool. Great for large, independent operations, but measure first, small streams have overhead.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Concurrency and Multithreading
Previous Lesson
Concurrency and Multithreading