Java Programming

Chapter 9
Functional Java and Stream Processing

Certificate in Java-Python-Programming Languages
Java Programming   •   Certificate

Functional Java and Stream Processing

Introduction

Modern software frequently transforms collections of data: filtering transactions, converting records into reports, grouping customers, calculating totals, and identifying exceptions. Java traditionally performed these tasks with explicit loops, mutable variables, and conditional statements. Since Java 8, the language has also supported a functional style through lambda expressions, functional interfaces, method references, Optional, streams, and collectors.

Functional Java does not replace object-oriented programming. Instead, it provides a complementary way to describe what should happen to data while allowing the library to manage much of how the iteration occurs. This declarative approach can improve readability, reduce accidental mutation, and make data-processing pipelines easier to compose. It also introduces responsibilities: developers must understand evaluation order, side effects, null handling, collector behavior, and the limitations of parallel execution.

This chapter develops the practical foundations required to use these features safely in certificate-level Java programming.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain lambda expressions and functional interfaces;
  • use built-in interfaces such as Predicate, Function, Consumer, Supplier, and Comparator;
  • apply method references appropriately;
  • use Optional to represent potentially absent values;
  • construct stream pipelines using intermediate and terminal operations;
  • collect results into lists, sets, maps, groups, and summaries;
  • distinguish stateless, stateful, and short-circuiting operations;
  • recognize when parallel streams are unsuitable;
  • design readable and reliable functional data transformations.

1. The Functional Programming Model in Java

Functional programming treats computation as the evaluation and composition of functions. A function accepts input and produces output. In a pure function, the same input produces the same output and the function does not change external state.

Java remains a statically typed, object-oriented language. Its functional facilities are implemented through objects representing behavior. A lambda expression is therefore not an independent function in the same sense as in some purely functional languages; it is an implementation of a compatible functional interface.

Consider an ordinary loop:

List<String> names = new ArrayList<>();
for (String name : users) {
    if (name.length() >= 5) {
        names.add(name.toUpperCase());
    }
}

A stream pipeline expresses the same transformation more declaratively:

List<String> names = users.stream()
        .filter(name -> name.length() >= 5)
        .map(String::toUpperCase)
        .toList();

The pipeline describes three conceptual operations: retain qualifying elements, transform each retained element, and materialize the result. The toList() method returns an unmodifiable list in modern Java versions; use Collectors.toList() when compatibility or a particular collector behavior is required.

Important terminology

  • Behavior parameterization: passing behavior, such as a predicate, into a method.
  • Lambda expression: a concise expression representing behavior.
  • Functional interface: an interface with exactly one abstract method.
  • Stream: a sequence-oriented abstraction for processing data, not a storage structure.
  • Pipeline: a source, zero or more intermediate operations, and a terminal operation.
  • Lazy evaluation: postponing an operation until a terminal result is requested.
  • Side effect: a change to state outside the local calculation.

2. Lambda Expressions and Functional Interfaces

A lambda has the general form:

(parameters) -> expression

or:

(parameters) -> { statements }

Examples include:

Predicate<Integer> positive = n -> n > 0;
Function<String, Integer> length = text -> text.length();
Consumer<String> printer = text -> System.out.println(text);
Supplier<Double> randomValue = () -> Math.random();

The compiler determines the target type from the context. This is called target typing. The following declarations are different because the expected interface is different:

Function<String, Integer> parse = Integer::valueOf;
Consumer<String> print = System.out::println;

Both references involve a String, but one returns an Integer and the other returns no value.

Built-in functional interfaces

InterfaceAbstract methodTypical meaning
Predicate<T>boolean test(T)Tests a condition
Function<T,R>R apply(T)Converts T to R
Consumer<T>void accept(T)Performs an action
Supplier<T>T get()Supplies a value without input
UnaryOperator<T>T apply(T)Converts T to the same type
BinaryOperator<T>T apply(T,T)Combines two values of the same type
Comparator<T>int compare(T,T)Defines ordering

A functional interface may contain default and static methods, but it must have only one abstract method. The @FunctionalInterface annotation asks the compiler to verify this design intention.

@FunctionalInterface
interface Validator<T> {
    boolean isValid(T value);
}

Capturing variables

A lambda may use local variables only when they are final or effectively final—assigned once and not subsequently changed.

