Generics, Lambdas and Streams in Java

Write Java classes and methods that work with any type while the compiler checks every use, then pass behavior to methods as a value with lambda expressions and method references. Sort objects by any rule with comparators, and process collections with the Stream API: filter, transform, sort, count, sum, group and collect data in a few readable steps. Every example is a small, complete piece of code with its exact result, and the common compiler errors and run-time exceptions are explained along the way.

IntermediateLesson 11 of 11 in Object-Oriented Programming with Java Course

What you will learn

  1. Use type parameters to write classes, interfaces and methods that work with many types, while the compiler checks every use.
    • Define and use generic classes
      Declare classes and interfaces with type parameters, create objects with type arguments and explain the type safety this gives compared with storing Object.
    • Write generic methods
      Declare 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 wildcards
      Use bounded type parameters to call methods on a generic type, explain why a List<Integer> is not a List<Number> and choose between ? extends and ? super.
    • Work within the limits of type erasure
      Explain how the compiler erases type arguments, which operations generics therefore do not allow and why raw types should be avoided.
  2. 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 interfaces
      Recognize 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 interfaces
      Choose 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 references
      Use variables from the surrounding code inside a lambda, apply the effectively final rule and replace simple lambdas with the four kinds of method reference.
  3. 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 it
      Implement the compare contract correctly, sort lists and arrays with a comparator and decide between Comparable and Comparator.
    • Combine comparators with comparing, thenComparing and reversed
      Build comparators from sort keys, add tie-breakers, reverse orders, handle null values and sort the entries of a map.
  4. 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 runs
      Create 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 elements
      Use filter, map, mapToInt, flatMap, distinct, sorted, limit and skip, and move between object streams and primitive streams.
    • Reduce a stream to a single result
      Compute counts, sums, averages, extremes and true-or-false answers with terminal operations, and handle missing results with Optional.
  5. 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 strings
      Use toList, Collectors.toList, toSet, toCollection, joining and toMap, and handle unmodifiable results and duplicate keys.
    • Group and partition data with collectors
      Split data into groups with groupingBy and partitioningBy and summarize each group with a downstream collector.
    • Choose between streams and loops in a complete task
      Solve a data-processing task from start to finish, compare a stream solution with a loop and keep pipelines free of side effects.

Generics, Lambdas and Streams in Java study guide

5 chapters

1

Write 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 Integer as 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.

ParameterSafe 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 T to read elements as T.
  • Use ? super T to add values of type T; reads have type Object.
  • 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.copy replaces 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 Object when unbounded.
  • Parameterized uses share one run-time class.
  • Use a supplied factory instead of new T().
  • Erased parameter types cannot distinguish generic overloads.
2

Pass 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.
  • @FunctionalInterface checks 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
InterfaceFunctional methodJob
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.
  • andThen runs the receiver first; compose runs its argument first.
  • removeIf tests a predicate; replaceAll applies a unary operator.
  • merge combines an existing map value with a supplied value.
  • computeIfAbsent can 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.

KindReferenceEquivalent lambda
Static methodInteger::parseInts -> Integer.parseInt(s)
Bound instanceSystem.out::printlnx -> System.out.println(x)
Unbound instanceString::lengths -> s.length()
ConstructorArrayList::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.
3

Order 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.
  • Comparable supplies 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

  • comparing extracts a key; primitive builders avoid boxing numeric keys.
  • thenComparing is 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.
4

Process 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.
  • sorted must 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

  • filter keeps the element type; map can change it.
  • Primitive streams provide numeric operations absent from Stream<Integer>.
  • The order of skip, limit and sorting changes the selected elements.
  • distinct follows equality; ordered-stream sorting preserves ties.
  • flatMap flattens streams produced from the input elements.
  • peek is 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 allMatch and noneMatch are true; empty anyMatch is false.
  • Reduction needs an identity and an associative accumulator.
  • Widen to long before summing values that exceed the int total range.
5

Collect 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 toCollection when a specific collection type is required.
  • joining gathers text with an optional separator, prefix and suffix.
  • Two-argument toMap rejects 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 collectorValue under AValue type
counting()3Long
summingInt(Student::getPoints)234Integer
averagingInt(Student::getPoints)78.0Double
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.
  • counting produces Long; averaging collectors produce Double.
  • 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

5 Walkthroughs

1

Build a type-safe reusable API

