- Learn
- Computer Science & IT
- Programming
- Object-Oriented Programming with Java
- Generics, Lambdas and Streams in Java
Generics, Lambdas and Streams in Java
IntermediateLesson 11 of 11 in Object-Oriented Programming with Java Course
What you will learn
- Use type parameters to write classes, interfaces and methods that work with many types, while the compiler checks every use.
- Define and use generic classesDeclare classes and interfaces with type parameters, create objects with type arguments and explain the type safety this gives compared with storing
Object. - Write generic methodsDeclare methods with their own type parameters, call them with inferred or explicit type arguments and decide when a method rather than a class should be generic.
- Restrict type parameters with bounds and wildcardsUse bounded type parameters to call methods on a generic type, explain why a
List<Integer>is not aList<Number>and choose between? extendsand? super. - Work within the limits of type erasureExplain how the compiler erases type arguments, which operations generics therefore do not allow and why raw types should be avoided.
- Define and use generic classes
- Write lambda expressions and method references for functional interfaces, use Java's standard functional interfaces and pass behavior to methods as a value.
- Write lambda expressions for functional interfacesRecognize functional interfaces, write lambdas in their short and block forms and explain how the target type gives a lambda its meaning.
- Use the standard functional interfacesChoose among Predicate, Function, Consumer, Supplier and their relatives, combine them and pass them to collection methods such as removeIf and replaceAll.
- Capture variables and write method referencesUse variables from the surrounding code inside a lambda, apply the effectively final rule and replace simple lambdas with the four kinds of method reference.
- Write lambda expressions for functional interfaces
- Sort lists by any rule using comparators, from a hand-written compare method to chains built with comparing, thenComparing and reversed.
- Write a comparator and sort with itImplement the compare contract correctly, sort lists and arrays with a comparator and decide between Comparable and Comparator.
- Combine comparators with comparing, thenComparing and reversedBuild comparators from sort keys, add tie-breakers, reverse orders, handle null values and sort the entries of a map.
- Write a comparator and sort with it
- Build Stream API pipelines that filter, transform, sort and reduce data, and explain how a pipeline runs.
- Build a stream pipeline and understand how it runsCreate streams from collections, arrays and ranges, tell intermediate from terminal operations and explain laziness, single use and why the source stays unchanged.
- Filter, transform and reorder elementsUse filter, map, mapToInt, flatMap, distinct, sorted, limit and skip, and move between object streams and primitive streams.
- Reduce a stream to a single resultCompute counts, sums, averages, extremes and true-or-false answers with terminal operations, and handle missing results with Optional.
- Build a stream pipeline and understand how it runs
- Gather stream results into lists, sets, maps and strings, group and partition data with collectors and decide when a stream or a loop is the better tool.
- Collect results into lists, sets, maps and stringsUse toList, Collectors.toList, toSet, toCollection, joining and toMap, and handle unmodifiable results and duplicate keys.
- Group and partition data with collectorsSplit data into groups with groupingBy and partitioningBy and summarize each group with a downstream collector.
- Choose between streams and loops in a complete taskSolve a data-processing task from start to finish, compare a stream solution with a loop and keep pipelines free of side effects.
- Collect results into lists, sets, maps and strings
Generics, Lambdas and Streams in Java study guide
1Write generic classes and methods
Generic code keeps a useful relationship between inputs, stored values and results. A class can keep that relationship for an object, while a method can choose it anew for each call. Bounds and wildcards let APIs accept the types they need without allowing unsafe writes. The compiler checks those promises before erasing type parameters, which explains both the benefits of generics and the run-time operations they cannot support.
1.1 Define and use generic classes
A container should tell you what it holds. With a type parameter, one class can hold strings, numbers or your own objects, and the compiler checks each use before the program runs.
Give a class a type parameter
In class Box<T>, T is a type parameter: a name for a type that the caller will supply. Use it for fields, method parameters and results.
class Box<T> {
private T value;
public Box(T value) { this.value = value; }
public T get() { return value; }
public void set(T value) { this.value = value; }
}
The constructor's name is Box, without <T>. The declaration after the class name introduces T for the class body. Now create a box:
Box<String> b = new Box<>("tea");
String drink = b.get();
b.set("water");
System.out.println(drink); // tea
System.out.println(b.get()); // water
String is the type argument. For this variable, get() returns a String, and set accepts a String. There is no cast when reading. The attempted call b.set(42) fails at compilation: "incompatible types: int cannot be converted to String". The error marks the bad write before a later read can fail.
Why not store Object?
An Object field can refer to either a string or an integer. Code that needs a string must cast the returned object. If an integer was stored, that cast throws ClassCastException while the program runs. A Box<String> makes the intended relationship part of the declared type.
The diamond <> in new Box<>("tea") asks the compiler to infer the type arguments. It keeps the checks. Omitting angle brackets altogether, as in new Box("tea"), uses a raw type and loses checks involving the type parameter. Assigning a raw construction to a typed variable produces unchecked warnings; the typed variable still enforces its own declared argument on later calls.
Use wrappers and several parameters
A type argument must be a reference type. Use Box<Integer>, not Box<int>. Java boxes the literal 5 as an Integer when constructing new Box<Integer>(5) and unboxes it when arithmetic needs an int. If n holds that box, int next = n.get() + 1; gives 6.
Two independent types need two parameters. A Pair<K, V> can have a K first field and a V second field. For a Pair<String, Integer>, its getters return a String and an Integer. Common names are T for a type, E for an element, K and V for a key and value, and R for a result.
Put type parameters on interfaces
interface Container<T> {
void put(T item);
T take();
}
A class can preserve the choice with class Shelf<T> implements Container<T>, or fix it with class NameList implements Container<String>. List<E> and Comparable<T> use the same idea. A generic class remains one class: Box<String> and Box<Integer> describe different compile-time uses, not two class declarations. They cannot be assigned to each other because their element promises differ.
Key Takeaways
- A type parameter names a type inside a declaration; a type argument supplies it at a use.
- A generic getter returns the declared element type without a cast.
- The diamond preserves type checks; a raw type loses generic checks.
- Use wrapper classes such as
Integeras type arguments. - Classes and interfaces can have more than one type parameter.
1.2 Write generic methods
Some operations need a type choice for one call, not for an object's lifetime. A generic method introduces its own type parameter and keeps a relationship between its input and its result.
Declare the type before the return type
import java.util.List;
class Lists {
public static <T> T last(List<T> list) {
return list.get(list.size() - 1);
}
public static void main(String[] args) {
int number = last(List.of(4, 8, 15));
String word = Lists.<String>last(List.of("x", "y"));
System.out.println(number); // 15
System.out.println(word); // y
}
}
Read the declaration in pieces. static makes it a class method. <T> introduces a type parameter belonging to the method. The next T is the return type. List<T> says that the input list and the result share the element type.
For the first call, Java infers T as Integer. The result can then be unboxed into int. In the second call, <String> is an explicit type witness, placed between the dot and the method name. Lists.last(...) would infer the same type here. A witness needs a receiver before the dot: a class name for a static method, or an object such as this for an instance method.
This method expects a nonempty list. On an empty list, size() - 1 is -1, so get throws IndexOutOfBoundsException. Generics check types; they do not prove that an index exists.
Connect several types
A generic method can introduce several parameters. Given a Pair<K, V> class with a constructor and getters, this complete method reverses the roles:
static <K, V> Pair<V, K> swap(Pair<K, V> p) {
return new Pair<>(p.getSecond(), p.getFirst());
}
For an input pair ("Ada", 36) of type Pair<String, Integer>, the result is (36, "Ada") of type Pair<Integer, String>. The return declaration carries the relationship; a return type of Object would hide it and force the caller to cast.
A method such as <T> int countEqual(List<T> list, T target) also relates inputs. Its loop can compare each item with target using equals. With ["a", "b", "a", "c", "a"] and "a", the count is 3. Inference may choose a common supertype for arguments, so repeating T does not always force two argument expressions to have identical declared types.
Decide what owns the type parameter
A Box<String> keeps its type argument for that object's use. Lists.last chooses a type argument afresh for each call. An ordinary utility class can therefore contain generic static methods without being generic itself.
A static method inside Box<T> cannot use the class's T: the static method belongs to the shared class, not one box. Declare a separate parameter, for example static <E> Box<E> of(E value). If a method declaration starts static T first(...) with no method <T>, the compiler reports that it cannot find T in an ordinary class. Add the declaration before the return type; changing the return type to Object would discard the useful relationship.
Key Takeaways
- A generic method declares its own parameters before its return type.
- Inference happens separately for every call.
- A type witness appears after a dot and before the method name.
- Use several type parameters when input and output have several related roles.
- A static method must declare its own type parameter.
- Type safety does not check a method’s nonempty-input requirement.
1.3 Restrict type parameters with bounds and wildcards
An unrestricted T supports only operations available on Object. Bounds add a useful promise. Wildcards let a parameter accept a family of generic types without pretending that all their element types are identical.
Require the operations you need
static <T extends Comparable<T>> T largest(List<T> list) {
T best = list.get(0);
for (T x : list) {
if (x.compareTo(best) > 0) {
best = x;
}
}
return best;
}
The bound says that a T can compare itself with another T. Starting with 3, the call largest(List.of(3, 11, 7)) replaces the best with 11, then leaves it unchanged for 7. It returns 11. For ["pear", "apple", "plum"], the largest word is "plum" in natural order. An empty list fails at get(0).
extends introduces bounds for interfaces too. <T extends Number> makes doubleValue() available. Multiple bounds use &, with a class first if present: <T extends Number & Comparable<T>>.
Why List is not List
Integer extends Number, but List<Integer> is not a subtype of List<Number>. Generic types are invariant. If that assignment were allowed, code using the List<Number> reference could add a Double; the original owner would then read it as an Integer. A List<Number> parameter does accept implementations such as ArrayList<Number>, but it rejects List<Integer>.
Read with extends; add with super
static double sum(List<? extends Number> nums) {
double total = 0;
for (Number n : nums) total += n.doubleValue();
return total;
}
static void addOneToThree(List<? super Integer> target) {
target.add(1);
target.add(2);
target.add(3);
}
? extends Number means one unknown type that is Number or a subtype. sum accepts lists of integers, doubles or numbers. It reads each element as a Number. sum(List.of(1.5, 2.5)) gives 4.0. It cannot add 3: the unknown element type might be Double. The type rules permit adding null, though a particular list may reject it at run time.
? super Integer means Integer or a supertype. The second method can add integers to a modifiable List<Integer>, List<Number> or List<Object>. A read through that parameter has only type Object: existing elements might be other numbers or objects.
| Parameter | Safe element operation |
|---|---|
List<? extends T> | Read an element as T |
List<? super T> | Add a T; read as Object |
List<T> | Read and write using the same named type |
List<?> | Read as Object; inspect size |
The memory aid is producer extends, consumer super. It describes the direction of element transfer, not whether the whole collection is immutable. For example, removing elements may still be possible through an extends view.
Use a named parameter when a type must connect several positions, especially an input and a return type. Use a wildcard when only a range of accepted inputs matters. Collections.copy reads from List<? extends T> and writes into List<? super T>; its destination must already have enough positions to replace.
Key Takeaways
- Bounds make the bound type’s methods available on
T. - Generic invariance protects a list from writes of the wrong element type.
- Use
? extends Tto read elements asT. - Use
? super Tto add values of typeT; reads have typeObject. - A wildcard restricts operations through a reference; it does not make the object immutable.
- Use a named type parameter to connect input and result types.
Collections.copyreplaces existing destination positions.
1.4 Work within the limits of type erasure
Generics give the compiler more information than an ordinary run-time class test has. Understanding what remains after compilation explains raw-type warnings and several operations that Java rejects.
What erasure removes
Java checks type arguments, then erases type parameters to their bounds. An unbounded T becomes Object; a T extends Number becomes Number. The compiler inserts casts where needed when values are read. There is one run-time Box class, so this prints true:
System.out.println(
new Box<String>("a").getClass()
== new Box<Integer>(1).getClass()
);
The Box declaration can be the generic class with a value field, constructor and getter. Both constructions have the same run-time class even though their declared uses have different compile-time checks.
Operations that need a known run-time type
Inside a generic class, new T() does not compile. Java does not know what constructor the caller's type has. Accept a factory to create the value:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
class Factory {
static <T> T create(Supplier<T> factory) {
return factory.get();
}
public static void main(String[] args) {
List<String> words = create(ArrayList::new);
words.add("ready");
System.out.println(words); // [ready]
}
}
The supplier provides the creation behavior. new T[10] and new List<Integer>[2] are also rejected as generic array creation. Store a sequence in ArrayList<T> when you do not need array behavior.
For an Object obj, obj instanceof List<String> cannot safely test the element argument. Use obj instanceof List<?> to test whether it is a list, then treat its elements as unknown. That test does not establish a string element type.
Shared static members and erased overloads
A field such as static T shared cannot use the enclosing class's T. All parameterizations share the static member. A static method can introduce its own <E>, independent of the class parameter.
Two overloads print(Set<String> s) and print(Set<Integer> s) cannot coexist in one class. Both erase to a method accepting Set, leaving a name clash. Give the operations different names, or design one method that handles the common behavior.
Follow an unchecked warning to the bad read
List raw = new ArrayList();
raw.add("text");
raw.add(42);
for (Object value : raw) {
String text = (String) value;
System.out.println(text);
}
This prints text, then the cast of 42 throws ClassCastException. The raw declaration can trigger a raw-type diagnostic; the additions trigger unchecked-call warnings because generic checks have been bypassed. Replacing it with List<String> moves the failure to add(42) at compile time. An unchecked warning signals a lost guarantee, a risk of execution failure. Fix the raw use or unsafe cast instead of assuming a later read will be safe.
Key Takeaways
- Erasure replaces type parameters by their bounds, or
Objectwhen unbounded. - Parameterized uses share one run-time class.
- Use a supplied factory instead of
new T(). - Erased parameter types cannot distinguish generic overloads.
2Pass behavior with lambda expressions
A method can accept behavior as well as data. Functional interfaces describe the shape of that behavior, and lambdas supply the work to perform. The standard interfaces give common jobs clear names: testing, transforming, acting and supplying. Captured variables let a rule use surrounding information, while method references name existing behavior. Matching the target type keeps each piece of syntax connected to an ordinary method call.
2.1 Write lambda expressions for functional interfaces
A method can accept a rule as an argument. A lambda writes that rule where it is used, while a functional interface tells Java what inputs the rule takes and what it returns.
Match one abstract method
A functional interface has one abstract method to implement. Declarations matching public instance methods of Object, such as equals(Object), do not count. Default and static methods do not add abstract work. Comparator<T> and Runnable are functional interfaces.
@FunctionalInterface
interface Operation {
int apply(int a, int b);
}
class Combine {
static int combine(int a, int b, Operation op) {
return op.apply(a, b);
}
public static void main(String[] args) {
Operation add = (a, b) -> a + b;
System.out.println(add.apply(3, 4)); // 7
System.out.println(combine(3, 4, (x, y) -> x * y)); // 12
}
}
The interface supplies the method name apply, the two int parameter types and the int result type. The lambda supplies the parameter names and body. It creates an implementation usable through Operation; it does not call apply until the program invokes it.
combine always passes its two numbers to op. The caller changes the behavior by supplying addition or multiplication. This is also how sort receives an ordering rule and filter receives a condition.
Read the syntax
A lambda has parameters, ->, then an expression or a block. One inferred parameter can omit parentheses: w -> w.length() > 5. Zero parameters need (), and two or more need parentheses, as in (a, b) -> a * b. Explicit types are allowed: (int a, int b) -> a + b.
An expression body returns its result automatically. A block that produces a value must return it:
Operation max = (a, b) -> {
if (a > b) {
return a;
}
return b;
};
System.out.println(max.apply(3, 9)); // 9
Every path through this block returns an int. Adding braces around a + b without return does not create a valid value-returning block.
Let the target supply the meaning
The target type is the functional interface expected by an assignment, argument or return position. The text x -> x * 2 can fit Function<Integer, Integer> or UnaryOperator<Integer>. Its parameter and result types come from that target. Object o = () -> 1; fails because Object supplies no functional method.
@FunctionalInterface asks the compiler to check the interface definition. It is useful protection if somebody later adds a second distinct abstract method. Without the annotation, such an interface can still be declared, but a lambda cannot implement its two unrelated requirements.
A lambda can also clash with local names. If the surrounding method already declares int a, it cannot declare a lambda parameter named a in that scope. Choose another parameter name. Compared with an anonymous class implementing Comparator<String>, a lambda removes the repeated class and method declaration; the ordering computation still has to satisfy the comparator contract.
Key Takeaways
- A lambda supplies the implementation of a functional interface’s abstract method.
- The target interface determines input types, result type and method name.
- Expression bodies return a value directly; value-returning blocks need
return. @FunctionalInterfacechecks the interface definition.- A lambda parameter cannot redeclare a surrounding local variable.
2.2 Use the standard functional interfaces
Java already names the common shapes of behavior. Choose an interface by the number of inputs and the kind of result, then call its functional method to run the behavior.
Choose an input and output shape
| Interface | Functional method | Job |
|---|---|---|
Predicate<T> | boolean test(T t) | Test a condition |
Function<T, R> | R apply(T t) | Transform a value |
Consumer<T> | void accept(T t) | Perform an action |
Supplier<T> | T get() | Supply a value without input |
These interfaces are in java.util.function. For strings, w -> w.length() > 5 fits a predicate, String::length fits a function returning Integer, and System.out::println fits a consumer. A supplier such as () -> new ArrayList<String>() creates a fresh list whenever get() runs. A condition used by filter needs Predicate<T>, even though a Function<T, Boolean> could also compute a Boolean value in other code.
UnaryOperator<T> is a function from T to the same T. BinaryOperator<T> combines two values of type T into a T. BiFunction<T, U, R>, BiPredicate<T, U> and BiConsumer<T, U> take two inputs. Primitive forms avoid boxing: IntPredicate.test(int), IntBinaryOperator.applyAsInt(int, int) and ToIntFunction<T>.applyAsInt(T).
Combine rules in a stated order
import java.util.function.Function;
import java.util.function.Predicate;
class Rules {
public static void main(String[] args) {
Predicate<String> isLong = w -> w.length() > 5;
Predicate<String> longB = isLong.and(w -> w.startsWith("b"));
System.out.println(longB.test("banana")); // true
System.out.println(isLong.negate().test("kiwi")); // true
Function<Integer, Integer> twice = x -> x * 2;
Function<Integer, Integer> inc = x -> x + 1;
System.out.println(twice.andThen(inc).apply(5)); // 11
System.out.println(twice.compose(inc).apply(5)); // 12
}
}
and requires both conditions, or requires either, and negate reverses the answer. Predicate.not(p) also negates a predicate. andThen runs the receiver first; compose runs its argument first. Here the first chain computes 10 then 11, while the second computes 6 then 12.
Pass a rule to a method
static <T> List<T> keep(List<T> list, Predicate<T> test) {
List<T> kept = new ArrayList<>();
for (T x : list) {
if (test.test(x)) kept.add(x);
}
return kept;
}
keep(List.of(4, 9, 12, 15), n -> n % 3 == 0) gives [9, 12, 15]. T makes element types reusable; the predicate makes selection behavior reusable.
For a modifiable list [3, 8, 1, 10, 6], removeIf(n -> n < 5) leaves [8, 10, 6]. replaceAll(n -> n * 2) then gives [16, 20, 12]. These methods change the list; forEach accepts a consumer for actions such as printing. Copy a List.of(...) result into an ArrayList before using modifying operations.
For maps, forEach((key, value) -> ...) takes a BiConsumer. merge(word, 1, Integer::sum) stores 1 for a new word and increments an existing count. computeIfAbsent(key, k -> new ArrayList<>()) creates a value for a missing or null-mapped key and returns the resulting value. These methods keep the lookup and update rule together.
Key Takeaways
- Choose a functional interface from its input and result shape.
andThenruns the receiver first;composeruns its argument first.removeIftests a predicate;replaceAllapplies a unary operator.mergecombines an existing map value with a supplied value.computeIfAbsentcan build and store a missing map value.- Primitive specializations work directly with primitive values.
2.3 Capture variables and write method references
A lambda can use information from its surrounding method. When its body only calls one existing method, a method reference often expresses the same behavior more directly.
Capture a local variable
int limit = 10;
Predicate<Integer> small = n -> n < limit;
System.out.println(small.test(7)); // true
The lambda captures limit. A captured local variable must be final or effectively final: it is assigned once and never reassigned. Adding limit = 20; anywhere after that first assignment makes the capture fail to compile, even if the reassignment appears before the lambda.
The rule applies to the local variable, not to all state inside its object. This compiles:
StringBuilder log = new StringBuilder();
Runnable action = () -> log.append("ran");
action.run();
System.out.println(log); // ran
log still refers to the same builder. Updating a field of the surrounding object is also permitted, but hidden changes can make behavior hard to track.
Trying numbers.forEach(n -> total += n) with a local int total = 0 fails because it changes the captured variable. Use a loop when the task is an update, or compute a result directly with numbers.stream().mapToInt(n -> n).sum().
Read the four reference forms
A method reference uses ::, without an argument list. The target functional interface supplies the inputs and expected result.
| Kind | Reference | Equivalent lambda |
|---|---|---|
| Static method | Integer::parseInt | s -> Integer.parseInt(s) |
| Bound instance | System.out::println | x -> System.out.println(x) |
| Unbound instance | String::length | s -> s.length() |
| Constructor | ArrayList::new | () -> new ArrayList<>() |
For the bound reference, the receiver is already fixed: the PrintStream referred to by System.out. For the unbound reference, the first functional argument becomes the receiver. Thus String::compareToIgnoreCase expands to (a, b) -> a.compareToIgnoreCase(b): the first string is the receiver and the second is the method argument.
Match the reference to a target
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
class References {
public static void main(String[] args) {
Function<String, Integer> parse = Integer::parseInt;
Function<String, Integer> length = String::length;
Supplier<List<String>> fresh = ArrayList::new;
System.out.println(parse.apply("42")); // 42
System.out.println(length.apply("stream")); // 6
System.out.println(fresh.get()); // []
}
}
A constructor reference can take arguments too. Student::new fits a Function<String, Student> when a matching constructor takes one String. The target determines which constructor overload is selected.
Student::getName refers to an instance getter on each supplied student; it does not require a static getter. Write a lambda when there is extra work, as in s -> s.getPoints() * 2 or w -> w.length() > 5. Writing String::length() is invalid: the reference names behavior without invoking it there.
Key Takeaways
- A captured local variable must be final or effectively final.
- A final reference can still refer to a mutable object.
- A bound reference fixes its receiver; an unbound reference takes it from the first argument.
- Constructor references use
ClassName::new. - Method references have no invocation parentheses.
3Order objects with Comparator
The same objects can need several useful orders: names alphabetically, points from high to low, or groups with a score tie-breaker. Comparators keep these rules separate from the class’s natural order. A correct comparison must keep its sign and ordering relationships consistent. Builder methods make the keys visible, but the position of a reversal and the handling of ties or null values still determine the result.
3.1 Write a comparator and sort with it
An object's natural order is not always the order a task needs. A separate Comparator<T> lets you sort the same objects by different rules without changing their class.
Read the sign of compare
compare(a, b) returns a negative number when a comes first, zero when the two rank equally, and a positive number when a comes after b. Only the sign matters. A comparator does not return a Boolean.
Comparable<T> belongs to the class and supplies its natural order through compareTo. A comparator is a separate value passed to a sorting method. Strings have a natural character order, but a comparator can sort them by length instead.
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class SortWords {
public static void main(String[] args) {
List<String> words = new ArrayList<>(List.of("ccc", "a", "bb"));
Comparator<String> byLength =
(a, b) -> Integer.compare(a.length(), b.length());
words.sort(byLength);
System.out.println(words); // [a, bb, ccc]
}
}
The comparator extracts lengths and compares the numbers. words.sort changes the modifiable list and returns void; it does not return a new list. Sorting a List.of(...) result throws UnsupportedOperationException, so the example first makes an ArrayList copy.
Compare numbers safely
Use Integer.compare or Double.compare. Subtraction can reverse the sign through overflow: 2_000_000_000 - (-2_000_000_000) evaluates to -294,967,296 as an int. That wrongly puts the larger number first in ascending order. Integer.compare(2_000_000_000, -2_000_000_000) correctly returns 1.
Casting a difference of doubles to int also loses information. (int) (1.2 - 0.7) is 0, although the first value is larger. Double.compare(1.2, 0.7) reports a positive result.
Keep the ordering consistent
Reversing the inputs must reverse the comparison sign. If a comes before b and b before c, a must come before c. Values comparing as zero must rank the same against every other value. Breaking these rules can give a wrong order or make sorting throw IllegalArgumentException with "Comparison method violates its general contract!". Returning only 0 or 1 cannot express both directions.
Java's list sorts and comparator-based object-array sorts are stable: ties keep their original order. For students Ada 85, Ben 72, Chen 85, Dara 90 and Eli 64, sorting by ascending points gives Eli, Ben, Ada, Chen, Dara. Ada stays before Chen because they tie and Ada appeared first.
The student examples use this class and a modifiable list:
class Student {
private final String name;
private final String group;
private final int points;
Student(String name, String group, int points) {
this.name = name;
this.group = group;
this.points = points;
}
String getName() { return name; }
String getGroup() { return group; }
int getPoints() { return points; }
}
List<Student> students = new ArrayList<>(List.of(
new Student("Ada", "A", 85), new Student("Ben", "B", 72),
new Student("Chen", "A", 85), new Student("Dara", "B", 90),
new Student("Eli", "A", 64)));
students.sort(Comparator.comparingInt(Student::getPoints));
students.forEach(s -> System.out.println(s.getName()));
// Eli, Ben, Ada, Chen, Dara, each on a separate line
Choose a sorting entry point
list.sort(cmp) and Collections.sort(list, cmp) change a modifiable list. list.sort(null) requests natural order. Arrays.sort(array, cmp) accepts an object array such as Integer[]; an int[] has no comparator overload.
Natural string order compares UTF-16 code units. Uppercase A–Z precede lowercase a–z, so ["banana", "Cherry", "apple"] becomes [Cherry, apple, banana]. String.CASE_INSENSITIVE_ORDER gives [apple, banana, Cherry] for those words. Choose the text rule explicitly when capitalization should not decide the order.
Key Takeaways
- A comparator returns a signed comparison result, not a Boolean.
Comparablesupplies natural order; a comparator supplies a separate order.- Use numeric comparison methods to avoid overflow and truncation.
- Stable sorts retain the original order of ties.
- List sorting changes a modifiable list and returns
void. - Primitive arrays cannot take a comparator.
3.2 Combine comparators with comparing, thenComparing and reversed
A multi-key order is easiest to read as a sequence of decisions: compare the main key, then compare a second key only if the first tied. Comparator builders express those decisions directly.
Extract a key and break ties
For a Student with getName(), getGroup() and getPoints(), Comparator.comparing(Student::getName) orders by name. comparingInt(Student::getPoints) extracts an int without boxing it.
Comparator<Student> ranking =
Comparator.comparingInt(Student::getPoints)
.reversed()
.thenComparing(Student::getName);
This ranks high points first, then names in ascending order for equal points. For Ada (A, 85), Ben (B, 72), Chen (A, 85), Dara (B, 90), Eli (A, 64), it gives Dara, Ada, Chen, Ben, Eli. The name comparison runs only for a points tie.
Place reversed at the right level
reversed() reverses everything built before it. Reversing the ranking after thenComparing would also reverse names within each score tie. To order groups ascending and points descending inside each group, reverse only the points comparator:
Comparator<Student> byGroup =
Comparator.comparing(Student::getGroup)
.thenComparing(
Comparator.comparingInt(Student::getPoints).reversed());
The result is Ada, Chen, Eli, Dara, Ben: group A precedes B; within A, 85 precedes 64. Putting .reversed() at the end instead would reverse both group order and point order relative to the preceding chain.
Choose text and null rules
Comparator.naturalOrder() follows Comparable; reverseOrder() reverses it. Supply a key comparator when a key needs special handling, as in Comparator.comparing(Student::getName, String.CASE_INSENSITIVE_ORDER).
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
class SortNullable {
public static void main(String[] args) {
List<String> words = new ArrayList<>();
words.add("b");
words.add(null);
words.add("a");
words.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
System.out.println(words); // [null, a, b]
}
}
Wrap the comparison at the level where null occurs. nullsLast around the whole student comparator handles null student objects. A name extractor returning null needs a null-aware comparator for the name key itself. Natural-order and ordinary key comparators do not handle null keys automatically.
Sort map entries and resolve inference
A HashMap has no promised encounter order. Stream its entries and choose an order explicitly:
stock.entrySet().stream()
.sorted(Map.Entry.<String, Integer>comparingByValue().reversed())
.forEach(System.out::println);
For rice=12, oats=3 and flour=7, this prints rice=12, flour=7, oats=3, on separate lines. It orders the streamed entries without reorganizing stock. Add thenComparing(Map.Entry.comparingByKey()) if tied values need a predictable key order.
The explicit <String, Integer> helps inference across the chained call. Similarly, Comparator.comparing(s -> s.getName()).reversed() can infer s as Object. Use Student::getName or (Student s) -> s.getName() to supply the missing type. A named comparator can be reused with list sorting, stream sorted, min and max, or an ordered map or set.
Key Takeaways
comparingextracts a key; primitive builders avoid boxing numeric keys.thenComparingis used only when the preceding comparison ties.reversed()reverses the comparator already built.- Handle nulls where they occur: whole objects or extracted keys.
- Sorting a stream of entries does not reorder its source map.
4Process collections with streams
A stream pipeline turns a collection into a sequence of processing decisions. Follow the values and their types as a filter selects, a mapping changes them and a terminal operation creates the answer. Lazy execution means some elements may never be visited, while sorting needs the whole input. Numeric streams and optionals make result types explicit, including what happens when no values survive or a numeric total is too large.
4.1 Build a stream pipeline and understand how it runs
A stream describes how to process a sequence of values. It reads from a source, passes elements through intermediate operations and ends with a terminal operation that produces a result or performs an action.
Identify the three parts
import java.util.List;
class CountPasses {
public static void main(String[] args) {
List<Integer> points = List.of(72, 45, 90, 58, 81);
long passed = points.stream()
.filter(p -> p >= 60)
.count();
System.out.println(passed); // 3
System.out.println(points); // [72, 45, 90, 58, 81]
}
}
points.stream() is the source. filter is an intermediate operation: it returns another stream. count is the terminal operation: it returns a long. The values 72, 90 and 81 survive the filter. Reading through the stream has not removed 45 or 58 from points.
Pick a source
Collections expose stream(). A map exposes streams through entrySet(), keySet() or values(). Arrays.stream(array) streams array elements; an int[] produces an IntStream. Stream.of("a", "b", "c") supplies listed values.
IntStream.range(1, 5) contains 1, 2, 3, 4, with the end excluded. rangeClosed(1, 5) includes 5 and sums to 15. Stream.iterate(1, n -> n * 2) has no fixed endpoint; .limit(5).toList() collects [1, 2, 4, 8, 16]. A bounded result is essential when consuming that infinite sequence.
Trace lazy execution
Intermediate operations are lazy. Merely writing names.stream().map(String::toUpperCase); does not perform the transformations. A terminal operation starts processing, and elements are consumed only as needed.
String first = java.util.stream.Stream.of("one", "two", "three", "four")
.filter(w -> w.length() > 3)
.map(String::toUpperCase)
.findFirst()
.orElse("none");
System.out.println(first); // THREE
The first two words fail the filter. "three" passes, becomes "THREE", and satisfies findFirst. The final word is not needed. This is short-circuiting: the answer becomes known before the whole source is read.
sorted needs all input before it can emit the first sorted value. Some other stateful operations, such as sequential distinct, can emit values as input arrives. An operation's place in the chain affects how much work is needed.
Create a fresh stream for another result
After a terminal operation, the stream is consumed. Reusing that stream commonly throws IllegalStateException with "stream has already been operated upon or closed". Call points.stream() again for another pipeline. A collection is reusable storage; a stream is one processing path.
Creating a stream does not copy its elements. Keep the source unchanged while processing it. map produces mapped values; it does not write them back into the list. For intentional list changes, methods such as replaceAll and removeIf express that job directly. Use one operation per line so the processing order and result type remain visible.
Key Takeaways
- A pipeline has a source, intermediate operations and one terminal operation.
- Intermediate operations are lazy.
- Short-circuiting can avoid reading the rest of a source.
sortedmust see all input before emitting sorted output.- A consumed stream must be replaced by a fresh source stream.
- Creating a stream neither copies nor rewrites its source.
4.2 Filter, transform and reorder elements
Track both the values and their type after every operation: a later lambda receives the current stream element, not the original source object.
Select before transforming
filter keeps elements that pass a predicate. map replaces each element with a computed value. Given students with name and points getters, selecting points must happen before mapping to names:
List<String> names = students.stream()
.filter(s -> s.getPoints() >= 80)
.map(Student::getName)
.sorted()
.toList();
For Ada 85, Ben 72, Chen 85, Dara 90 and Eli 64, this yields [Ada, Chen, Dara]. Before map, the element type is Student. After it, the type is String, so a points getter is no longer available.
Move to a primitive stream
Stream<Integer> holds references to wrapper objects and has no sum() method. mapToInt(Integer::intValue) produces an IntStream, which supplies numeric operations. mapToInt(String::length) similarly measures strings as primitive lengths.
import java.util.List;
import java.util.stream.IntStream;
class Windows {
public static void main(String[] args) {
List<Integer> window = IntStream.rangeClosed(1, 10)
.skip(2)
.limit(3)
.boxed()
.toList();
System.out.println(window); // [3, 4, 5]
}
}
skip(2) removes 1 and 2 from the stream. limit(3) keeps the next three values. Reversing those calls leaves only 3. boxed() returns to Stream<Integer> so the elements can be collected as objects. mapToObj creates other objects from primitive values; long and double streams have matching conversion operations.
Remove duplicates and choose an order
distinct uses object equality. ["map", "filter", "map", "sorted", "filter"] becomes [map, filter, sorted], then sorted() gives [filter, map, sorted]. For your own classes, implement consistent equals and hashCode if matching field values should count as duplicates.
sorted() uses natural order. Supply a comparator for another order or for objects without a natural order. Sorting an ordered stream is stable. Sorting the five students by descending points, then taking two, selects Dara and Ada: Ada precedes Chen in the original list at their tied score. Calling limit(2) before sorting would examine only the original first two students.
Flatten nested sequences
List<Integer> all = List.of(
List.of(1, 2), List.of(3), List.of(4, 5, 6))
.stream()
.flatMap(List::stream)
.toList();
System.out.println(all); // [1, 2, 3, 4, 5, 6]
Each inner list becomes a stream, and flatMap joins its elements into one stream. Replacing flatMap with map here gives a Stream<Stream<Integer>>: one stream object per inner list, without flattening it.
peek passes a value on unchanged and can print it for debugging. Its action runs only for elements actually traversed, and an implementation may skip an unneeded stage, for example when optimizing count. Do not use it to perform required work. For a single largest value, use max(comparator) instead of sorting every element and taking one.
Key Takeaways
filterkeeps the element type;mapcan change it.- Primitive streams provide numeric operations absent from
Stream<Integer>. - The order of
skip,limitand sorting changes the selected elements. distinctfollows equality; ordered-stream sorting preserves ties.flatMapflattens streams produced from the input elements.peekis for inspection, not required program effects.
4.3 Reduce a stream to a single result
Choose the terminal operation from the result you need: a count, a number, a truth value, one element or a general combination. Empty input matters whenever no result value can be chosen.
Compute a number
count() returns a long. Primitive-stream sum() returns a number of the stream's primitive type. For points [72, 45, 90, 58, 81], filtering for at least 60 then summing gives 243. The average of those three values is 81.0.
import java.util.List;
class PointResults {
public static void main(String[] args) {
List<Integer> points = List.of(72, 45, 90, 58, 81);
int sum = points.stream().filter(p -> p >= 60)
.mapToInt(Integer::intValue).sum();
double average = points.stream().filter(p -> p >= 60)
.mapToInt(Integer::intValue).average().orElse(0.0);
System.out.println(sum); // 243
System.out.println(average); // 81.0
}
}
The second pipeline uses a fresh stream. An empty sum is 0, but an empty average has no value. average() returns OptionalDouble, and orElse(0.0) supplies a chosen fallback. That default is a program decision, not the mathematical average of an empty set.
An int sum can overflow. Summing two values of 2,000,000,000 produces -294,967,296 as an int. Convert before adding: mapToInt(...).asLongStream().sum() gives 4,000,000,000 as a long.
Handle one value or none
An Optional<T> contains one value or no value. Object-stream min, max, findFirst, findAny and reduction without an identity return optionals. points.stream().max(Integer::compare) gives Optional[90]; printing the container includes the wrapper text.
Use orElse for a fallback value, orElseGet for a fallback supplier, ifPresent for an action, or map to transform a present value. Filtering for a student above 95 points, mapping the first match to a name, and applying orElse("nobody") gives "nobody" for the five-student data. Calling get() or orElseThrow() on an empty optional throws NoSuchElementException.
Ask a true-or-false question
anyMatch asks whether at least one value passes, allMatch whether every value passes, and noneMatch whether none passes. They can stop once the answer is known. On empty input, anyMatch is false, while allMatch and noneMatch are true: there is no value that breaks either universal claim.
Combine with an identity
Stream.of(3, 5, 7).reduce(0, Integer::sum) accumulates 0, 3, 8, 15. For multiplication, start with 1 and use (a, b) -> a * b to get 105. An identity leaves a value unchanged under the operation; 10 is not an addition identity and would incorrectly shift the total to 25. The accumulator must be associative so grouping does not change the result.
Without an identity, reduce(Integer::max) gives Optional[7] and an empty optional for empty input. forEach instead performs an action and returns void; it suits printing. Use reduction or collection when the task is to produce a result value.
Key Takeaways
- Counts use
long; numeric stream results follow their primitive type. - An empty sum is zero; an empty average or extreme has no value.
- Use an optional’s empty-case operations before reading its value.
- Empty
allMatchandnoneMatchare true; emptyanyMatchis false. - Reduction needs an identity and an associative accumulator.
- Widen to
longbefore summing values that exceed theinttotal range.
5Collect and apply stream results
A useful stream result may be a list, set, map, string or summary for each group. The collector decides what is stored and what happens when keys repeat. Grouping combines selection rules with downstream summaries; partitioning gives two Boolean groups. A complete task can then query the resulting map. Comparing that pipeline with a loop helps you choose readable code and keep result-building separate from changes to outside state.
5.1 Collect results into lists, sets, maps and strings
A collector turns a stream into a useful result structure. Decide what the result must support: duplicates, ordering, later changes or duplicate map keys. Different collectors make different promises.
Choose a list or set
stream.toList() returns an unmodifiable list in encounter order. Adding or removing elements throws UnsupportedOperationException. collect(Collectors.toList()) also collects a list, but does not guarantee a particular implementation or modifiability. For a list you will change, request it explicitly:
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
class Gather {
public static void main(String[] args) {
List<String> words = Stream.of("a", "b", "a")
.collect(Collectors.toCollection(ArrayList::new));
words.add("c");
System.out.println(words); // [a, b, a, c]
System.out.println(Stream.of("a", "b", "a")
.collect(Collectors.toSet()).size()); // 2
}
}
toSet removes duplicates and promises no order. toCollection(TreeSet::new) requests a sorted set. A list keeps duplicate elements; choose a set when the result represents distinct values.
Join text
Collectors.joining(", ") joins strings with a separator, without a trailing separator. Its three-argument form also adds a prefix and suffix:
String names = Stream.of("Ada", "Chen", "Dara")
.collect(Collectors.joining(", ", "[", "]"));
System.out.println(names); // [Ada, Chen, Dara]
The elements must be CharSequence values, usually strings. Map students to Student::getName or numbers with String::valueOf before joining. Joining returns one string, not a list that merely prints similarly.
Choose map keys and values
Collectors.toMap(keyFunction, valueFunction) computes an entry from every element. With the five students Ada (A, 85), Ben (B, 72), Chen (A, 85), Dara (B, 90), Eli (A, 64), mapping names to points gives five entries such as Ada=85 and Eli=64.
Grouping keys are different: several students have the same group. A two-argument toMap(Student::getGroup, Student::getPoints) throws IllegalStateException when a key repeats, even if both values are 85. A map needs one resulting value per key, and this form supplies no rule for choosing it.
Provide a third argument to merge duplicates:
Map<String, Integer> totals = students.stream()
.collect(Collectors.toMap(
Student::getGroup, Student::getPoints, Integer::sum));
Group A receives 85 + 85 + 64 = 234; group B receives 72 + 90 = 162. Replacing Integer::sum with Math::max keeps the highest points per group, A=85 and B=90. Use the rule that answers the task, not an arbitrary first value.
toMap promises no particular map order. A fourth argument can request one, for example TreeMap::new for sorted keys. toMap(Student::getName, Student::getPoints, (a, b) -> a, TreeMap::new) keeps the first value on duplicate names and sorts by name. A collector also avoids creating an outside list and mutating it in forEach; the result is built by the operation designed to gather it.
Key Takeaways
Stream.toList()is unmodifiable.Collectors.toList()does not guarantee a modifiable implementation.- Use
toCollectionwhen a specific collection type is required. joininggathers text with an optional separator, prefix and suffix.- Two-argument
toMaprejects duplicate keys, including equal duplicate values. - A merge function resolves repeated map keys.
- Request a map factory when its ordering matters.
5.2 Group and partition data with collectors
A grouped result answers a question for each key. A partition answers a yes-or-no question with two groups. A downstream collector determines what each group stores.
Group by a computed key
groupingBy(classifier) produces a map from each encountered key to a list of its elements. Grouping ["fig", "kiwi", "pear", "plum", "apple", "mango", "banana"] by word length gives length 4 the list [kiwi, pear, plum]. Missing lengths have no entry; looking one up returns null, so use getOrDefault(key, List.of()) when an empty list is useful.
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
class WordGroups {
public static void main(String[] args) {
List<String> words = List.of(
"fig", "kiwi", "pear", "plum", "apple", "mango", "banana");
Map<Integer, Long> counts = words.stream()
.collect(Collectors.groupingBy(
String::length, TreeMap::new, Collectors.counting()));
System.out.println(counts); // {3=1, 4=3, 5=2, 6=1}
}
}
The classifier chooses integer keys. TreeMap::new orders those keys. counting() replaces each group's list with its size as a Long. The result type is therefore Map<Integer, Long>, not a map of integers to integers.
Change what a group stores
With students Ada (A, 85), Ben (B, 72), Chen (A, 85), Dara (B, 90), Eli (A, 64), grouping by Student::getGroup gives these possibilities:
| Downstream collector | Value under A | Value type |
|---|---|---|
counting() | 3 | Long |
summingInt(Student::getPoints) | 234 | Integer |
averagingInt(Student::getPoints) | 78.0 | Double |
mapping(Student::getName, Collectors.toList()) | [Ada, Chen, Eli] | List<String> |
A downstream collector works inside each group. For A, the average is 234 divided by 3, or 78.0; B averages 81.0. maxBy and minBy store an Optional containing each group's extreme. Plain groupingBy gives no particular map type or key order; its map-factory form gives an explicit choice.
Split into two answers
Map<Boolean, List<String>> partition = students.stream()
.collect(Collectors.partitioningBy(
s -> s.getPoints() >= 80,
Collectors.mapping(Student::getName, Collectors.toList())));
The true list is [Ada, Chen, Dara], and the false list is [Ben, Eli]. Both Boolean keys are present, even if one side has no students. With counting() downstream, empty sides hold 0 as a Long. groupingBy with a Boolean classifier instead creates only keys that occur.
Process the map you built
A grouped result is an ordinary map. Loop over it with forEach((group, value) -> ...), or stream its entries. For averages A=78.0 and B=81.0, averages.entrySet().stream().max(Map.Entry.comparingByValue()) returns an Optional<Map.Entry<String, Double>> containing B=81.0. The optional wraps the winning entry; it is not the plain value 81.0. A separate step can map that entry to its key or value and handle an empty map.
Key Takeaways
- A classifier chooses group keys; plain grouping stores lists of elements.
- A downstream collector changes each group’s result shape.
countingproducesLong; averaging collectors produceDouble.- A partition always contains both Boolean keys.
- An absent grouping key returns
null, not an empty group.
5.3 Choose between streams and loops in a complete task
A complete data task often has two stages: summarize the input, then query the summary. Choose streams when these stages read naturally as standard operations, and loops when explicit control makes the work clearer.
Average readings, then select cities
Suppose a Reading has getCity() and getCelsius(). The readings are Oslo 4.5, Lima 19.0, Oslo 6.0, Cairo 27.5 and Lima 21.0, in degrees Celsius.
class Reading {
private final String city;
private final double celsius;
Reading(String city, double celsius) {
this.city = city;
this.celsius = celsius;
}
String getCity() { return city; }
double getCelsius() { return celsius; }
}
Create the input list before either implementation:
List<Reading> readings = List.of(
new Reading("Oslo", 4.5), new Reading("Lima", 19.0),
new Reading("Oslo", 6.0), new Reading("Cairo", 27.5),
new Reading("Lima", 21.0));
Map<String, Double> averages = readings.stream()
.collect(Collectors.groupingBy(
Reading::getCity,
TreeMap::new,
Collectors.averagingDouble(Reading::getCelsius)));
List<String> warmCities = averages.entrySet().stream()
.filter(e -> e.getValue() > 15)
.map(Map.Entry::getKey)
.toList();
The first result is {Cairo=27.5, Lima=20.0, Oslo=5.25}. Oslo's average is (4.5 + 6.0) / 2, or 5.25. Lima's is 20.0. The second result is [Cairo, Lima]; the TreeMap provides ascending city order. Filter the averages after grouping: filtering individual readings first answers a different question.
For the warmest single reading, use readings.stream().max(Comparator.comparingDouble(Reading::getCelsius)). It returns an optional holding the Cairo reading, not the city with the warmest average by definition. Those tasks can have different answers in other data.
Express the same calculation with loops
Map<String, List<Double>> byCity = new TreeMap<>();
for (Reading r : readings) {
byCity.computeIfAbsent(r.getCity(), k -> new ArrayList<>())
.add(r.getCelsius());
}
Map<String, Double> averages = new TreeMap<>();
for (Map.Entry<String, List<Double>> e : byCity.entrySet()) {
double sum = 0;
for (double value : e.getValue()) sum += value;
averages.put(e.getKey(), sum / e.getValue().size());
}
System.out.println(averages);
// {Cairo=27.5, Lima=20.0, Oslo=5.25}
These are alternative implementations of the same result; each group is nonempty because a reading created it. The stream makes the grouping and average explicit in one collector. The loops expose the intermediate lists and the sum.
Keep processing behavior predictable
A pipeline lambda should compute from its input without changing outside state. For points [72, 45, 90, 58, 81], filter(p -> p >= 60).toList() directly produces [72, 90, 81]. Adding to an outside list inside forEach hides that result-building job in an action.
Do not change an ArrayList source inside its pipeline: adding elements from a forEach can throw ConcurrentModificationException. First collect new elements separately, then add them after processing finishes. A final captured reference may permit mutation in Java's type rules; that does not make mutation suitable inside a stream.
Choose the clearer control flow
Use a loop when you need adjacent indexes, a complicated stopping condition or several coordinated updates. Use streams for a readable selection, transformation and collection. Stream syntax alone gives no speed guarantee. Name a long predicate or comparator, and split a long chain at a meaningful intermediate result. For debugging, inspect with peek or collect a stage to a list and print it; continue from that list's fresh stream instead of reusing a consumed pipeline.
Key Takeaways
- Group before filtering when the condition concerns group averages.
- A maximum individual reading and a maximum group average are different questions.
- A collector states the result-building job directly.
- Keep pipeline behavior free of outside updates and source mutation.
- Use loops when indexes or coordinated control make the task clearer.
- Inspect a materialized stage with a fresh stream for subsequent work.
Generics, Lambdas and Streams in Java examples
1Build a type-safe reusable API
17 steps
- 1IntroductionKeep the type relationship
A reusable container needs to remember what its callers may put in and what they can get back. A type parameter carries that relationship through a declaration.
You will build a box, move the type choice onto a method, then check what changes when a parameter uses a wildcard. At each step, ask what the compiler knows about a value before the program runs.
- 2ExplanationDeclare the box
Tnames the caller's chosen type inside this class:class Box<T> { private T value; Box(T value) { this.value = value; } T get() { return value; } void set(T value) { this.value = value; } }The field, constructor input, getter result and setter input all use the same
T. The declaration introduces a placeholder; no particular stored type has been chosen yet. - 3ExampleUse one concrete type
Create a box and read its value:
Box<String> b = new Box<>("tea"); String drink = b.get(); b.set("water");Stringsupplies the type argument. The getter returns a string without a cast, and the setter accepts strings. The diamond on the right asks Java to infer the arguments. It does not remove checking; omitting angle brackets altogether would create a raw use. - 4ChallengePredict the rejected write
The generic
Box<T>hasvoid set(T value). AfterBox<Integer> b = new Box<>(8);, willb.set("nine")compile? Explain the failure point.---reveal---
It fails at the write because this reference expects an
Integer, not aString. No later read is needed to discover the error. The literal 8 can be boxed intoInteger; type arguments themselves must still be reference types, soBox<int>is invalid. - 5ConnectionChoose a type for each call
A utility method can introduce its own type parameter:
static <T> T last(List<T> list) { return list.get(list.size() - 1); }The first
<T>declares the parameter, and the nextTnames the result type. Calling it on[4, 8, 15]returns theInteger15; calling it on["x", "y"]returns the string"y". The method requires a nonempty list, independently of its type checks. - 6TipWrite a witness after the dot
Usually Java infers the method argument from the supplied list. A call can also state it:
String word = Lists.<String>last(words);The type witness follows the dot and precedes
last. A static method inside a generic class still needs its own parameter:static <E> Box<E> of(E value). The class'sTbelongs to parameterized instance uses, while the static method belongs to the shared class. - 7ChallengeLink input and output
A utility method should return an element from a
List<E>without requiring a caller-written cast. An author writesstatic E first(List<E> xs). What declaration is missing?---reveal---
Add
<E>before the return type:static <E> E first(List<E> xs). This introduces a method-owned parameter and links its input elements to its result. WritingObjectas the result would lose that useful relationship instead of repairing it. - 8ExplanationMake comparison available
An unrestricted
Thas no knowncompareTooperation. A bound can supply that promise:<T extends Comparable<T>>A
largest(List<T>)method can then keep aT best, compare eachxwithbest, and replace it whenx.compareTo(best) > 0. For 3, 11 and 7, the best values are 3, then 11, then 11. The return remains the list's own element type. - 9Key InsightProtect an integer list
An
Integeris aNumber, but aList<Integer>is not aList<Number>.If that list assignment were allowed, code using the number-list reference could add a
Double. The original owner would then read it as anInteger. Generic invariance prevents the assignment so that both reads and writes keep their promises.ArrayList<Number>is accepted asList<Number>because its element argument still matches. - 10ExampleTransfer in the safe direction
An extends wildcard lets code read a known upper type; a super wildcard lets it insert a known lower type.
static <T> void append( List<? super T> dst, List<? extends T> src) { dst.addAll(src); }With a modifiable
List<Number>destination andList<Integer>source, Java can chooseTasInteger. Every source element is acceptable to the destination, without claiming both lists have identical element arguments. - 11ChallengeChoose the safe read
A parameter has type
List<? super Integer>. Can its first element be assigned directly to anIntegervariable? What type is guaranteed instead?---reveal---
Only
Objectis guaranteed. The actual list may be aList<Number>orList<Object>and contain existing values other than integers. Adding an integer is safe through this type, but the write permission does not describe every element already stored. - 12ExplanationSeparate checking from run-time classes
Erasure replaces type parameters by their bounds, using
Objectwhen no bound is stated. Java has already checked the generic uses and inserts needed casts at reads.A
Box<String>and aBox<Integer>share the same run-timeBoxclass. There is no separate integer-box class for a run-time test to identify. Generics preserve type relationships in source checking, which is why they remove many visible casts without specializing the class for each argument. - 13ExampleSupply the creation behavior
A type parameter cannot be instantiated with
new T(). Instead, ask the caller for a factory:static <T> T create(Supplier<T> factory) { return factory.get(); } List<String> xs = create(ArrayList::new);The reference chooses a list constructor through the target supplier type. Generic arrays such as
new T[10]are also rejected; a collection such asArrayList<T>usually serves the storage job. - 14TipFollow the raw-type warning
A raw
Listcan accept"text"and 42 with unchecked-call warnings. A later(String)cast fails when it reaches 42.Changing the declaration to
List<String>moves the problem to the invalid addition. An unchecked warning means the compiler has lost a guarantee; execution may still succeed for some data. Check the raw use or cast that caused the warning, because the eventual failing read may be far away. - 15ChallengeCheck the erased overload
One class declares
show(Set<String> xs)andshow(Set<Integer> xs). Why can Java not use the element arguments to choose an overload?---reveal---
Both parameter types erase to
Set, so the declarations have the same erased signature. Give them distinct names or one common operation. A run-time test ofObjectagainstList<String>has a related limit: testList<?>without asserting its element argument. - 16ChallengeReview a reusable transfer API
An API uses a raw source list, casts each element to
Number, and appends it to a number list. It should accept integers and doubles as sources. What source parameter removes that cast while preserving safe numeric reads?---reveal---
Use
List<? extends Number>. Each element can be read asNumber, and both integer and double lists fit.List<Number>would reject those narrower list arguments. The destination should be modifiable; its generic type alone cannot guarantee successful writes. - 17RecapRead an API from its promises
When checking a generic declaration, identify where each type parameter is introduced, which positions it connects and which operations its bound supplies.
For a transfer, mark the source as producer and the destination as consumer. For a creation step, look for supplied construction behavior. For a warning or rejected operation, ask whether generic information was bypassed or whether the operation would require an element type at run time.
2Turn behavior into an argument
14 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionStart with the behavior’s shape
- 2ExplanationFind the method to implement
- 3ExampleUse a block when a decision needs it
- 4ChallengeRepair the missing result
- 5ExplanationMatch four everyday jobs
- 6ExampleWatch composition order
- 7ConnectionLet collections run the rule
- 8ChallengeDistinguish removal from selection
- 9ExplanationCapture a value that stays assigned
- 10ExampleRead the receiver from the reference
- 11ChallengeCheck what final protects
- 12ExampleCount with a map update rule
- 13ChallengeCombine target typing and capture
- 14RecapTrace one call through the rule
3Build an order one decision at a time
12 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionName the order the task needs
- 2ExplanationRead a comparison result
- 3ExampleAvoid subtraction in numeric comparisons
- 4ExampleKeep equal scores in source order
- 5ChallengeSeparate natural and task-specific order
- 6ExplanationAdd a rule only for ties
- 7Key InsightReverse only the intended decisions
- 8TipHandle nulls at the matching level
- 9ChallengeRead the finished reversal
- 10ConnectionOrder the entries, not the map
- 11ChallengeMake tied map entries predictable
- 12RecapCheck the comparison tree
4Follow values through a stream
16 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionTrack values and types together
- 2ExplanationLocate the terminal result
- 3ExampleStop after the first match
- 4ChallengeRun another calculation
- 5ExampleSelect students before mapping names
- 6ConnectionMove into numeric operations
- 7ExampleFlatten before removing duplicates
- 8ChallengeRead the operation order
- 9ExampleCompute a sum and an average
- 10ExplanationKeep absence separate from a value
- 11ChallengeHandle a missing maximum
- 12Key InsightCheck universal tests on empty input
- 13ExampleChoose a neutral starting value
- 14TipWiden before the overflow happens
- 15ChallengeCombine flattening, selection and a result
- 16RecapCheck the result’s edge cases
5Build a useful data summary
15 steps
Save this lesson to follow every Walkthrough step by step.
- 1IntroductionWork backward from the desired result
- 2ExplanationMake later changes an explicit requirement
- 3ExampleResolve collisions with a merge rule
- 4ChallengeChoose one value per repeated key
- 5ConnectionJoin values as text
- 6ExplanationChoose a key, then a group result
- 7ExampleSummarize inside the student groups
- 8Key InsightKeep both sides of a partition
- 9ChallengePredict an empty side
- 10ExampleCompute city averages first
- 11ExampleQuery the summary you built
- 12TipExpose control flow when it helps
- 13ChallengeProtect the source during processing
- 14ChallengeFind the winning group safely
- 15RecapReview the whole data task
Generics, Lambdas and Streams in Java practice questions
Set 1: Use the foundations10 Practice Questions
Question 1 of 10
Box<T> has T get() and void set(T value). For Box<String> label = new Box<>("north");, what is the declared result type of label.get()?Show answerHide answer
String replaces the class parameter T in the getter’s declared result. The compiler therefore treats the call as returning a string, with no caller-written cast. Erasure does not make the source-level result Object; compile-time checking still uses the argument.Question 2 of 10
Show answerHide answer
E before its return type and repeats it for the input elements and result. That gives each call its own inferred type argument. Merely writing E in the parameter and return positions does not declare it, so the version without <E> fails in an ordinary utility class.Question 3 of 10
doubleValue() on each element. It must accept both List<Integer> and List<Double>. What parameter type fits?Show answerHide answer
Number, so every read can be treated as Number. List<Number> cannot accept List<Integer> because generic types are invariant. A super wildcard would allow broader element types but reads would have only type Object.Question 4 of 10
Function<Integer, Integer> must double its input using a block body. What lambda body fits?Show answerHide answer
return x * 2; supplies the integer result required by the function. An expression lambda could simply use x -> x * 2, but adding braces does not preserve that automatic expression result.Question 5 of 10
int limit = 4; List<String> notes = new ArrayList<>(); Predicate<String> p = s -> { notes.add(s); return s.length() > limit; }; limit++;. What repair preserves those requirements?Show answerHide answer
Question 6 of 10
compare(a, b) say?Show answerHide answer
Question 7 of 10
names.stream().filter(s -> s.length() > 2) and does nothing with the returned stream. When are the predicate calls made?Show answerHide answer
filter is an intermediate operation and records a lazy processing stage. No terminal operation starts traversal here, so the predicate is not called. Creating a stream does not eagerly copy or filter the source list, and an unrelated later list read does not execute this pipeline.Question 8 of 10
reduce(0, (a, b) -> a * b). What causes the wrong result?Show answerHide answer
Question 9 of 10
add. What terminal expression explicitly provides an ArrayList?Show answerHide answer
toCollection(ArrayList::new) requests a known modifiable list implementation. Stream.toList() is unmodifiable, and Collectors.toList() gives no modifiability guarantee even if a particular implementation happens to permit changes. A set collector also changes the result’s structure and duplicate behavior.Question 10 of 10
ArrayList<String> words calls forEach(w -> words.add(w + "!")). How should the program safely append one transformed copy of each original word?Show answerHide answer
ArrayList while its stream is reading it interferes with traversal and can throw ConcurrentModificationException. Another reference still points to the same source; it does not separate the read and write stages.Set 2: Follow types and results10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
Box<Integer> holds 12. The program assigns int next = box.get() + 3;. How can this compile and give 15?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
pick belongs to class Items and declares <T> T pick(List<T> xs). How is an explicit String type argument written at a call?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
T during erasure?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
int run(int x), a default reset() method and a static help() method. Can x -> x + 2 implement it?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
String and decides whether it ends with ".csv". What standard interface fits the condition expected by filter?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
comparing(Item::getGroup).thenComparingInt(Item::getScore).reversed(). What name order results?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
IntStream.rangeClosed(2, 9).skip(3).limit(2).boxed().toList()?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Stream.<Integer>empty(), what are the results of separate fresh-stream calls to anyMatch(n -> n > 0), allMatch(n -> n > 0) and noneMatch(n -> n > 0), in that order?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
toMap(Item::getCode, Item::getCount) sees two items with code "R", both with count 3. What happens?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
partitioningBy(x -> x > 10, Collectors.counting()). What result is guaranteed?Show answerHide answer
Save this lesson to see this answer.
Set 3: Choose a safe implementation10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
List<? super Integer> sink. What is the most specific type guaranteed for sink.get(0)?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Pair<String, Integer> p = new Pair<>("zone", 4);, what are the type of p.getFirst() and the value of p.getSecond() + 2?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
show(Set<Long> xs) and show(Set<String> xs). Why does compilation fail?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Badge has a constructor Badge(String text). A method accepts Function<String, Badge> factory. What reference creates the same behavior as s -> new Badge(s)?Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
["z", null, "a"]. It is sorted with Comparator.nullsLast(Comparator.reverseOrder()). What order results?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
s is used for s.count() and then s.toList(). How should the program obtain both results from a reusable list data?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Stream.of("mint", "ash", "mint", "elm").distinct().map(String::length).sorted().toList()?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Set 4: Trace the complete task10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
Slot<T> needs a static factory that creates a typed slot from any value. What declaration avoids using the class parameter from a static context?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
addAll. It must copy integers into either a number list or an object list and preserve type safety. What signature fits?Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
boolean check(String s) and boolean equals(Object other), with no other abstract methods. Can a lambda implement it, and why?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
String::compareToIgnoreCase when used as a Comparator<String>?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
(int) (a - b). What happens when comparing 3.6 with 3.1?Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
List.of(List.of("red"), List.of("blue", "gold")).stream().map(List::stream)?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
"reed", "oak", "fern", "elm", "cedar". After grouping by length with counting() into a TreeMap, what map prints?Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
Show answerHide answer
Save this lesson to see this answer.
Set 5: Transfer the ideas to new tasks10 Practice Questions
Save this lesson to see every answer and practice every set.
Question 1 of 10
List<? super Integer> dst, List<? extends Number> src and tries dst.add(src.get(0)). It must accept integer or double sources and modifiable number or object destinations. How should its parameters change?Show answerHide answer
Save this lesson to see this answer.
Question 2 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 3 of 10
Function<Integer, Integer> triple = x -> x * 3; and shift = x -> x + 2;, what does triple.andThen(shift).apply(4) return?Show answerHide answer
Save this lesson to see this answer.
Question 4 of 10
HashMap<String, Integer> holds oak=6, ash=6, elm=2. What entry comparator guarantees descending values and ascending keys on ties?Show answerHide answer
Save this lesson to see this answer.
Question 5 of 10
Show answerHide answer
Save this lesson to see this answer.
Question 6 of 10
Stream.of("ash", "cedar", "elm", "pine").filter(w -> w.length() > 3).map(String::length).findFirst()Show answerHide answer
Save this lesson to see this answer.
Question 7 of 10
[2, 5], [] and [5, 8]. What does groups.stream().flatMap(List::stream).distinct().filter(n -> n > 3).mapToInt(n -> n).sum() return?Show answerHide answer
Save this lesson to see this answer.
Question 8 of 10
values.stream().filter(n -> n > -8).max(Integer::compare).map(n -> n * 2).orElse(0) return?Show answerHide answer
Save this lesson to see this answer.
Question 9 of 10
Sale has a team and integer units. Ignore sales with zero or negative units, compute each team’s mean from its remaining sales, and return the highest-mean team or "none" on empty input. Assume no tied means. What pipeline fits? Collector methods are statically imported.Show answerHide answer
Save this lesson to see this answer.
Question 10 of 10
"an", "oak", "to", "elm", what is the true-side value from partitioningBy(w -> w.length() == 2, mapping(String::toUpperCase, toList()))?Show answerHide answer
Save this lesson to see this answer.
Stuck on your own question? Homework Help
Key Terms
A
Associativity
Autoboxing
5 be passed to a constructor expecting an Integer.B
BinaryOperator
BinaryOperator<Integer> such as Integer::sum can merge counts for a repeated map key.Block body
return a; and return b; to implement a maximum operation.Bound method reference
System.out::println prints using that particular output object.C
Classifier
String::length puts all four-letter words under the same integer key.Collection factory
ArrayList::new explicitly requests a list that permits later additions.Collector
forEach.Comparator
compare method returns a negative, zero or positive result for two input values.Comparator<String> can order words by length without changing their natural string order.Comparator reversal
reversed() call, including existing tie-breakers.Constructor reference
ClassName::new to supply object creation behavior, with arguments determined by the target interface.Student::new can turn a name into a student with a matching constructor.Consumer
accept method returns no value.Consumer<String> can print each word when passed to a collection's forEach.D
Diamond operator
<> in a generic constructor call, asking the compiler to infer type arguments while keeping type checks.new ArrayList<>() keeps the list typed from its context.distinct
distinct operation removes duplicate elements from a stream using object equality, retaining encounter order for an ordered stream.distinct before mapping words to lengths can still leave repeated lengths in the result.Downstream collector
counting() changes a map of group lists into a map of Long counts.Duplicate key
toMap fail even when both associated values are 85.E
Effectively final
final keyword.Encounter order
findFirst choose the first matching name in a source list.Erased signature
show(Set) is shared by overload attempts using Set<String> and Set<Long>.Expression body
x + 1 lets x -> x + 1 return the incremented integer directly.F
filter
filter operation keeps stream elements whose predicate is true, preserving their element type while dropping other values.filter must run before mapping students to names if it needs the points getter.flatMap
flatMap operation converts each input to a stream and combines those streams' elements into one output sequence.flatMap using List::stream turns nested integer lists into one stream of integers.Function
T into a result of type R through apply.Function<String, Integer> can transform each word into its length before grouping.Function composition
andThen and compose specify opposite call orders.twice.andThen(inc), giving 11 from 5.Functional interface
Object.Operation lets a caller supply its calculation with a lambda.G
Generic class
Box<T> supports a string box and an integer box with one declaration.Generic interface
Container<T> can keep the input to put linked to the result of take.Generic method
last returns a string for a string list and an integer for an integer list.groupingBy
groupingBy collector builds a map from encountered classifier keys to lists or downstream summaries of their matching elements.groupingBy collector with averagingInt stores one double-valued average per student group.I
Identity
Indexed loop
list.get(i) with list.get(i - 1) while updating two counters.Integer overflow
Intermediate operation
filter waits for a terminal operation before testing elements.Intermediate result
IntStream
int values with numeric operations such as sum and average, plus conversions back to objects.IntStream from mapToInt can sum points without first collecting wrapper values.Invariance
List<Integer> as List<Number>, protecting it from double-valued writes.J
joining
joining collector combines character sequences into one string, with optional text between elements and around the whole result.joining collector with a comma delimiter produces no trailing comma after the final name.K
Key extractor
Student::getPoints builds a numeric student comparator with comparingInt.L
Lambda expression
-> between them.(a, b) -> a * b supplies the multiplication rule for an operation.Lazy evaluation
findFirst stop once a passing word has been found and transformed.limit
limit operation retains at most the requested number of elements from the start of an ordered stream, allowing later input to be skipped.limit before sorting selects from the original beginning, not the highest values overall.Lower-bounded wildcard
? super Integer names an unknown type that is the stated type or one of its supertypes.List<Number> destination.M
map
map operation replaces each input element with one computed result, so it can change the stream's element type.map using String::length changes words into integer wrapper values, not a primitive stream.Map entry
mapping
mapping collector transforms elements before passing their results to another collector, often within a group or partition.mapping collector can extract student names inside each group before collecting a list.Matching operation
anyMatch but true for allMatch.Merge function
Math::max keeps the greatest quantity when a stock key occurs twice.Method reference
::, fitting the inputs and result of a functional-interface target.Integer::parseInt supplies parsing behavior to a string-to-integer function.Multiple bounds
&, so a type argument must satisfy each bound, with a class listed first if present.<T extends Number & Comparable<T>> require numeric and comparison behavior.N
Natural order
Comparable.compareTo, used when no separate comparator is supplied."Cherry" before lowercase "apple".Non-interference
ArrayList.Null-aware comparator
nullsFirst or nullsLast.O
Optional
Optional from a filtered maximum can be empty, so a fallback handles the missing case.OptionalDouble
OptionalDouble from average() can supply a chosen 0.0 fallback through orElse.P
partitioningBy
partitioningBy collector divides elements by a predicate and always returns both Boolean keys, even if one side is empty.partitioningBy collector can return an empty true list when no student reaches the threshold its predicate tests.peek
peek operation passes elements on unchanged while running an inspection action for traversed values; its calls may be skipped.peek used to print values is unreliable for required work because execution may omit its stage.Pipeline
stream through filter to count produces a long-valued number of matches.Predicate
boolean through test.Predicate<String> passed to removeIf identifies matching words that should be deleted.Primitive specialization
ToIntFunction<String> supplies lengths directly to mapToInt.Producer extends, consumer super
R
Raw type
List xs can allow a bad write that a List<String> would reject.Reduction
S
Short-circuiting
anyMatch return true as soon as one element satisfies its predicate.Side effect
skip
skip operation discards the requested number of initial elements from an ordered stream and passes the remaining elements on.skip(2) on the inclusive range 1 through 10, the next emitted value is 3.Stable sort
Static context
<E> instead of using the class's T.Stream
points can count the values that pass a test while leaving the list unchanged.Supplier
get method provides a value of its result type.Supplier<List<String>> using ArrayList::new can provide a fresh list on each call.T
Target type
Predicate<String> makes s -> s.isEmpty() a Boolean test of a string.Terminal operation
count returns a long and leaves the same stream unavailable for reuse.Tie-breaker
Transitivity
Type argument
String makes a Box<String> getter return text without a cast.Type erasure
Object, after checking, with necessary casts inserted by the compiler.Box class.Type inference
last(words) produce a String when words is a list of strings.Type parameter
T gives a box's field and getter the same caller-chosen type.Type witness
Lists.<String>last(words) states the method's type choice explicitly.U
UnaryOperator
UnaryOperator<String> can uppercase every word through a modifiable list's replaceAll.Unbound method reference
String::length calls length() on each supplied string.Unbounded wildcard
? without a bound, representing an unknown argument while permitting element reads only as Object.List<?> lets a display method inspect list size without knowing its element type.Unchecked warning
add points to a potential wrong-type read later.Unmodifiable list
Stream.toList() throws when code tries to add another element.Upper bound
<T extends Number> allows calls to doubleValue() on a T.Upper-bounded wildcard
? extends Number names an unknown type that is the stated type or one of its subtypes.V
Variable capture
limit value.W
Wrapper class
Integer makes List<Integer> valid, while List<int> is rejected.About this lesson
Make your own Study Materials
Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.