Unit 1: Collections, Streams, Filters, and Lambdas - Subjective Questions
CSE406 — Advanced Java Programming • Practice Questions with Detailed Answers
20 questions
Define the Builder design pattern. Explain its structure, advantages, and use in Java with a suitable example.
The Builder pattern is a creational design pattern used to construct complex objects step by step. It is especially useful when an object has many optional parameters.
Main components:
- Product: The object being created.
- Builder: Provides methods for configuring the product.
- Build method: Creates and returns the final object.
Example:
class Student {
private final String name;
private final int age;
private final String course;
private Student(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.course = builder.course;
}
static class Builder {
private String name;
private int age;
private String course;
Builder name(String name) {
this.name = name;
return this;
}
Builder age(int age) {
this.age = age;
return this;
}
Builder course(String course) {
this.course = course;
return this;
}
Student build() {
return new Student(this);
}
}
}
The object can be created as follows:
Student student = new Student.Builder()
.name("Asha")
.age(20)
.course("Java")
.build();
Advantages:
- Avoids constructors with many parameters.
- Improves readability through method chaining.
- Supports immutable objects.
- Allows validation before object creation.
- Makes optional attributes easy to manage.
Explain how a Java collection can be iterated using lambda syntax. Compare it with the traditional enhanced for loop.
Java 8 introduced the forEach method, which accepts a Consumer functional interface and enables collection iteration using a lambda expression.
Using a lambda expression:
List<String> names = Arrays.asList("Asha", "Ravi", "Kiran");
names.forEach(name -> System.out.println(name));
Using a method reference:
names.forEach(System.out::println);
Traditional enhanced for loop:
for (String name : names) {
System.out.println(name);
}
Comparison:
- A lambda provides concise and declarative syntax.
- The enhanced
forloop provides explicit control over iteration. - Variables used inside a lambda must be final or effectively final.
breakandcontinuecannot be used to control the outer collection iteration from insideforEach.- Lambda-based iteration works naturally with stream pipelines and internal iteration.
Thus, forEach is suitable for simple actions on every element, while a loop may be preferable when detailed flow control is required.
Describe the Java Stream interface. How does a stream differ from a collection?
A Java stream is a sequence of elements that supports functional-style data-processing operations. The main stream API is represented by java.util.stream.Stream<T>.
A stream pipeline normally consists of:
- A source, such as a collection or array.
- Zero or more intermediate operations, such as
filter,map, andsorted. - A terminal operation, such as
collect,reduce, orforEach.
Example:
List<String> result = names.stream()
.filter(name -> name.length() > 4)
.map(String::toUpperCase)
.collect(Collectors.toList());
Stream versus collection:
- A collection stores data, whereas a stream processes data.
- A stream does not normally modify its source.
- A stream uses internal iteration; a collection is commonly traversed using external iteration.
- Stream intermediate operations are lazy.
- A stream can be consumed only once.
- Streams can execute sequentially or in parallel.
- Streams may represent finite or potentially infinite sequences.
Therefore, a collection is primarily a data structure, while a stream is a computational view of data.
How can a collection be filtered using lambda expressions? Explain the role of the Predicate interface with an example.
Filtering selects only those elements that satisfy a given condition. The stream method filter accepts a Predicate<T>.
A predicate represents a function that:
- Accepts one argument of type
T. - Tests a condition.
- Returns a Boolean result.
Its abstract method is conceptually:
boolean test(T value);
Example:
List<Integer> numbers = Arrays.asList(10, 15, 20, 25, 30);
List<Integer> evenNumbers = numbers.stream()
.filter(number -> number % 2 == 0)
.collect(Collectors.toList());
The lambda number -> number % 2 == 0 is a Predicate<Integer>. The result is [10, 20, 30].
Predicates can also be combined:
Predicate<Integer> even = number -> number % 2 == 0;
Predicate<Integer> greaterThanTen = number -> number > 10;
List<Integer> result = numbers.stream()
.filter(even.and(greaterThanTen))
.collect(Collectors.toList());
Filtering is non-destructive: the original collection remains unchanged.
Explain method chaining in Java stream processing. Illustrate how multiple stream methods are chained together.
Method chaining means invoking several methods in a sequence, where each method returns an object on which the next method can operate. Stream intermediate operations return another stream, making them suitable for chaining.
Example:
List<String> result = names.stream()
.filter(name -> name.length() >= 4)
.map(String::toUpperCase)
.distinct()
.sorted()
.collect(Collectors.toList());
Execution stages:
stream()creates a stream from the collection.filter(...)retains names having at least four characters.map(...)converts each selected name to uppercase.distinct()removes duplicate values.sorted()arranges values in natural order.collect(...)stores the result in a list.
Benefits:
- Produces concise and readable code.
- Expresses what should be calculated rather than how to iterate.
- Avoids temporary collections between processing stages.
- Supports lazy processing and operation fusion.
The order of chained operations can affect both the output and performance. For example, applying filter before map may reduce the number of elements that must be transformed.
Define a stream pipeline in terms of lambdas and collections. Explain its stages with a suitable example.
A stream pipeline is a sequence of operations that processes data obtained from a source. Lambda expressions commonly specify the behavior of individual processing stages.
A pipeline has three parts:
- Source: Supplies elements, for example a
List,Set, array, or generated stream. - Intermediate operations: Transform or select elements and return streams.
- Terminal operation: Produces a result or side effect and starts execution.
Example:
double total = products.stream()
.filter(product -> product.getPrice() >= 1000)
.map(product -> product.getPrice())
.reduce(0.0, (a, b) -> a + b);
Pipeline interpretation:
productsis the source collection.stream()creates the stream.filterselects products priced at least1000.mapextracts each selected product's price.reduceadds the prices and returns the total.
The lambda expressions describe the filtering, mapping, and reduction rules. Intermediate operations remain lazy until the terminal operation is requested.
Describe how to make a stream pipeline execute in parallel. Discuss the benefits, limitations, and safety considerations of parallel streams.
A parallel stream divides data into parts, processes those parts concurrently, and combines the partial results.
A parallel stream can be created in two main ways:
collection.parallelStream()
or:
collection.stream().parallel()
Example:
long total = numbers.parallelStream()
.filter(number -> number > 0)
.mapToLong(Integer::longValue)
.sum();
Parallel streams generally use Java's common ForkJoinPool.
Benefits:
- May improve performance for large data sets.
- Useful for CPU-intensive and independent computations.
- Requires little change to an existing stream pipeline.
Limitations:
- Thread creation, splitting, synchronization, and merging introduce overhead.
- Small data sets may execute more slowly in parallel.
- Ordered operations can limit parallel performance.
- Blocking I/O can interfere with the shared common pool.
Safety considerations:
- Operations should be stateless and non-interfering.
- Shared mutable state should be avoided.
- Reduction operators should be associative.
- Thread-safe collectors or proper stream collectors should be used instead of manually mutating shared collections.
Parallel execution should therefore be selected only after testing performance with realistic data.
Define reduction in Java streams. Distinguish between mutable and immutable reduction.
Reduction combines a sequence of stream elements into a single result or summary value.
Examples of reduction include:
- Calculating a sum or product.
- Finding a minimum or maximum.
- Concatenating strings.
- Building a result collection.
Immutable reduction:
- Produces a new accumulated value at each logical step.
-
Commonly performed using
reduce.int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
Here, 0 is the identity and the lambda is the accumulator.
Mutable reduction:
- Accumulates results into a mutable container such as a list, set, or map.
-
Commonly performed using
collect.List<String> result = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
Difference:
reduceis appropriate for immutable scalar results.collectis appropriate when elements must be accumulated into mutable result containers.- A mutable object should generally not be mutated inside
reduce, particularly in parallel pipelines.
Explain the overloaded forms of the reduce method. Write a stream expression to calculate the sum and product of a list of integers.
The reduce operation combines stream elements using an accumulator.
1. Reduction without an identity:
Optional<T> reduce(BinaryOperator<T> accumulator)
It returns an Optional because an empty stream has no value.
Optional<Integer> sum = numbers.stream()
.reduce((a, b) -> a + b);
2. Reduction with an identity:
T reduce(T identity, BinaryOperator<T> accumulator)
The identity is the neutral value of the operation.
int sum = numbers.stream()
.reduce(0, (a, b) -> a + b);
For addition, the identity is because .
Product calculation:
int product = numbers.stream()
.reduce(1, (a, b) -> a * b);
For multiplication, the identity is because .
3. Three-argument reduction:
U reduce(U identity,
BiFunction<U, ? super T, U> accumulator,
BinaryOperator<U> combiner)
This form can produce a result type different from the element type. The combiner merges partial results during parallel execution.
For correctness in parallel streams, the operation should be associative; that is, .
Describe the process of decomposing and merging work in a parallel stream. Explain the roles of the identity, accumulator, and combiner.
A parallel stream applies a divide-and-conquer strategy.
Process:
- The stream source is divided into smaller partitions.
- Different worker threads process partitions independently.
- Each worker produces a partial result.
- Partial results are merged to obtain the final result.
Java commonly uses a Spliterator to traverse and divide the source, while tasks are executed through the fork/join framework.
In a three-argument reduction:
int result = values.parallelStream().reduce(
0,
(partial, value) -> partial + value,
(left, right) -> left + right
);
- Identity: Initial value for every partial computation. Here it is
0. - Accumulator: Adds one stream element to a partial result.
- Combiner: Merges two partial results.
Correctness requirements:
- The combiner should be associative.
- The identity must be neutral for the operation.
- The accumulator and combiner must be compatible.
- The functions should not depend on shared mutable state.
If these rules are violated, a sequential pipeline may appear correct while the parallel version produces incorrect or unpredictable results.
Explain how the map operation extracts or transforms data from an object. Differentiate map from flatMap.
The map operation transforms each stream element by applying a Function<T, R>. It creates a new stream containing the transformed results.
Suppose each Employee has a getName() method:
List<String> employeeNames = employees.stream()
.map(Employee::getName)
.collect(Collectors.toList());
Here, map extracts the name from every Employee, converting a Stream<Employee> into a Stream<String>.
Other examples:
List<Double> prices = products.stream()
.map(Product::getPrice)
.collect(Collectors.toList());
Primitive specializations can avoid boxing:
double total = products.stream()
.mapToDouble(Product::getPrice)
.sum();
map versus flatMap:
mapconverts one input element into one output value.flatMapconverts one input element into a stream of values and then flattens all nested streams.
For example, when each department contains a list of employees:
List<Employee> allEmployees = departments.stream()
.flatMap(department -> department.getEmployees().stream())
.collect(Collectors.toList());
Thus, map is used for direct extraction or transformation, whereas flatMap is used for one-to-many transformations and nested structures.
Describe the different types of stream operations. Distinguish between intermediate, terminal, stateless, stateful, short-circuiting, and non-short-circuiting operations.
Stream operations can be classified according to when they execute and how they process elements.
Intermediate operations:
- Return another stream.
- Are lazy and do not normally execute until a terminal operation is called.
- Examples:
filter,map,distinct,sorted,limit, andskip.
Terminal operations:
- Produce a final value or side effect.
- Consume the stream and initiate pipeline execution.
- Examples:
collect,reduce,count,forEach,min, andmax.
Stateless operations:
- Process an element without retaining information about previously processed elements.
- Examples:
filterandmap.
Stateful operations:
- Need information about other elements.
- Examples:
sortedmay need all elements, anddistinctmust remember observed values.
Short-circuiting operations:
- May finish without processing the entire stream.
- Examples:
limit,findFirst,findAny,anyMatch,allMatch, andnoneMatch.
Non-short-circuiting operations:
- Generally process all relevant elements.
- Examples:
collect,count,reduce, andforEach.
Understanding these categories helps in designing correct and efficient stream pipelines.
Describe the Java Optional class. Explain how it helps avoid null-related errors and discuss its important methods.
Optional<T> is a container that may either contain a non-null value or be empty. It makes the possible absence of a result explicit and can reduce direct null checking.
Creating optional values:
Optional<String> empty = Optional.empty();
Optional<String> value = Optional.of("Java");
Optional<String> nullable = Optional.ofNullable(possiblyNullValue);
Optional.of throws an exception if its argument is null, whereas ofNullable creates an empty optional for null.
Important methods:
isPresent()checks whether a value exists.isEmpty()checks whether the optional is empty.ifPresent(action)executes an action when a value exists.map(function)transforms a contained value.filter(predicate)retains a value only if it satisfies a condition.orElse(defaultValue)supplies a default value.orElseGet(supplier)lazily obtains a default value.orElseThrow()throws an exception if empty.
Example:
String name = employees.stream()
.filter(employee -> employee.getId() == id)
.map(Employee::getName)
.findFirst()
.orElse("Unknown");
Optional should not normally be tested with isPresent() followed immediately by get(). Methods such as map, ifPresent, and orElseThrow communicate intent more safely.
What is lazy processing in Java streams? Explain how laziness and short-circuiting can improve efficiency.
Lazy processing means that intermediate stream operations are not executed when they are declared. Instead, they are stored as a pipeline and evaluated only when a terminal operation is invoked.
Example:
Optional<String> result = names.stream()
.filter(name -> {
System.out.println("Filtering " + name);
return name.startsWith("A");
})
.map(name -> {
System.out.println("Mapping " + name);
return name.toUpperCase();
})
.findFirst();
Until findFirst() is called, neither filter nor map processes any elements.
Advantages:
- Intermediate collections are usually not created.
- Operations can be fused and applied element by element.
- Unnecessary calculations can be avoided.
- Infinite streams can be processed when combined with limiting operations.
Because findFirst is short-circuiting, processing stops after the first matching element is found. Similarly:
long firstFive = Stream.iterate(1L, n -> n + 1)
.filter(n -> n % 2 == 0)
.limit(5)
.count();
Although the source is infinite, limit(5) allows the terminal operation to finish.
Explain how to sort a stream in natural order and custom order. How can objects be sorted using Comparator?
The sorted intermediate operation returns a stream whose elements are arranged in a specified order.
Natural-order sorting:
List<Integer> sortedNumbers = numbers.stream()
.sorted()
.collect(Collectors.toList());
Elements must implement Comparable for natural-order sorting.
Reverse-order sorting:
List<Integer> descending = numbers.stream()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
Sorting objects by one property:
List<Employee> bySalary = employees.stream()
.sorted(Comparator.comparing(Employee::getSalary))
.collect(Collectors.toList());
Sorting in descending order:
List<Employee> highestSalaryFirst = employees.stream()
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.collect(Collectors.toList());
Sorting by multiple fields:
List<Employee> sortedEmployees = employees.stream()
.sorted(Comparator.comparing(Employee::getDepartment)
.thenComparing(Employee::getName))
.collect(Collectors.toList());
sorted is a stateful intermediate operation because it may need to inspect all elements before producing ordered output. The source collection itself is not modified.
Explain how stream results are saved into collections using the collect method. Describe the supplier, accumulator, and combiner used in mutable reduction.
The collect terminal operation performs mutable reduction. It accumulates stream elements into a mutable result container such as a list, set, or map.
Using predefined collectors:
List<String> list = names.stream()
.filter(name -> name.length() > 3)
.collect(Collectors.toList());
Set<String> set = names.stream()
.collect(Collectors.toSet());
A specific collection implementation can be requested with toCollection:
LinkedList<String> linkedList = names.stream()
.collect(Collectors.toCollection(LinkedList::new));
Three-argument collect:
List<String> result = names.stream().collect(
ArrayList::new,
List::add,
List::addAll
);
Its arguments are:
- Supplier: Creates a new result container, such as
ArrayList::new. - Accumulator: Adds one stream element to the current container.
- Combiner: Merges two partial containers, especially during parallel execution.
Unlike an incorrect shared-list approach, collect can create separate containers for parallel tasks and safely merge them. It is therefore the preferred operation for building mutable collections from streams.
Explain how data is grouped using Collectors.groupingBy. Demonstrate simple grouping and downstream aggregation with examples.
Collectors.groupingBy classifies stream elements according to a key produced by a classifier function. Its result is normally a Map<K, List<T>>.
Grouping employees by department:
Map<String, List<Employee>> byDepartment = employees.stream()
.collect(Collectors.groupingBy(Employee::getDepartment));
Each map key is a department, and its value is the list of employees in that department.
Counting employees in each department:
Map<String, Long> employeeCount = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.counting()
));
Calculating average salary by department:
Map<String, Double> averageSalary = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
Mapping grouped employees to their names:
Map<String, List<String>> namesByDepartment = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.mapping(Employee::getName, Collectors.toList())
));
The second collector is called a downstream collector. It determines how the elements in each group are reduced or stored.
What is partitioning in the Java Stream API? Compare Collectors.partitioningBy with Collectors.groupingBy using examples.
Partitioning divides elements into two groups according to a Boolean predicate. It is performed using Collectors.partitioningBy.
Example:
Map<Boolean, List<Student>> result = students.stream()
.collect(Collectors.partitioningBy(
student -> student.getMarks() >= 40
));
result.get(true)contains students who passed.result.get(false)contains students who failed.
A downstream collector can summarize each partition:
Map<Boolean, Long> counts = students.stream()
.collect(Collectors.partitioningBy(
student -> student.getMarks() >= 40,
Collectors.counting()
));
Partitioning versus grouping:
partitioningByuses aPredicate<T>.- Its keys are Boolean values:
trueandfalse. - It creates at most two logical categories.
groupingByuses a classifier function and can create any number of keys.
For example, pass/fail classification is naturally modeled by partitioning, whereas grouping students by grade, course, or city is better modeled by grouping.
Write a Java program using lambda expressions to perform arithmetic operations. Explain the functional interface and lambda expressions used.
A custom functional interface can represent a binary arithmetic operation.
@FunctionalInterface
interface Operation {
double apply(double a, double b);
}
public class LambdaCalculator {
public static void main(String[] args) {
Operation add = (a, b) -> a + b;
Operation subtract = (a, b) -> a - b;
Operation multiply = (a, b) -> a * b;
Operation divide = (a, b) -> {
if (b == 0) {
throw new ArithmeticException("Division by zero");
}
return a / b;
};
double x = 20;
double y = 5;
System.out.println("Addition: " + add.apply(x, y));
System.out.println("Subtraction: " + subtract.apply(x, y));
System.out.println("Multiplication: " + multiply.apply(x, y));
System.out.println("Division: " + divide.apply(x, y));
}
}
Explanation:
Operationis a functional interface because it contains one abstract method.- Each lambda supplies an implementation of
apply. (a, b)represents the parameters.- The expression after
->represents the implementation. - The division lambda uses a block body because validation and multiple statements are required.
The example demonstrates that behavior can be stored in variables and passed like data.
Develop a stream-based solution that filters employees, extracts data, sorts results, performs reduction, and groups employees. Explain the complete pipeline and mention how it can be parallelized safely.
Assume that Employee contains name, department, salary, and active properties.
Filter active employees, sort by salary, and extract names:
List<String> activeEmployeeNames = employees.stream()
.filter(Employee::isActive)
.sorted(Comparator.comparing(Employee::getSalary).reversed())
.map(Employee::getName)
.collect(Collectors.toList());
Calculate the total salary of active employees:
double totalSalary = employees.stream()
.filter(Employee::isActive)
.map(Employee::getSalary)
.reduce(0.0, Double::sum);
Alternatively, a primitive stream can be used:
double totalSalary = employees.stream()
.filter(Employee::isActive)
.mapToDouble(Employee::getSalary)
.sum();
Group active employees by department:
Map<String, List<Employee>> byDepartment = employees.stream()
.filter(Employee::isActive)
.collect(Collectors.groupingBy(Employee::getDepartment));
Find the highest-paid active employee:
Optional<Employee> highestPaid = employees.stream()
.filter(Employee::isActive)
.max(Comparator.comparing(Employee::getSalary));
Pipeline explanation:
- The collection is the source.
filter,sorted, andmapare intermediate operations.collect,reduce,sum, andmaxare terminal operations.- Processing is lazy until a terminal operation is called.
Optional<Employee>safely represents the possibility that no active employee exists.
The total can be calculated in parallel using employees.parallelStream() because filtering and salary extraction are stateless, while addition is associative. Shared mutable variables must not be updated from the pipeline. Parallelization should be used only when the data set and processing cost are large enough to justify its overhead.
Define the Builder design pattern. Explain its structure, advantages, and use in Java with a suitable example.
The Builder pattern is a creational design pattern used to construct complex objects step by step. It is especially useful when an object has many optional parameters.
Main components:
- Product: The object being created.
- Builder: Provides methods for configuring the product.
- Build method: Creates and returns the final object.
Example:
class Student {
private final String name;
private final int age;
private final String course;
private Student(Builder builder) {
this.name = builder.name;
this.age = builder.age;
this.course = builder.course;
}
static class Builder {
private String name;
private int age;
private String course;
Builder name(String name) {
this.name = name;
return this;
}
Builder age(int age) {
this.age = age;
return this;
}
Builder course(String course) {
this.course = course;
return this;
}
Student build() {
return new Student(this);
}
}
}
The object can be created as follows:
Student student = new Student.Builder()
.name("Asha")
.age(20)
.course("Java")
.build();
Advantages:
- Avoids constructors with many parameters.
- Improves readability through method chaining.
- Supports immutable objects.
- Allows validation before object creation.
- Makes optional attributes easy to manage.
Did this save you a night before the exam?
LPU Notes is free, and it stays free. Ads cover part of the server bill. The rest comes out of a student's own pocket: the domain, the storage, and keeping the site up through the weeks everyone needs it at once.
The payment button didn't load. An ad blocker or a filtered network is the usual reason. to try again.
Nothing here is ever locked, and nothing unlocks. Chip in only if it was worth it. What it pays for →