17 steps
Follow a typed box, a generic method and a list transfer to see what Java can check before execution.
  1. 1
    IntroductionKeep 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.

  2. 2
    ExplanationDeclare the box

    T names 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.

  3. 3
    ExampleUse one concrete type

    Create a box and read its value:

    Box<String> b = new Box<>("tea");
    String drink = b.get();
    b.set("water");
    

    String supplies 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.

  4. 4
    ChallengePredict the rejected write

    The generic Box<T> has void set(T value). After Box<Integer> b = new Box<>(8);, will b.set("nine") compile? Explain the failure point.

    ---reveal---

    It fails at the write because this reference expects an Integer, not a String. No later read is needed to discover the error. The literal 8 can be boxed into Integer; type arguments themselves must still be reference types, so Box<int> is invalid.

  5. 5
    ConnectionChoose 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 next T names the result type. Calling it on [4, 8, 15] returns the Integer 15; calling it on ["x", "y"] returns the string "y". The method requires a nonempty list, independently of its type checks.

  6. 6
    TipWrite 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's T belongs to parameterized instance uses, while the static method belongs to the shared class.

  7. 7
    ChallengeLink input and output

    A utility method should return an element from a List<E> without requiring a caller-written cast. An author writes static 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. Writing Object as the result would lose that useful relationship instead of repairing it.

  8. 8
    ExplanationMake comparison available

    An unrestricted T has no known compareTo operation. A bound can supply that promise:

    <T extends Comparable<T>>
    

    A largest(List<T>) method can then keep a T best, compare each x with best, and replace it when x.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.

  9. 9
    Key InsightProtect an integer list

    An Integer is a Number, but a List<Integer> is not a List<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 an Integer. Generic invariance prevents the assignment so that both reads and writes keep their promises. ArrayList<Number> is accepted as List<Number> because its element argument still matches.

  10. 10
    ExampleTransfer 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 and List<Integer> source, Java can choose T as Integer. Every source element is acceptable to the destination, without claiming both lists have identical element arguments.

  11. 11
    ChallengeChoose the safe read

    A parameter has type List<? super Integer>. Can its first element be assigned directly to an Integer variable? What type is guaranteed instead?

    ---reveal---

    Only Object is guaranteed. The actual list may be a List<Number> or List<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.

  12. 12
    ExplanationSeparate checking from run-time classes

    Erasure replaces type parameters by their bounds, using Object when no bound is stated. Java has already checked the generic uses and inserts needed casts at reads.

    A Box<String> and a Box<Integer> share the same run-time Box class. 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.

  13. 13
    ExampleSupply 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 as ArrayList<T> usually serves the storage job.

  14. 14
    TipFollow the raw-type warning

    A raw List can 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.

  15. 15
    ChallengeCheck the erased overload

    One class declares show(Set<String> xs) and show(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 of Object against List<String> has a related limit: test List<?> without asserting its element argument.

  16. 16
    ChallengeReview 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 as Number, 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.

  17. 17
    RecapRead 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.

2

Turn behavior into an argument

14 steps
Build and combine functional rules, then read captured variables and method references correctly.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionStart with the behavior’s shape
  2. 2
    ExplanationFind the method to implement
  3. 3
    ExampleUse a block when a decision needs it
  4. 4
    ChallengeRepair the missing result
  5. 5
    ExplanationMatch four everyday jobs
  6. 6
    ExampleWatch composition order
  7. 7
    ConnectionLet collections run the rule
  8. 8
    ChallengeDistinguish removal from selection
  9. 9
    ExplanationCapture a value that stays assigned
  10. 10
    ExampleRead the receiver from the reference
  11. 11
    ChallengeCheck what final protects
  12. 12
    ExampleCount with a map update rule
  13. 13
    ChallengeCombine target typing and capture
  14. 14
    RecapTrace one call through the rule
3

Build an order one decision at a time

12 steps
Trace comparison signs, ties and reversals before using a comparator on lists or map entries.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionName the order the task needs
  2. 2
    ExplanationRead a comparison result
  3. 3
    ExampleAvoid subtraction in numeric comparisons
  4. 4
    ExampleKeep equal scores in source order
  5. 5
    ChallengeSeparate natural and task-specific order
  6. 6
    ExplanationAdd a rule only for ties
  7. 7
    Key InsightReverse only the intended decisions
  8. 8
    TipHandle nulls at the matching level
  9. 9
    ChallengeRead the finished reversal
  10. 10
    ConnectionOrder the entries, not the map
  11. 11
    ChallengeMake tied map entries predictable
  12. 12
    RecapCheck the comparison tree
4

Follow values through a stream

16 steps
Trace a pipeline’s values and types, then choose a terminal result and handle missing values.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionTrack values and types together
  2. 2
    ExplanationLocate the terminal result
  3. 3
    ExampleStop after the first match
  4. 4
    ChallengeRun another calculation
  5. 5
    ExampleSelect students before mapping names
  6. 6
    ConnectionMove into numeric operations
  7. 7
    ExampleFlatten before removing duplicates
  8. 8
    ChallengeRead the operation order
  9. 9
    ExampleCompute a sum and an average
  10. 10
    ExplanationKeep absence separate from a value
  11. 11
    ChallengeHandle a missing maximum
  12. 12
    Key InsightCheck universal tests on empty input
  13. 13
    ExampleChoose a neutral starting value
  14. 14
    TipWiden before the overflow happens
  15. 15
    ChallengeCombine flattening, selection and a result
  16. 16
    RecapCheck the result’s edge cases
5

Build a useful data summary

15 steps
Choose result containers, resolve duplicate keys and query per-group summaries without hidden updates.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionWork backward from the desired result
  2. 2
    ExplanationMake later changes an explicit requirement
  3. 3
    ExampleResolve collisions with a merge rule
  4. 4
    ChallengeChoose one value per repeated key
  5. 5
    ConnectionJoin values as text
  6. 6
    ExplanationChoose a key, then a group result
  7. 7
    ExampleSummarize inside the student groups
  8. 8
    Key InsightKeep both sides of a partition
  9. 9
    ChallengePredict an empty side
  10. 10
    ExampleCompute city averages first
  11. 11
    ExampleQuery the summary you built
  12. 12
    TipExpose control flow when it helps
  13. 13
    ChallengeProtect the source during processing
  14. 14
    ChallengeFind the winning group safely
  15. 15
    RecapReview the whole data task

Generics, Lambdas and Streams in Java practice questions

50 Practice Questions in 5 sets

Set 1: Use the foundations10 Practice Questions

Question 1 of 10

A generic 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()?
  1. Object
  2. String
  3. Box<String>
  4. CharSequence
Show answerHide answer
Answer: B
The type argument 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

An ordinary utility class needs a static method that returns the first element of any nonempty list, preserving its element type. What declaration fits?
  1. static E first(List<E> xs)
  2. static <E> Object first(List<Object> xs)
  3. static <E> List<E> first(E xs)
  4. static <E> E first(List<E> xs)
Show answerHide answer
Answer: D
The method introduces 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

A method reads a numeric list only to call doubleValue() on each element. It must accept both List<Integer> and List<Double>. What parameter type fits?
  1. List<? extends Number>
  2. List<Number>
  3. List<? super Number>
  4. List<Object>
Show answerHide answer
Answer: A
An extends wildcard accepts an unknown subtype of 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

A Function<Integer, Integer> must double its input using a block body. What lambda body fits?
  1. x -> { x * 2; }
  2. x -> { return; }
  3. x -> { return x * 2; }
  4. () -> { return x * 2; }
Show answerHide answer
Answer: C
A value-returning block must explicitly return its value. 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

A predicate must log each tested string and use the initial threshold, while a separate counter named limit can keep changing. The code captures both locals: 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?
  1. Make the list reference final; continue capturing the changing limit.
  2. Move the increment before the lambda; still capture the same limit.
  3. Capture a separate fixed threshold; keep the same list reference.
  4. Replace the list with an unmodifiable list; still capture changing limit.
Show answerHide answer
Answer: C
The increment prevents capture of limit, so copy its initial value into a separate effectively final local and capture that threshold. The unchanged notes reference may still refer to a list that receives log entries. Moving the increment earlier would not restore effective finality after reassignment.

Question 6 of 10

For ascending order, what does a negative result from compare(a, b) say?
  1. a belongs before b.
  2. a belongs after b.
  3. a and b are equal objects.
  4. a is smaller by exactly one position.
Show answerHide answer
Answer: A
The comparison sign determines relative position: a negative result places the first argument before the second. Zero denotes an ordering tie, while a positive result puts the first argument after the second. The numeric size of the negative result does not represent a distance or a number of positions.

Question 7 of 10

A program constructs names.stream().filter(s -> s.length() > 2) and does nothing with the returned stream. When are the predicate calls made?
  1. They are all made during the call to stream().
  2. They are all made during the call to filter().
  3. They are made when the source list is next read.
  4. They are not made without a terminal operation.
Show answerHide answer
Answer: D
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

A nonempty stream contains 2, 4 and 6. A product is computed with reduce(0, (a, b) -> a * b). What causes the wrong result?
  1. The accumulator must take one argument instead of two.
  2. Zero is not the multiplication identity and keeps the accumulator at zero.
  3. An integer stream cannot be reduced with multiplication.
  4. The first input value must always serve as an explicit identity.
Show answerHide answer
Answer: B
Multiplying the initial zero by 2 gives zero, and every later multiplication stays zero. Use 1 as the identity so the accumulator preserves the first value and then builds the product 48. An identity is chosen for the operation, not taken arbitrarily from the input values.

Question 9 of 10

A result list must support a later add. What terminal expression explicitly provides an ArrayList?
  1. toList()
  2. collect(Collectors.toList())
  3. collect(Collectors.toCollection(ArrayList::new))
  4. collect(Collectors.toSet())
Show answerHide answer
Answer: C
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

A pipeline on an ArrayList<String> words calls forEach(w -> words.add(w + "!")). How should the program safely append one transformed copy of each original word?
  1. Collect the copies separately, then call words.addAll(copies).
  2. Add peek(w -> words.add(w + "!")) before forEach.
  3. Replace forEach with map while keeping words.add inside.
  4. Wrap words in another variable and add through that reference.
Show answerHide answer
Answer: A
Collecting copies first lets traversal finish before the original list is changed. Adding to an 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

A Box<Integer> holds 12. The program assigns int next = box.get() + 3;. How can this compile and give 15?
  1. The getter returns int, because generic arguments are primitives.
  2. The getter returns Object, which arithmetic casts automatically.
  3. The getter returns Integer, which arithmetic unboxes.
  4. The getter returns Double, which arithmetic narrows automatically.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A static method pick belongs to class Items and declares <T> T pick(List<T> xs). How is an explicit String type argument written at a call?
  1. Items.<String>pick(words)
  2. Items.pick<String>(words)
  3. <String>Items.pick(words)
  4. Items.String.pick(words)
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

Inside an unbounded generic method, what replaces T during erasure?
  1. String
  2. Object
  3. Class<T>
  4. Number
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

An interface declares int run(int x), a default reset() method and a static help() method. Can x -> x + 2 implement it?
  1. No; three declared methods require three lambda bodies.
  2. Yes; a lambda supplies default and static bodies as well.
  3. No; a functional method must be named apply.
  4. Yes; only run needs an abstract implementation.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A reusable rule takes a String and decides whether it ends with ".csv". What standard interface fits the condition expected by filter?
  1. Predicate<String>
  2. Consumer<String>
  3. Supplier<Boolean>
  4. Function<String, Boolean>
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Items are Mira (group A, 7), Niko (B, 9), Oren (A, 4), Pia (B, 6). They are sorted by comparing(Item::getGroup).thenComparingInt(Item::getScore).reversed(). What name order results?
  1. [Pia, Niko, Oren, Mira]
  2. [Niko, Pia, Mira, Oren]
  3. [Mira, Oren, Niko, Pia]
  4. [Oren, Mira, Pia, Niko]
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

What list results from IntStream.rangeClosed(2, 9).skip(3).limit(2).boxed().toList()?
  1. [2, 3]
  2. [4, 5]
  3. [5, 6]
  4. [7, 8]
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

For 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?
  1. false, false, true
  2. true, true, false
  3. false, false, false
  4. false, true, true
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A two-argument toMap(Item::getCode, Item::getCount) sees two items with code "R", both with count 3. What happens?
  1. It stores R=3 because the repeated values are equal.
  2. It stores R=6 by adding equal values automatically.
  3. It stores both entries under two copies of key R.
  4. It throws IllegalStateException for the repeated key.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

An empty stream is collected with partitioningBy(x -> x > 10, Collectors.counting()). What result is guaranteed?
  1. The map is empty because no classifier ran.
  2. Both Boolean keys map to 0L.
  3. Only false is present and maps to 0L.
  4. Both Boolean keys map to empty optionals.
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

A parameter is declared List<? super Integer> sink. What is the most specific type guaranteed for sink.get(0)?
  1. Integer
  2. Number
  3. Object
  4. Comparable<Integer>
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A generic Pair<L, R> has getters returning L and R. For Pair<String, Integer> p = new Pair<>("zone", 4);, what are the type of p.getFirst() and the value of p.getSecond() + 2?
  1. String and 6
  2. Object and 6
  3. String and 42
  4. Integer and 6
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A class declares both show(Set<Long> xs) and show(Set<String> xs). Why does compilation fail?
  1. Every generic method must return its element type.
  2. Set permits only one type argument in an entire class.
  3. Java overloads cannot use collection parameters at all.
  4. Both parameter types erase to Set.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A method must run a supplied action on each string without receiving a result from that action. What parameter interface fits?
  1. Supplier<String>
  2. Consumer<String>
  3. Predicate<String>
  4. UnaryOperator<String>
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A class 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)?
  1. Badge::toString
  2. Badge::new
  3. new Badge::new
  4. Badge::Badge
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A list is ["z", null, "a"]. It is sorted with Comparator.nullsLast(Comparator.reverseOrder()). What order results?
  1. [null, z, a]
  2. [a, z, null]
  3. [null, a, z]
  4. [z, a, null]
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A stream variable s is used for s.count() and then s.toList(). How should the program obtain both results from a reusable list data?
  1. Start a fresh data.stream() for each terminal operation.
  2. Call s.close() between the terminal operations.
  3. Store s in another variable before calling count().
  4. Insert s.limit(data.size()) after calling count().
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

