CSE406Semester 7 • 4 CreditsElective Subjects

Advanced Java Programming

This course covers advanced Java programming, including collections streams, lambda operations, GUI and event handling, JDBC database applications, file I/O, JSP, date/time APIs, localization, multithreading, and concurrency. Students develop Java applications, connect applications to databases using JDBC, and use lambda expressions and concurrency features.

6 Units • Notes • Model Answers • MCQs
Select Course Unit (6 Units)
Active: Unit 1
Unit 1:Unit 1: Collections, Streams, Filters, and Lambdas
7 min read•High-Yield Unit Guide

Unit 1: Collections, Streams, Filters, and Lambdas

Unit 1 Exam Focus & Core Takeaways

Review the core definitions, standard architectures, and syntax patterns below. Pay special attention to sections highlighted with bullet points for your mid-term and end-term subjective questions.

Unit 1: Collections, Streams, Filters, and Lambdas

1. Object Construction, Immutability, and the Builder Pattern

Modern enterprise Java applications prioritize immutable, thread-safe data structures to eliminate concurrency hazards and side-effects.

Object Creation Paradigms

  • Telescoping Constructor Anti-Pattern: Providing overloaded constructors with increasing parameter counts (2,3,4,…2, 3, 4, \dots parameters). This leads to poor readability, error-prone parameter ordering, and difficulties in scaling optional fields.
  • JavaBeans Pattern (Setters): Creating an empty object and calling mutator methods (setName(), setAge()).
    • Severe Flaws: Leaves objects in an inconsistent state during multi-step construction; prevents constructing strictly immutable objects.
  • The Builder Design Pattern (GoF / Joshua Bloch): Separates the construction of a complex object from its representation, allowing the same construction process to create various representations safely.
java
public class UserProfile {
    private final String username;     // Required
    private final String email;        // Required
    private final int age;             // Optional
    private final boolean premium;     // Optional

    private UserProfile(Builder builder) {
        this.username = builder.username;
        this.email = builder.email;
        this.age = builder.age;
        this.premium = builder.premium;
    }

    public static class Builder {
        private final String username;
        private final String email;
        private int age = 18;
        private boolean premium = false;

        public Builder(String username, String email) {
            this.username = username;
            this.email = email;
        }

        public Builder age(int age) {
            if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
            this.age = age;
            return this;
        }

        public Builder premium(boolean premium) {
            this.premium = premium;
            return this;
        }

        public UserProfile build() {
            return new UserProfile(this);
        }
    }
}

Immutable Collection Factories (Java 9+)

Modern Java provides convenient static factory methods returning unmodifiable, compact collection implementations:

  • List.of("A", "B", "C"): Returns an immutable, null-hostile list (throws NullPointerException if null is inserted, UnsupportedOperationException if modified).
  • Set.of(1, 2, 3): Rejects duplicates at instantiation time (IllegalArgumentException).
  • Map.of("k1", "v1", "k2", "v2") and Map.ofEntries(): Immutable key-value mappings.

2. Lambdas, Closures, and Functional Interfaces

Introduced in Java 8, lambda expressions treat functionality as method arguments, enabling functional programming paradigms.

The @FunctionalInterface Annotation

A Functional Interface (Single Abstract Method - SAM interface) contains exactly one abstract method. It may contain any number of default or static methods.

  • The @FunctionalInterface annotation informs the compiler to enforce the single-abstract-method constraint.

Standard Functional Interfaces (java.util.function)

Java standardizes core functional interfaces to eliminate boilerplate custom SAM definitions:

InterfaceSignatureCore MethodPrimary Use Case
Predicate<T>T -> booleanboolean test(T t)Filtering, matching, validation
Consumer<T>T -> voidvoid accept(T t)Side-effects, logging, printing
Supplier<T>() -> TT get()Factory instantiation, lazy generation
Function<T, R>T -> RR apply(T t)Data transformation, field extraction
UnaryOperator<T>T -> TT apply(T t)In-place transformations
BinaryOperator<T>(T, T) -> TT apply(T t1, T t2)Reductions, mathematical aggregations
BiPredicate<T, U>(T, U) -> booleanboolean test(T t, U u)Two-argument conditional evaluation
BiFunction<T, U, R>(T, U) -> RR apply(T t, U u)Combining two distinct inputs

