Java / Intermediate / Lesson 8
Generics, lambdas, and stream pipelines
Express type-safe reusable operations and data transformations without raw types, hidden side effects, or assumptions about pipeline execution.
On this page
Lesson goals
- Explain generic invariance and bounded wildcards.
- Read lambdas as implementations of functional contracts.
- Build deterministic, side-effect-free stream reductions.
Preserve relationships between types
Generics let an API preserve type relationships without converting everything to Object. List<String> communicates that retrieved elements are strings and rejects adding an Integer at compile time. Raw List bypasses useful checks and can defer failure to a ClassCastException far from the original insertion. Treat unchecked warnings as evidence to investigate, not routine noise to suppress globally.
Java generics are invariant: List<Integer> is not a subtype of List<Number>, even though Integer is a subtype of Number. If it were, a caller could insert a Double into a list that promises integers. Type erasure also limits runtime type tests; instanceof List<String> is not an ordinary reifiable type test. Keep compile-time guarantees distinct from runtime representation.
Use a bounded wildcard to describe data flow
A parameter List<? extends Number> can read elements as Number from lists of suitable numeric subtypes. It cannot safely accept an arbitrary Number addition, because the actual list might be List<Integer>. For a destination that receives values of type T, ? super T often expresses the opposite relationship. Choose the bound from whether the API produces or consumes the type.
The example sums numeric values as doubles without modifying its input. The result 6.0 follows from 1, 2, and 3. For exact decimal domains this conversion may lose precision, so generic flexibility does not make one arithmetic representation universally correct. Document numeric semantics separately from collection variance and include empty-input behavior in the method contract.
public class Main {
static double sum(java.util.List<? extends Number> values) {
double total=0; for(Number n:values) total+=n.doubleValue(); return total;
}
public static void main(String[] args) throws Exception {
System.out.println(sum(java.util.List.of(1,2,3)));
}
}
6.0
Each Integer is safely read as Number and converted to double for this explicitly approximate sum.
Treat lambdas as typed behavior
A lambda implements a functional interface's single abstract method. Predicate<T> tests a condition, Function<T,R> transforms a value, and Consumer<T> performs an action. The target type determines the parameter and result contract; a lambda is not dynamically typed code. Method references are another syntax for compatible behavior, such as String::length or Integer::sum.
Captured local variables must be final or effectively final, but a captured reference can still point to mutable state. Capturing an ArrayList and changing it from concurrent tasks is not made safe by lambda syntax. Prefer stateless transformations whose result depends only on input when building pipelines, and pass mutable collaborators explicitly when effects are a genuine requirement.
Separate intermediate operations and terminal work
A stream describes a pipeline over data; it is not itself a stored collection. Intermediate operations such as filter and map are lazy, and a terminal operation such as sum drives the calculation. The example keeps even integers, squares them, and sums 4 plus 16 to obtain 20. The original list remains unchanged because these operations do not mutate it.
A stream is normally single-use. Create a fresh stream for a second terminal calculation rather than reusing a consumed pipeline. Use primitive streams when they clarify numeric reductions. Do not depend on peek or a mapping side effect for essential business actions, because optimizations and short-circuiting can change which intermediate operations are evaluated.
public class Main {
public static void main(String[] args) throws Exception {
var values=java.util.List.of(1,2,3,4);
int answer=values.stream().filter(n->n%2==0)
.mapToInt(n->n*n).sum();
System.out.println(answer);
System.out.println(values.size());
}
}
20
4
Only 2 and 4 survive. Squaring gives 4 and 16, and the list still contains all four original elements.
Require evidence before parallelizing a pipeline
parallelStream does not guarantee a faster calculation. Work size, splitting cost, blocking, shared-pool competition, and reduction design all matter. A reduction must use an associative operation and a correct identity; subtracting values or modifying a shared non-thread-safe accumulator can produce invalid results. Encounter order and unordered execution also have distinct contracts.
Test the transformation with empty input, rejected values, duplicates, and numeric boundaries before considering speed. Compare a straightforward loop when it is easier to read or debug. The value of streams is an explicit transformation contract, not avoiding all loops. A maintainable API uses generics to preserve relationships and uses functional operations to expose data flow without concealing essential state changes.
Common errors and fixes
incompatible types: List<Integer> cannot be converted to List<Number>
- Why it happens
- Generic type arguments are invariant.
- How to fix it
- Use a suitable bounded wildcard for a read-only producer.
- Prevent it next time
- Describe producer and consumer roles in the signature.
ClassCastException after raw collection use
- Why it happens
- Unchecked insertion broke the promised element type.
- How to fix it
- Restore parameterized types and remove the unsafe insertion.
- Prevent it next time
- Treat unchecked warnings as review items.
stream has already been operated upon or closed
- Why it happens
- A terminal operation consumed the stream.
- How to fix it
- Create a new stream from the source for another calculation.
- Prevent it next time
- Keep each pipeline local to one terminal result.
Check your understanding
1. Why may a sum method read List<? extends Number> but not add a Double to it?
Show worked solution
The actual input could be List<Integer>. Reading each value as Number is safe, while adding a Double would violate that list's element promise. The wildcard describes uncertainty about the precise subtype, not a general writable numeric list.
2. For [1,2,3,4,5], filter values > 2 and sum their doubles. What is the result?
Show worked solution
The retained values are 3,4,5 and their doubled values are 6,8,10, totaling 24. Use filter followed by mapToInt and sum, and verify empty input produces the sum identity zero.
Key takeaways
- Generics preserve relationships without raw casts.
- Lambdas implement typed behavior contracts.
- Streams are lazy, single-use pipelines rather than stored results.