What list results from Stream.of("mint", "ash", "mint", "elm").distinct().map(String::length).sorted().toList()?
  1. [3, 4]
  2. [3, 3, 4, 4]
  3. [3, 3, 4]
  4. [4, 3, 3]
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

Items have codes and quantities P=2, Q=6, P=5, Q=1. The program must return a modifiable list of distinct codes in ascending order. What pipeline fits?
  1. items.stream().map(Item::getCode).distinct().sorted().toList()
  2. items.stream().map(Item::getCode).distinct().sorted(Comparator.reverseOrder()).collect(Collectors.toCollection(ArrayList::new))
  3. items.stream().map(Item::getCode).distinct().sorted().collect(Collectors.toCollection(ArrayList::new))
  4. items.stream().map(Item::getCode).sorted().collect(Collectors.toCollection(ArrayList::new))
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A task compares every list element with the one immediately before it and updates two counters under several conditions. What makes an indexed loop a reasonable choice?
  1. It exposes neighboring indexes and coordinated counter updates.
  2. It is required because streams cannot inspect neighboring list positions.
  3. It guarantees faster processing than a stream over the same list.
  4. It permits source additions inside an ordinary for-each traversal.
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

A generic class 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?
  1. static Slot<T> of(T value)
  2. static <E> Slot<T> of(E value)
  3. static Slot<Object> of(T value)
  4. static <E> Slot<E> of(E value)
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A method transfers any chosen element type from a source list into a modifiable destination list using addAll. It must copy integers into either a number list or an object list and preserve type safety. What signature fits?
  1. <T> void append(List<? extends T> dst, List<? super T> src)
  2. <T> void append(List<T> dst, List<T> src)
  3. <T> void append(List<? super T> dst, List<? extends T> src)
  4. <T> void append(List<Object> dst, List<T> src)
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