int minimum = 10;
Predicate<Integer> largeEnough = value -> value >= minimum;

This restriction helps avoid unclear interactions between a lambda and a changing local variable. Objects referenced by an effectively final variable may still be mutable, so the restriction does not make all captured state immutable.

Composition

Functional interfaces often provide composition methods:

Predicate<String> nonBlank = text -> !text.isBlank();
Predicate<String> shortText = text -> text.length() <= 20;
Predicate<String> acceptable = nonBlank.and(shortText);

For functions:

Function<String, String> trim = String::trim;
Function<String, String> upper = String::toUpperCase;
Function<String, String> clean = trim.andThen(upper);

Composition is useful when rules are named and reused. Very long inline lambdas, however, can reduce readability and should often be replaced with a named method.

3. Method References

A method reference is a compact form of a lambda when an existing method already expresses the required behavior.

names.forEach(System.out::println);

Common forms are:

FormExampleMeaning
Static methodInteger::parseInttext -> Integer.parseInt(text)
Bound instance methodprinter::printvalue -> printer.print(value)
Unbound instance methodString::lengthtext -> text.length()
ConstructorArrayList::new() -> new ArrayList<>()

Method references are not automatically better than lambdas. Use them when they make the operation clearer. A lambda is preferable when adaptation, conditions, or multiple steps are involved.

4. Optional and Explicit Absence

A method that may fail to find a value traditionally returns null. This forces every caller to remember a null check and can produce a NullPointerException. Optional<T> is a container that explicitly represents either a present value or no value.

Optional<User> result = users.stream()
        .filter(user -> user.id() == requestedId)
        .findFirst();

Useful operations include:

String displayName = result
        .map(User::name)
        .filter(name -> !name.isBlank())
        .orElse("Unknown user");
  • of(value) requires a non-null value.
  • ofNullable(value) converts a possibly null reference.
  • empty() represents absence.
  • map transforms a present value.
  • flatMap avoids nested Optional values when the mapper already returns an Optional.
  • filter retains a value only when a condition is true.
  • orElse supplies a fallback value.
  • orElseGet computes a fallback lazily.
  • orElseThrow reports absence through an exception.
  • ifPresent performs an action only when a value exists.

A crucial distinction is that orElse evaluates its argument immediately, whereas orElseGet invokes its supplier only if needed:

value.orElse(expensiveDefault());
value.orElseGet(() -> expensiveDefault());

Use Optional primarily as a return type for a value that may be absent. It is generally not appropriate for every field, method parameter, or collection element. A collection should normally be empty rather than represented as Optional<List<T>>.

Avoid calling get() without first establishing presence. Code such as optional.get() merely moves the possibility of failure to another location and is usually less expressive than map, orElse, or orElseThrow.

5. Stream Architecture

A stream is not a collection and does not store elements. It obtains elements from a source, processes them through operations, and may produce a result.

Source → Intermediate operations → Terminal operation
List   → filter → map → sorted  → collect

Sources

Common sources include:

list.stream();
Set.of("A", "B").stream();
Stream.of("red", "green", "blue");
Arrays.stream(array);
IntStream.range(0, 10);

Streams are single-use. After a terminal operation, the stream is closed for practical purposes and must not be reused.

Intermediate operations

Intermediate operations return another stream and are lazy. Common examples include:

  • filter: retains elements satisfying a predicate;
  • map: transforms each element;
  • flatMap: transforms each element into a stream and flattens the results;
  • distinct: removes duplicates according to equality;
  • sorted: orders elements;
  • peek: observes elements, mainly for debugging;
  • limit and skip: restrict the sequence.

Terminal operations

Terminal operations initiate evaluation. Examples include forEach, toList, collect, reduce, count, min, max, findFirst, findAny, anyMatch, allMatch, and noneMatch.

Short-circuiting operations may stop before processing all elements. For example, anyMatch can finish as soon as it finds a matching element. This is both a performance feature and a reason not to rely on a pipeline processing every element.

Laziness and fusion

The following pipeline performs no work until count() is called:

Stream<String> pipeline = words.stream()
        .filter(word -> word.length() > 4)
        .map(String::toUpperCase);
long total = pipeline.count();

The implementation can often fuse operations so that each element passes through the stages without creating an intermediate collection. This can improve efficiency, but it does not guarantee a particular internal execution strategy.