Variable Capture and Closures

Lambdas can reference local variables declared outside the lambda body only if those variables are effectively final:

  • A variable is effectively final if its value is never modified after initialization.
  • Reason: Java captures local variables by value (copying), not by reference, to avoid race conditions and stack lifecycle hazards.

Method References

Method references provide shorthand syntax for lambdas that simply invoke an existing named method (:: operator):

  1. Static Method Reference: ClassName::staticMethod ≡\equiv (args) -> ClassName.staticMethod(args) (e.g., Math::max).
  2. Instance Method of an Arbitrary Object: ClassName::instanceMethod ≡\equiv (obj, args) -> obj.instanceMethod(args) (e.g., String::toUpperCase).
  3. Instance Method of a Specific Object: instanceRef::instanceMethod ≡\equiv (args) -> instanceRef.instanceMethod(args) (e.g., System.out::println).
  4. Constructor Reference: ClassName::new ≡\equiv (args) -> new ClassName(args) (e.g., ArrayList::new).

3. The Java Streams API: Architecture and Pipelines

A Stream is a sequence of elements supporting sequential and parallel aggregate operations.

Key Distinctions: Collections vs. Streams

  • Data Storage: A Collection is an in-memory data structure holding all its elements. A Stream is a computational pipeline that does not store data; it conveys elements from a source through computational stages.
  • Consumption: Collections can be traversed repeatedly. A Stream is traversed only once; calling a terminal operation consumes the stream, and subsequent operations throw IllegalStateException.
  • Iteration: Collections use external iteration (for, Iterator), where client code controls traversal. Streams use internal iteration, allowing the runtime to optimize traversal, pipelining, and parallel execution.
CODE
+------------------------------------------------------------------------+
|                        STREAM PIPELINE ANATOMY                         |
|                                                                        |
|  [ Data Source ]  ──► List, Set, Array, I/O Channel, Generator         |
|         │                                                              |
|         ▼                                                              |
|  [ Intermediate ] ──► filter(), map(), flatMap(), sorted(), distinct() |
|  [  Operations  ]     (Lazy, return Stream<T>, chained sequentially)   |
|         │                                                              |
|         ▼                                                              |
|  [   Terminal   ] ──► collect(), forEach(), reduce(), count(), anyMatch()|
|  [  Operation   ]     (Eager, triggers execution, produces result/void)|
+------------------------------------------------------------------------+

Lazy Evaluation and Short-Circuiting

  • Laziness: Intermediate operations are not executed when invoked. They merely construct a recipe (a pipeline of stages). Data processing begins only when a terminal operation is executed.
  • Loop Fusion: Intermediate operations are fused together. An element traverses the entire pipeline of filters and mappers in a single pass, rather than generating intermediate collection buffers.
  • Short-Circuiting Operations:
    • Intermediate: limit(n), takeWhile(predicate) truncate processing once conditions are met.
    • Terminal: findFirst(), findAny(), anyMatch(), allMatch(), noneMatch() terminate traversal as soon as the result is determined without evaluating remaining elements.

4. Intermediate Operations: Filtering and Transformation

1. filter(Predicate<T>)

Evaluates a boolean condition against each stream element, retaining only matching elements.

2. map(Function<T, R>)

Transforms each element of type T into a corresponding element of type R in a 1:11:1 ratio.

java
List<String> names = employees.stream()
                              .map(Employee::getName)
                              .toList();

3. flatMap(Function<T, Stream<R>>)

Flattens nested data structures (1:N1:N mapping). Transforms each element into a stream of values and flattens multiple generated streams into a single unified stream:

java
List<List<String>> nested = List.of(List.of("A", "B"), List.of("C", "D"));
List<String> flat = nested.stream()
                          .flatMap(Collection::stream)
                          .toList(); // ["A", "B", "C", "D"]

4. distinct(), sorted(), peek(), skip(), limit()

  • distinct(): Eliminates duplicates based on equals() and hashCode(). Stateful operation.
  • sorted(): Sorts elements according to natural order or a custom Comparator. Stateful operation.
  • peek(Consumer<T>): Inspects elements as they flow through the pipeline (primarily for debugging).
  • takeWhile(Predicate<T>) / dropWhile(Predicate<T>) (Java 9): Evaluates ordered streams up to the first non-matching element.