An interface declares boolean check(String s) and boolean equals(Object other), with no other abstract methods. Can a lambda implement it, and why?
  1. Yes; the Object-method match does not add abstract implementation work.
  2. No; every declared abstract method counts, so the interface has two.
  3. Yes; Java always supplies one missing method for any two-method interface.
  4. No; a Boolean-returning functional method must be named test.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

What lambda has the same behavior as the unbound reference String::compareToIgnoreCase when used as a Comparator<String>?
  1. (a, b) -> b.compareToIgnoreCase(a)
  2. (a, b) -> a.compareToIgnoreCase(b)
  3. a -> a.compareToIgnoreCase(a)
  4. (a, b) -> String.compareToIgnoreCase(a, b)
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A modifiable list contains X with score 8, Y with score 3, Z with score 8, W with score 3, in that order. It is sorted only by ascending score with a valid comparator. What name order is guaranteed?
  1. [Y, W, X, Z]
  2. [W, Y, Z, X]
  3. [Y, W, Z, X]
  4. [X, Z, Y, W]
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

A comparator for double-valued measurements uses (int) (a - b). What happens when comparing 3.6 with 3.1?
  1. It correctly reports that the first value is larger.
  2. It correctly reports that the first value is smaller.
  3. It reports a tie even though the first value is larger.
  4. It throws an exception for a fractional result.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