6. Core Transformations

Filtering and mapping

List<String> activeEmails = accounts.stream()
        .filter(Account::active)
        .map(Account::email)
        .filter(email -> email.endsWith(".edu"))
        .toList();

The order matters. Filtering before an expensive mapping can reduce work. A pipeline should normally place inexpensive, highly selective filters early, while preserving the required semantics.

Flattening with flatMap

Suppose each order contains several line items:

List<LineItem> items = orders.stream()
        .flatMap(order -> order.items().stream())
        .toList();

map(Order::items) would produce a stream of lists. flatMap produces one stream containing the individual items.

Reduction

A reduction combines many values into one result:

int total = quantities.stream()
        .reduce(0, Integer::sum);

The identity 0 is the result for an empty stream and the neutral element for addition. For multiplication, the identity would be 1. The accumulator must be compatible with the identity and operation. In parallel execution, a combiner may also be required; therefore, the operation should be associative. Addition is associative in exact integer arithmetic, but floating-point addition can produce small order-dependent differences because of rounding.

Prefer specialized operations when available:

int total = quantities.stream().mapToInt(Integer::intValue).sum();

Primitive streams such as IntStream, LongStream, and DoubleStream avoid unnecessary boxing for numerical work.

7. Collectors and Aggregation

A collector describes how stream elements are accumulated into a result. The Collectors utility class provides common collectors.

List<String> list = stream.collect(Collectors.toList());
Set<String> set = stream.collect(Collectors.toSet());
String joined = stream.collect(Collectors.joining(", "));

Grouping

Map<String, List<Employee>> byDepartment = employees.stream()
        .collect(Collectors.groupingBy(Employee::department));

A downstream collector can summarize each group:

Map<String, Long> headcount = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.counting()));

For averages:

Map<String, Double> averageSalary = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.averagingDouble(Employee::salary)));

For partitioning by a Boolean condition:

Map<Boolean, List<Employee>> experienced = employees.stream()
        .collect(Collectors.partitioningBy(e -> e.years() >= 5));

groupingBy can create many groups; partitioningBy creates two logical groups, one for true and one for false.

Mapping and filtering downstream

Map<String, Set<String>> skillsByDepartment = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.mapping(Employee::primarySkill,
                                   Collectors.toSet())));

A collector can also filter values within each group:

Map<String, List<Employee>> seniorByDepartment = employees.stream()
        .collect(Collectors.groupingBy(
                Employee::department,
                Collectors.filtering(e -> e.years() >= 5,
                                     Collectors.toList())));

toMap and duplicate keys

Map<Integer, String> namesById = employees.stream()
        .collect(Collectors.toMap(Employee::id, Employee::name));

This throws an exception if two employees have the same ID. Supply a merge function when duplicates are valid:

Map<String, Integer> totals = entries.stream()
        .collect(Collectors.toMap(
                Entry::category,
                Entry::amount,
                Integer::sum));

The merge policy is part of the business meaning of the program. Silently choosing the first or last value may discard important information.

8. A Worked Example: Order Analytics

Assume the following record types:

record Order(String customer, String region, double amount, boolean paid) {}

Given a list of orders, the following pipeline obtains the total paid value for European regions:

double total = orders.stream()
        .filter(Order::paid)
        .filter(order -> order.region().equals("EU"))
        .mapToDouble(Order::amount)
        .sum();

Suppose the data is:

CustomerRegionAmountPaid
AEU120.00true
BUS200.00true
CEU80.00false
DEU150.00true

The first filter retains A, C, and D. The second retains A and D. The amounts are 120.00 and 150.00, so the total is 270.00 monetary units. The code does not itself impose a currency; production software should use an appropriate money representation, such as integer minor units or BigDecimal, rather than relying casually on binary floating-point values for financial calculations.

To group paid orders by region:

Map<String, Double> revenueByRegion = orders.stream()
        .filter(Order::paid)
        .collect(Collectors.groupingBy(
                Order::region,
                Collectors.summingDouble(Order::amount)));

For financial systems, BigDecimal is safer when decimal exactness and explicit rounding rules are required:

Map<String, BigDecimal> revenue = orders.stream()
        .filter(Order::paid)
        .collect(Collectors.groupingBy(
                Order::region,
                Collectors.reducing(
                        BigDecimal.ZERO,
                        order -> order.amountAsBigDecimal(),
                        BigDecimal::add)));

The required scale, rounding mode, tax treatment, and currency must be defined by the application rather than assumed by the stream code.

9. Side Effects, Ordering, and Correctness

Functional pipelines are easiest to reason about when operations are stateless and non-interfering. This is unsafe:

List<String> result = new ArrayList<>();
words.stream()
        .filter(String::isBlank)
        .forEach(result::add);

It introduces external mutation and becomes particularly dangerous if the stream is changed to parallel. Prefer:

List<String> result = words.stream()
        .filter(String::isBlank)
        .toList();

peek should not be used as the main mechanism for application behavior. It is useful for temporary diagnostics, but business actions such as sending messages, updating databases, or charging accounts should be explicit and carefully controlled.

Encounter order may matter. forEach on a parallel stream does not guarantee encounter order; forEachOrdered preserves order but may reduce parallel performance. Operations such as findAny are permitted to return any matching element, whereas findFirst respects encounter order when one exists.

Streams also do not automatically make mutable objects safe. If multiple operations access shared mutable state, normal concurrency rules still apply.

10. Parallel Streams: Awareness Before Adoption

A parallel stream divides work across the common Fork/Join pool. It can help for sufficiently large datasets and computationally expensive, independent operations. It is not a universal performance improvement.

long count = values.parallelStream()
        .filter(this::expensiveTest)
        .count();

Parallelism is more appropriate when:

  • the data set is large enough to offset task-splitting overhead;
  • each element can be processed independently;
  • the operation is CPU-bound rather than waiting on I/O;
  • the source can be efficiently divided;
  • the result combination is associative and safe;
  • ordering is not essential, or its cost is acceptable.

It is usually unsuitable for database calls, network requests, file operations, GUI updates, or code using shared mutable state. The common pool is shared with other application work, so an uncontrolled parallel stream can interfere with unrelated tasks.

A parallel reduction must satisfy identity, accumulator, and combiner requirements. For an operation ⊕ and identity e, the conceptual conditions include:

e ⊕ x = x
x ⊕ e = x
(x ⊕ y) ⊕ z = x ⊕ (y ⊕ z)

Here, e is the identity and ⊕ is the combining operation. These conditions explain why addition is a good reduction operation and why subtraction generally is not. Floating-point calculations are an important practical exception: although addition is mathematically associative, finite-precision machine arithmetic can vary slightly with grouping and order.

Never assume that parallelStream() is faster. Measure realistic workloads using a suitable benchmark, such as JMH, rather than relying on a single informal timing.

11. Exceptions and Debugging

Checked exceptions do not fit directly into standard functional interfaces whose abstract methods do not declare them. A common but risky response is to catch the exception inside a lambda and rethrow an unchecked exception. A clearer approach may be to define a domain-specific functional interface, handle errors before the pipeline, or return a result structure that represents success and failure.

For debugging, divide a complex pipeline into named stages or temporarily use peek:

orders.stream()
        .filter(Order::paid)
        .peek(order -> logger.debug("Paid order: {}", order))
        .map(this::toInvoice)
        .toList();

Logging should not expose confidential customer or payment information. Debugging techniques must respect privacy and security requirements.

12. Applied Case: Subscription Billing Review

Background

This illustrative case concerns a small subscription company that receives a list of customer accounts each day. Each account has a customer ID, region, monthly charge, and payment status. The billing team needs a report of paid revenue by region and a separate list of unpaid accounts requiring follow-up.

Problem

The original program used nested loops, modified several shared maps, and mixed data validation with report generation. Duplicate customer IDs sometimes overwrote earlier records without explanation. The code was difficult to test because the calculation and file-writing steps were interwoven.

Analysis

The task can be decomposed into independent transformations:

  1. validate the relevant account data;
  2. partition paid and unpaid accounts;
  3. sum paid charges by region;
  4. produce a follow-up list;
  5. make duplicate handling explicit.

The design should also distinguish absent data from invalid data. An absent optional discount is not necessarily an error, whereas a negative charge may violate a business rule.

Approach and solution

A simplified model is:

record Account(String id, String region,
               BigDecimal charge, boolean paid) {}