5. Terminal Operations and Reduction

Immutable Reductions (reduce())

The reduce() operation combines all stream elements into a single summary value through repeated application of an associative accumulator function.

  1. Two-Argument Form:
    T=reduce(T identity,BinaryOperator<T> accumulator)T = \text{reduce}(T \text{ identity}, \text{BinaryOperator}<T> \text{ accumulator})
    java
    int sum = numbers.stream().reduce(0, Integer::sum);
    
  2. One-Argument Form:
    Optional<T>=reduce(BinaryOperator<T> accumulator)\text{Optional}<T> = \text{reduce}(\text{BinaryOperator}<T> \text{ accumulator})
  3. Three-Argument Form (Parallel Pipelines):
    U=reduce(U identity,BiFunction<U,T,U> accumulator,BinaryOperator<U> combiner)U = \text{reduce}(U \text{ identity}, \text{BiFunction}<U, T, U> \text{ accumulator}, \text{BinaryOperator}<U> \text{ combiner})

Mutable Reductions (collect())

Accumulates elements into a mutable result container (such as a List, Set, or StringBuilder) using the Collector interface:

  • Supplier: () -> R (Creates a fresh result container).
  • Accumulator: (R, T) -> void (Folds an element into the container).
  • Combiner: (R, R) -> R (Merges two result containers in parallel execution).

Standard Collectors (java.util.stream.Collectors)

  • Collectors.toList() / Collectors.toSet() / Collectors.toCollection(TreeSet::new)
  • Collectors.joining(delimiter, prefix, suffix)
  • Collectors.groupingBy(): Groups elements by a classifier function into a Map<K, List<T>>.
  • Downstream Collectors:
    java
    // Group employees by department and count them
    Map<Department, Long> deptCounts = employees.stream()
        .collect(Collectors.groupingBy(Employee::getDepartment, Collectors.counting()));
    
  • Collectors.partitioningBy(Predicate): Partitions elements into two groups based on a boolean condition: Map<Boolean, List<T>>.

6. The Optional<T> API

Introduced in Java 8, Optional<T> is a container object that may or may not contain a non-null value, designed to represent the absence of a value without resorting to error-prone null references.

Instantiation

  • Optional.of(value): Wraps a non-null value (throws NullPointerException if null).
  • Optional.ofNullable(value): Wraps a value if non-null, or returns Optional.empty() if null.
  • Optional.empty(): Explicit empty container.

Modern Idiomatic Methods

  • isPresent() / isEmpty() (Java 11)
  • ifPresent(Consumer<T>): Executes action if value exists.
  • ifPresentOrElse(Consumer<T>, Runnable) (Java 9): Executes consumer if present, runnable if empty.
  • map(Function<T, U>) / flatMap(Function<T, Optional<U>>): Functional transformations.
  • filter(Predicate<T>): Discards value if predicate fails.
  • Extraction:
    • orElse(defaultValue): Eager fallback value evaluation.
    • orElseGet(Supplier<T>): Lazy fallback evaluation (executes supplier only when empty).
    • orElseThrow(): Throws NoSuchElementException if empty.

7. Parallel Streams and ForkJoinPool

Parallel streams split data across multiple CPU threads automatically using the common ForkJoinPool.

Execution Model: Divide-and-Conquer

  1. Split (Decomposition): The source Spliterator recursively partitions data into chunks.
  2. Compute: Independent worker threads process sub-streams concurrently.
  3. Combine (Reduction): The combiner merges intermediate partial results.

Performance Hazards and Best Practices

  • Shared Mutable State: Lambdas must be strictly stateless and side-effect-free. Mutating shared data causes race conditions and corrupted results.
  • Data Source Splittability:
    • Excellent: ArrayList, primitive arrays, IntStream.range (random-access, known size, efficient splitting).
    • Poor: LinkedList, I/O streams, Stream.iterate (sequential pointer chasing, high splitting overhead).
  • NQ Model: Parallelism is beneficial when N×Q>10,000N \times Q > 10{,}000, where NN is the element count and QQ is computational work per element.