What is the result type of List.of(List.of("red"), List.of("blue", "gold")).stream().map(List::stream)?
  1. Stream<String>
  2. Stream<Stream<String>>
  3. Stream<List<String>>
  4. List<Stream<String>>
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A method returns the largest integer in an arbitrary list, or -1 when the list is empty. What expression meets that requirement?
  1. xs.stream().reduce(-1, Integer::max)
  2. xs.stream().max(Integer::compare).get()
  3. xs.stream().findFirst().orElse(-1)
  4. xs.stream().max(Integer::compare).orElse(-1)
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

Words are "reed", "oak", "fern", "elm", "cedar". After grouping by length with counting() into a TreeMap, what map prints?
  1. {3=1, 4=2, 5=2}
  2. {3=2, 4=1, 5=1}
  3. {3=2, 4=2, 5=1}
  4. {2=3, 1=5}
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Readings are Delta 6 °C, Delta 18 °C, Cove 14 °C and Cove 16 °C. The task asks for cities whose average exceeds 13 °C. What result is correct?
  1. Only Cove qualifies.
  2. Cove and Delta qualify.
  3. Only Delta qualifies.
  4. No city qualifies.
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

A method has parameters 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?
  1. List<? super Integer> dst, List<? extends Integer> src
  2. List<? super Number> dst, List<? extends Number> src
  3. List<Number> dst, List<? extends Number> src
  4. List<? extends Number> dst, List<? super Number> src
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A generic factory must create a fresh value of its caller’s chosen reference type, without assuming a no-argument constructor exists. What design fits?
  1. Use new T() inside a method declaring <T>.
  2. Use new Object() and return it directly as T.
  3. Accept Supplier<T> and call its get() method.
  4. Use T.class to find the caller’s constructor.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

For Function<Integer, Integer> triple = x -> x * 3; and shift = x -> x + 2;, what does triple.andThen(shift).apply(4) return?
  1. 18
  2. 12
  3. 20
  4. 14
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A HashMap<String, Integer> holds oak=6, ash=6, elm=2. What entry comparator guarantees descending values and ascending keys on ties?
  1. Map.Entry.<String, Integer>comparingByValue().reversed().thenComparing(Map.Entry.comparingByKey())
  2. Map.Entry.<String, Integer>comparingByValue().thenComparing(Map.Entry.comparingByKey()).reversed()
  3. Map.Entry.<String, Integer>comparingByKey().thenComparing(Map.Entry.comparingByValue())
  4. Map.Entry.<String, Integer>comparingByValue().reversed()
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

Tickets are Ivo (2, 5), June (1, 7), Kai (1, 4), Luz (2, 5), where each pair is group and score. What order results from ascending group, descending score, then ascending name?
  1. [Kai, June, Ivo, Luz]
  2. [Ivo, Luz, June, Kai]
  3. [June, Kai, Ivo, Luz]
  4. [June, Kai, Luz, Ivo]
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

What does this produce, and which words are tested by the filter? Stream.of("ash", "cedar", "elm", "pine").filter(w -> w.length() > 3).map(String::length).findFirst()
  1. Optional[5]; ash and cedar are tested.
  2. Optional[4]; all four words are tested.
  3. Optional[5]; all four words are tested.
  4. Optional[3]; only ash is tested.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A list has nested groups [2, 5], [] and [5, 8]. What does groups.stream().flatMap(List::stream).distinct().filter(n -> n > 3).mapToInt(n -> n).sum() return?
  1. 18
  2. 15
  3. 20
  4. 13
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Values are -9, -4 and -12. What does values.stream().filter(n -> n > -8).max(Integer::compare).map(n -> n * 2).orElse(0) return?
  1. 0
  2. -8
  3. -4
  4. -18
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A 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.
  1. sales.stream().filter(s -> s.getUnits() > 0).collect(groupingBy(Sale::getTeam, summingInt(Sale::getUnits))).entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse("none")
  2. sales.stream().filter(s -> s.getUnits() > 0).collect(groupingBy(Sale::getTeam, averagingInt(Sale::getUnits))).entrySet().stream().max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse("none")
  3. sales.stream().collect(groupingBy(Sale::getTeam, averagingInt(Sale::getUnits))).entrySet().stream().filter(e -> e.getValue() > 0).max(Map.Entry.comparingByValue()).map(Map.Entry::getKey).orElse("none")
  4. sales.stream().filter(s -> s.getUnits() > 0).collect(groupingBy(Sale::getTeam, averagingInt(Sale::getUnits))).entrySet().stream().max(Map.Entry.comparingByKey()).map(Map.Entry::getKey).orElse("none")
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