The revenue report can be produced as follows:

Map<String, BigDecimal> revenueByRegion = accounts.stream()
        .filter(account -> account.charge().signum() >= 0)
        .filter(Account::paid)
        .collect(Collectors.groupingBy(
                Account::region,
                Collectors.reducing(
                        BigDecimal.ZERO,
                        Account::charge,
                        BigDecimal::add)));

The unpaid follow-up list is a separate query:

List<String> unpaidIds = accounts.stream()
        .filter(account -> !account.paid())
        .map(Account::id)
        .distinct()
        .toList();

If IDs must be unique and duplicates indicate a data-quality problem, the application should detect and report them instead of silently using distinct(). A toMap collector with an explicit merge function can encode an approved policy, but data-quality exceptions may deserve separate validation.

Outcome

The revised design separates validation, transformation, aggregation, and external reporting. Each stage can be unit-tested with small lists. Revenue calculations use BigDecimal, making decimal behavior explicit. No shared mutable map is updated inside forEach, and the code remains sequential because the workload includes modest data volumes and downstream file operations.

Learning points

  • A stream pipeline is a design tool, not merely a shorter loop.
  • Collector choices express business rules.
  • Financial precision and duplicate handling cannot be delegated blindly to the API.
  • Sequential streams are often the correct professional choice.
  • Validation and reporting should be separated for maintainability.

13. Best Practices and Limitations

  1. Give complex predicates and transformations meaningful names.
  2. Prefer immutable or unmodifiable results when callers should not mutate them.
  3. Use primitive streams for straightforward numeric calculations.
  4. Use Optional to communicate possible absence, not as a universal replacement for null.
  5. Make duplicate-key behavior explicit with toMap.
  6. Avoid side effects in intermediate operations.
  7. Do not use streams merely to make simple code appear sophisticated.
  8. Preserve domain semantics: filtering invalid records may hide a data-quality defect.
  9. Consider BigDecimal or integer minor units for monetary values.
  10. Benchmark before adopting parallel execution.
  11. Test empty streams, duplicate values, null-related boundaries, and ordering requirements.
  12. Treat data protection as part of pipeline design; avoid logging sensitive values and limit data exposure.

Streams have limitations. Deeply nested pipelines can be harder to debug than loops. Error recovery may be less obvious when exceptions arise in the middle of a transformation. A loop may be clearer when the algorithm requires complex state transitions, early actions with multiple effects, or detailed control flow. Good Java programming requires choosing the clearest correct abstraction, not maximizing the number of lambdas.

Contemporary Developments

Modern Java continues to improve collection and stream ergonomics. Stream.toList() provides a concise way to obtain an unmodifiable list. Records pair naturally with functional transformations because their accessors are compact and their data is generally immutable. Pattern matching and newer language features can make predicates and transformations clearer as Java evolves. These features do not change the central principles: make data flow visible, control side effects, define absence and errors explicitly, and measure performance rather than assuming it.

Chapter Summary

Functional Java provides a declarative approach to data processing through lambda expressions, functional interfaces, method references, Optional, streams, and collectors. Lambda expressions implement functional interfaces such as Predicate, Function, Consumer, and Supplier. Method references offer concise syntax when an existing method matches the required behavior.

Optional communicates possible absence and supports transformations and fallback policies without uncontrolled null checks. Streams process data through lazy intermediate operations and an initiating terminal operation. Filtering, mapping, flattening, reduction, grouping, partitioning, and summarizing support common analytical tasks. Collectors must be selected carefully because duplicate-key policies, ordering, precision, and mutability carry business meaning.

Side effects should be minimized, especially in stream pipelines. Parallel streams are appropriate only when the workload, source, operations, and reduction satisfy suitable conditions; they should be validated by measurement. Professional use of functional Java combines concise syntax with explicit domain rules, safe data handling, testability, and appropriate attention to limitations.

Key Terms

Lambda expression: A concise expression representing behavior compatible with a functional interface.

Functional interface: An interface containing exactly one abstract method.

Predicate: A function that tests a value and returns a Boolean result.

Function: A function that converts an input value into an output value.

Consumer: A function that accepts a value and returns no result.

Supplier: A function that supplies a value without receiving an input.

Method reference: Syntax that refers to an existing method as functional behavior.