For words "an", "oak", "to", "elm", what is the true-side value from partitioningBy(w -> w.length() == 2, mapping(String::toUpperCase, toList()))?
  1. [AN, TO]
  2. [OAK, ELM]
  3. [AN, TO, ELM]
  4. [AN, OAK, TO, ELM]
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

87 Key Terms, each with its definition

A

Associativity
Associativity means regrouping an operation's inputs does not change its result, as required for a stream reduction's accumulator.
Associativity makes addition suitable for integer reduction, while subtraction is unsuitable.
Autoboxing
Autoboxing is Java's automatic conversion of a primitive value into its corresponding wrapper object when a reference value is needed.
Autoboxing lets the literal 5 be passed to a constructor expecting an Integer.

B

BinaryOperator
A BinaryOperator is a function that combines two values of the same type into one result of that same type.
A BinaryOperator<Integer> such as Integer::sum can merge counts for a repeated map key.
Block body
A block body is a lambda body enclosed in braces; when a result is required, its paths must return values explicitly.
A block body uses return a; and return b; to implement a maximum operation.
Bound method reference
A bound method reference names an instance method on a particular receiver object fixed when the reference is evaluated.
The bound method reference System.out::println prints using that particular output object.

C

Classifier
A classifier is a function that assigns each input element a grouping key, determining which map entry receives it.
The classifier String::length puts all four-letter words under the same integer key.
Collection factory
A collection factory supplies a specific collection implementation to a collector so the result has the needed behavior or ordering.
The collection factory ArrayList::new explicitly requests a list that permits later additions.
Collector
A collector describes how a stream's elements are gathered or summarized into a result, such as a list, string, map or grouped calculation.
A collector can build the result list directly instead of changing an outside list in forEach.
Comparator
A Comparator is a separate ordering rule whose compare method returns a negative, zero or positive result for two input values.
A Comparator<String> can order words by length without changing their natural string order.
Comparator reversal
A comparator reversal flips the complete order represented by the comparator before the reversed() call, including existing tie-breakers.
Comparator reversal at the end of a group-and-score chain reverses both group and score order.
Constructor reference
A constructor reference uses ClassName::new to supply object creation behavior, with arguments determined by the target interface.
The constructor reference Student::new can turn a name into a student with a matching constructor.
Consumer
A Consumer is a standard functional interface for an action on one input; its accept method returns no value.
A Consumer<String> can print each word when passed to a collection's forEach.

D

Diamond operator
The diamond operator is <> in a generic constructor call, asking the compiler to infer type arguments while keeping type checks.
The diamond operator in new ArrayList<>() keeps the list typed from its context.
distinct
The distinct operation removes duplicate elements from a stream using object equality, retaining encounter order for an ordered stream.
Applying distinct before mapping words to lengths can still leave repeated lengths in the result.
Downstream collector
A downstream collector gathers or summarizes the elements within each group or partition, determining the map's value type.
The downstream collector counting() changes a map of group lists into a map of Long counts.
Duplicate key
A duplicate key occurs when different input elements produce equal map keys, requiring a rule to determine one stored value.
A duplicate key makes two-argument toMap fail even when both associated values are 85.

E

Effectively final
An effectively final local variable is assigned once without later reassignment, allowing a lambda to capture it without the final keyword.
An effectively final list reference may still have elements added to its referred list.
Encounter order
An encounter order is the defined sequence in which a source presents its elements, such as list order or a sorted map's key order.
Encounter order makes findFirst choose the first matching name in a source list.
Erased signature
An erased signature is a method signature with parameter types considered after erasure, which helps determine whether overloads clash.
The erased signature show(Set) is shared by overload attempts using Set<String> and Set<Long>.
Expression body
An expression body is a lambda body written as a single expression whose value supplies the required result without an explicit return.
The expression body x + 1 lets x -> x + 1 return the incremented integer directly.

F

filter
The filter operation keeps stream elements whose predicate is true, preserving their element type while dropping other values.
A points filter must run before mapping students to names if it needs the points getter.
flatMap
The flatMap operation converts each input to a stream and combines those streams' elements into one output sequence.
A flatMap using List::stream turns nested integer lists into one stream of integers.
Function
A Function is a standard functional interface that transforms one input of type T into a result of type R through apply.
A Function<String, Integer> can transform each word into its length before grouping.
Function composition
Function composition joins transformations so one receives another's result; andThen and compose specify opposite call orders.
Function composition doubles then increments in twice.andThen(inc), giving 11 from 5.
Functional interface
A functional interface has one abstract method to implement, excluding declarations matching public instance methods of Object.
A functional interface such as Operation lets a caller supply its calculation with a lambda.

G

Generic class
A generic class declares one or more type parameters so its fields and methods can preserve caller-chosen type relationships.
The generic class Box<T> supports a string box and an integer box with one declaration.
Generic interface
A generic interface declares type parameters that connect the types used in its method contract across different implementations.
A generic interface Container<T> can keep the input to put linked to the result of take.
Generic method
A generic method declares its own type parameters before its return type, allowing each call to choose related input and result types.
The generic method last returns a string for a string list and an integer for an integer list.
groupingBy
The groupingBy collector builds a map from encountered classifier keys to lists or downstream summaries of their matching elements.
A groupingBy collector with averagingInt stores one double-valued average per student group.