Optional: A container representing a present value or explicit absence.

Stream: A sequence-processing abstraction that does not itself store data.

Pipeline: A source followed by intermediate operations and a terminal operation.

Intermediate operation: A lazy stream operation that returns another stream.

Terminal operation: An operation that initiates stream evaluation and produces a result or effect.

Lazy evaluation: Delayed execution until a terminal result is requested.

Map: A transformation that converts each input element to an output element.

Flat map: A transformation that converts elements into streams and combines those streams.

Reduction: Combining multiple values into one result.

Collector: A reusable strategy for accumulating stream elements into a result.

Side effect: A change to state outside the local computation.

Parallel stream: A stream whose processing may be divided among multiple threads.

Encounter order: The order in which a stream source logically presents its elements.

Review Questions

Short Answer Questions

  1. What is a functional interface, and why is it required by a lambda expression?
  2. Distinguish map from flatMap in a stream pipeline.
  3. Why is Optional.orElseGet sometimes preferable to Optional.orElse?
  4. What is the purpose of a terminal stream operation?
  5. State two situations in which a parallel stream may be unsuitable.

Long Answer Questions

  1. Explain the structure of a stream pipeline and discuss laziness, intermediate operations, and terminal operations.
  2. Compare Predicate, Function, Consumer, and Supplier, giving a Java example of each.
  3. Explain how grouping, partitioning, and downstream collectors support data analytics.
  4. Discuss side effects, ordering, and thread safety in sequential and parallel streams.
  5. Describe the role of Optional in API design and identify practices that can make its use ineffective.

Analytical / Application Questions

  1. Design a stream pipeline that accepts a list of orders, retains paid orders above a chosen threshold, groups them by region, and calculates a total for each region. Explain your collector choices.
  2. A developer uses parallelStream() to perform a network request for every customer and adds each result to a shared ArrayList. Identify the risks and propose a safer design.
  3. A map-building pipeline fails because two records have the same key. Explain why the failure occurs and propose two different responses, including when each response is appropriate.

Multiple Choice Questions with Answers

Question 1

Which interface is most appropriate for a lambda that tests whether an integer is greater than zero?

A. Predicate<Integer>

B. Consumer<Integer>

C. Supplier<Integer>

D. Function<Integer, String>

Correct Answer: A. Predicate<Integer>

Question 2

What does the stream operation filter do?

A. Converts every element to another type

B. Retains elements that satisfy a condition

C. Combines all elements into one value

D. Sorts elements in descending order

Correct Answer: B. Retains elements that satisfy a condition

Question 3

Which expression is a method reference equivalent to text -> text.length()?

A. String::length

B. String.length()

C. text::length()

D. length::String

Correct Answer: A. String::length

Question 4

Which Optional operation transforms a present value while preserving Optional-based absence handling?

A. map

B. get

C. isEmpty

D. orElseThrow

Correct Answer: A. map

Question 5

What is the main purpose of flatMap?

A. To remove duplicate elements

B. To flatten streams produced from elements

C. To force parallel execution

D. To sort nested collections

Correct Answer: B. To flatten streams produced from elements

Question 6

What happens if Collectors.toMap encounters duplicate keys without a merge function?

A. The first value is always retained

B. The last value is always retained

C. An exception is thrown

D. The entries are automatically grouped into lists

Correct Answer: C. An exception is thrown

Question 7

Which operation initiates evaluation of a lazy stream pipeline?

A. filter

B. map

C. sorted

D. count

Correct Answer: D. count

Question 8

Why can mutating a shared ArrayList inside a parallel stream be unsafe?

A. ArrayList automatically sorts its values

B. The list is immutable

C. Multiple threads may access the mutable list without safe coordination

D. Parallel streams cannot process lists

Correct Answer: C. Multiple threads may access the mutable list without safe coordination

Question 9

Which collector is designed to divide elements into two groups according to a Boolean predicate?

A. partitioningBy

B. joining

C. mapping

D. counting

Correct Answer: A. partitioningBy

Question 10

Which operation is generally required for a reduction intended to work reliably in parallel?

A. A non-associative accumulator

B. An associative combining operation

C. A shared mutable accumulator

D. A mandatory encounter-order dependency

Correct Answer: B. An associative combining operation

Java Programming
Chapter 9 of 15