I

Identity
An identity is a value that leaves another value unchanged under a reduction's operation, such as zero for addition or one for multiplication.
The identity 1 lets a product reduction preserve its first factor instead of forcing the result to zero.
Indexed loop
An indexed loop uses an explicit position variable to access sequence elements, making neighboring positions and control decisions visible.
An indexed loop can compare list.get(i) with list.get(i - 1) while updating two counters.
Integer overflow
Integer overflow occurs when integer arithmetic exceeds the representable range and wraps, potentially reversing a comparison sign.
Integer overflow makes subtracting opposite large integers unsafe as a comparator implementation.
Intermediate operation
An intermediate operation adds a lazy processing stage and returns another stream, allowing selection or transformation before a final result.
The intermediate operation filter waits for a terminal operation before testing elements.
Intermediate result
An intermediate result is a named value between processing stages, such as a grouped map that a later pipeline queries.
An intermediate result holding average temperatures by city is filtered after grouping to select warm cities.
IntStream
An IntStream is a stream of primitive int values with numeric operations such as sum and average, plus conversions back to objects.
An IntStream from mapToInt can sum points without first collecting wrapper values.
Invariance
Invariance means different arguments of a generic type do not acquire a subtype relationship just because their argument types have one.
Invariance rejects using List<Integer> as List<Number>, protecting it from double-valued writes.

J

joining
The joining collector combines character sequences into one string, with optional text between elements and around the whole result.
A joining collector with a comma delimiter produces no trailing comma after the final name.

K

Key extractor
A key extractor is a function that selects the value used to compare an object, such as its name, group or numeric score.
The key extractor Student::getPoints builds a numeric student comparator with comparingInt.

L

Lambda expression
A lambda expression supplies parameters and a body for a functional interface's abstract method, using the arrow -> between them.
The lambda expression (a, b) -> a * b supplies the multiplication rule for an operation.
Lazy evaluation
Lazy evaluation delays processing until a result requires it and permits a stream to consume only as many elements as needed.
Lazy evaluation lets findFirst stop once a passing word has been found and transformed.
limit
The limit operation retains at most the requested number of elements from the start of an ordered stream, allowing later input to be skipped.
A limit before sorting selects from the original beginning, not the highest values overall.
Lower-bounded wildcard
A lower-bounded wildcard such as ? super Integer names an unknown type that is the stated type or one of its supertypes.
A lower-bounded wildcard lets an integer writer accept a modifiable List<Number> destination.

M

map
The map operation replaces each input element with one computed result, so it can change the stream's element type.
A map using String::length changes words into integer wrapper values, not a primitive stream.
Map entry
A map entry is a key-value pair viewed as one object, allowing a map's associations to be streamed and compared by key or value.
A map entry comparator can place the largest stock quantity first without reordering the source map.
mapping
The mapping collector transforms elements before passing their results to another collector, often within a group or partition.
A mapping collector can extract student names inside each group before collecting a list.
Matching operation
A matching operation returns whether any, all or none of a stream's elements satisfy a predicate, and can stop when the answer is known.
A matching operation on empty input gives false for anyMatch but true for allMatch.
Merge function
A merge function combines two values associated with the same key into the single value a map should retain for that key.
The merge function Math::max keeps the greatest quantity when a stock key occurs twice.
Method reference
A method reference names existing method or constructor behavior with ::, fitting the inputs and result of a functional-interface target.
The method reference Integer::parseInt supplies parsing behavior to a string-to-integer function.
Multiple bounds
Multiple bounds combine requirements with &, so a type argument must satisfy each bound, with a class listed first if present.
Multiple bounds in <T extends Number & Comparable<T>> require numeric and comparison behavior.

N

Natural order
A natural order is the order a class defines through Comparable.compareTo, used when no separate comparator is supplied.
The natural order of strings places the uppercase word "Cherry" before lowercase "apple".
Non-interference
Non-interference means pipeline behavior leaves its source unchanged while it is being processed, preserving valid traversal.
Non-interference is broken when a stream action adds new elements to its source ArrayList.
Null-aware comparator
A null-aware comparator gives null values an explicit position, typically by wrapping a non-null order with nullsFirst or nullsLast.
A null-aware comparator for names handles an existing student whose extracted name is null.

O

Optional
An Optional is a container representing either one value or no value, allowing a method to express a potentially missing result.
An Optional from a filtered maximum can be empty, so a fallback handles the missing case.
OptionalDouble
An OptionalDouble represents a primitive double value that may be absent, as with the average of an empty numeric stream.
An OptionalDouble from average() can supply a chosen 0.0 fallback through orElse.

P

partitioningBy
The partitioningBy collector divides elements by a predicate and always returns both Boolean keys, even if one side is empty.
A partitioningBy collector can return an empty true list when no student reaches the threshold its predicate tests.
peek
The peek operation passes elements on unchanged while running an inspection action for traversed values; its calls may be skipped.
A peek used to print values is unreliable for required work because execution may omit its stage.
Pipeline
A pipeline links a source, zero or more intermediate operations and one terminal operation that produces a result or performs an action.
The pipeline from stream through filter to count produces a long-valued number of matches.
Predicate
A Predicate is a standard functional interface for a condition, taking one value and returning boolean through test.
A Predicate<String> passed to removeIf identifies matching words that should be deleted.
Primitive specialization
A primitive specialization is a functional interface variant that accepts or returns primitive values directly, avoiding wrapper conversion.
The primitive specialization ToIntFunction<String> supplies lengths directly to mapToInt.
Producer extends, consumer super
Producer extends, consumer super is a rule for choosing extends wildcards for value sources and super wildcards for value destinations.
Producer extends, consumer super guides the source and destination parameters of a list transfer.

R

Raw type
A raw type is a generic class or interface used without type arguments, bypassing some checks that parameterized use would enforce.
A raw type in List xs can allow a bad write that a List<String> would reject.
Reduction
A reduction combines stream elements into a single result using an accumulator, optionally starting from a neutral identity value.
A reduction with addition and zero computes a total without updating an outside local counter.

S

Short-circuiting
Short-circuiting allows an operation to finish once its result is known, without necessarily processing every source element.
Short-circuiting lets anyMatch return true as soon as one element satisfies its predicate.
Side effect
A side effect is a change outside a computation's returned value, such as modifying a captured list or a surrounding object's field.
A side effect hidden in a pipeline can make the result depend on updates outside its input values.
skip
The skip operation discards the requested number of initial elements from an ordered stream and passes the remaining elements on.
After skip(2) on the inclusive range 1 through 10, the next emitted value is 3.
Stable sort
A stable sort preserves the original relative order of elements that compare as equal under the ordering rule being used.
A stable sort keeps Ada before Chen when both have 85 points and Ada appeared first.
Static context
A static context belongs to a class itself, so it cannot use a type parameter tied to that class's parameterized instances.
In a static context, a box factory introduces its own <E> instead of using the class's T.
Stream
A stream is a sequence processed through operations from a source; it does not store a separate collection of elements at every stage.
A stream from the list points can count the values that pass a test while leaving the list unchanged.
Supplier
A Supplier is a standard functional interface whose zero-argument get method provides a value of its result type.
A Supplier<List<String>> using ArrayList::new can provide a fresh list on each call.

T

Target type
A target type is the expected functional interface that supplies a lambda or method reference with its parameter and result types.
The target type Predicate<String> makes s -> s.isEmpty() a Boolean test of a string.
Terminal operation
A terminal operation ends a stream pipeline by producing a result or performing an action, consuming the stream for that computation.
The terminal operation count returns a long and leaves the same stream unavailable for reuse.
Tie-breaker
A tie-breaker is an additional comparison rule used only when all preceding rules rank the two elements as equal.
A name tie-breaker puts Ada before Chen after their equal scores leave the first comparison tied.
Transitivity
Transitivity requires an ordering to put a before c whenever it puts a before b and b before c, keeping comparisons consistent.
Transitivity prevents a comparator from creating a cycle among three ranked objects.
Type argument
A type argument is the actual reference type supplied when a generic class, interface or method is used.
The type argument String makes a Box<String> getter return text without a cast.
Type erasure
Type erasure replaces generic type parameters by their bounds, or Object, after checking, with necessary casts inserted by the compiler.
Type erasure makes string-box and integer-box objects share the same run-time Box class.
Type inference
Type inference is the compiler's process of determining omitted type information from arguments and the surrounding target context.
Type inference lets last(words) produce a String when words is a list of strings.
Type parameter
A type parameter is a name introduced in a generic declaration to stand for a type supplied by code that uses it.
The type parameter T gives a box's field and getter the same caller-chosen type.
Type witness
A type witness is an explicit method type argument written between a receiver's dot and the method name at a call site.
The type witness in Lists.<String>last(words) states the method's type choice explicitly.

U

UnaryOperator
A UnaryOperator is a function whose single input and result have the same type, often used for replacing collection elements.
A UnaryOperator<String> can uppercase every word through a modifiable list's replaceAll.
Unbound method reference
An unbound method reference names an instance method through its class, taking the receiver from the first functional argument.
The unbound method reference String::length calls length() on each supplied string.
Unbounded wildcard
An unbounded wildcard is ? without a bound, representing an unknown argument while permitting element reads only as Object.
An unbounded wildcard in List<?> lets a display method inspect list size without knowing its element type.
Unchecked warning
An unchecked warning reports an operation whose generic type safety the compiler cannot fully establish, often due to raw use or a cast.
An unchecked warning on a raw list's add points to a potential wrong-type read later.
Unmodifiable list
An unmodifiable list rejects operations that change its elements or structure through the list, without making its element objects immutable.
The unmodifiable list from Stream.toList() throws when code tries to add another element.
Upper bound
An upper bound restricts a type parameter to a named type and its subtypes, making that type's operations available in generic code.
The upper bound in <T extends Number> allows calls to doubleValue() on a T.
Upper-bounded wildcard
An upper-bounded wildcard such as ? extends Number names an unknown type that is the stated type or one of its subtypes.
An upper-bounded wildcard lets a numeric total read both integer lists and double lists.

V

Variable capture
Variable capture occurs when a lambda uses a local variable from its surrounding code, subject to Java's final-variable restrictions.
Variable capture lets a predicate compare each input against a surrounding limit value.

W

Wrapper class
A wrapper class is a reference type that represents a primitive value, allowing values such as integers to be used as generic arguments.
The wrapper class Integer makes List<Integer> valid, while List<int> is rejected.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started