Core JDK 8–17 Enhancements: Lambda Expressions, Stream API, and LTS Fundamentals

Java Release Cycle and Version Context

Cadence Shifts and LTS Strategy

JDK 8 (March 2014) marked the last feature-driven major release before Oracle shifted to a strict 6-month time-based cadence starting with JDK 9 (September 2017). This change unblocks Java/JVM evolution by decoupling releases from large, high-risk feature bundles. For enterprise stability, Oracle and OpenJDK maintain Long-Term Support (LTS) versions, updated for 3+ years (with extended paid options available). Current recommended LTS releases for production are JDK 8, 11, and 17.

LTS Version GA Date Public Support End Extended Support End
8 2014-03-18 2022-03 2030-12
11 2018-09-25 2026-09 2031-09
17 2021-09-14 2029-09 2036-09
21 2023-09-19 2031-09 2038-09

Oracle JDK vs OpenJDK

Both distributions share identical language and core API behavior, differing primarily in licensing:

  • Oracle JDK: JDK 17+ is free for all users (personal, commercial, production). Earlier versions have usage restrictions for commercial deployments.
  • OpenJDK: GPLv2-licensed, fully free for all use cases since September 2017, maintained by Oracle and the broader OpenJDK community.

JEP: Driving Java Evolution

All new Java features start as a JDK Enhancement Proposal (JEP), a formal specification outlining goals, design, risks, and testing requirements. Approved JEPs are assigned to a specific release and tracked on the OpenJDK website.


JDK 8: The Revolutionary Release

JDK 8 introduced foundational changes that modernized Java development: Lambda expressions, Stream API, functional interfaces, and Otpional. Below is a deep dive into core features.

Lambda Expressions

Lambda expressions are concise anonymous functions that replace verbose anonymous inner classes for single-abstract-method (SAM) interfaces. They follow the syntax (parameters) -> expression/block.

Syntax Examples

import java.util.function.Consumer;
import java.util.function.Supplier;

public class LambdaDemo {
    public static void main(String[] args) {
        // 1. No args, no return
        Runnable greet = () -> System.out.println("Exploring Java 8 Lambdas!");
        greet.run();

        // 2. Single arg (type inferred, parentheses optional)
        Consumer<String> printMessage = message -> System.out.println("Received: " + message);
        printMessage.accept("What's the difference between a joke and a commitment?");

        // 3. Multiple args, block body
        Supplier<Integer> randomIntSupplier = () -> {
            int min = 10;
            int max = 100;
            return (int) (Math.random() * (max - min + 1)) + min;
        };
        System.out.println("Random integer: " + randomIntSupplier.get());
    }
}

Functional Interfaces

A functional interface has exactly one abstract method (SAM), though it can include default/static methods. Use @FunctionalInterface to enforce this constraint at compile time.

Core Built-In Functional Interfaces

JDK 8 ships with java.util.function providing reusable functional interfaces:

Interface SAM Signature Purpose
Consumer<T> void accept(T t) Consume an input without returning value
Supplier<T> T get() Supply an output without input
Function<T, R> R apply(T t) Transform input T to output R
Predicate<T> boolean test(T t) Test a condition on input T

Extended Functional Interfaces

The package also includes specialized variants for primitives (e.g., IntConsumer, DoubleSupplier) and multi-argument operations (e.g., BiFunction<T, U, R>, BiPredicate<T, U>).

Practical Exercises

Exercise 1: Filter and Transform Strings

Create a list of mixed-case strings, filter those longer than 5 characters, convert to uppercase, and print:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class FunctionalExercise1 {
    public static void main(String[] args) {
        List<String> phrases = Arrays.asList(
            "hello lambda",
            "stream api",
            "functional",
            "java",
            "openjdk",
            "code"
        );

        List<String> transformed = phrases.stream()
            .filter(s -> s.length() > 5)
            .map(String::toUpperCase)
            .collect(Collectors.toList());

        transformed.forEach(System.out::println);
    }
}

Exercise 2: Staff Salary Adjustment

Define a Staff class with id, name, and salary. Use BiFunction to create a salary adjustment logic, then apply it to all staff earning less than 12000:

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiFunction;

class Staff {
    private int id;
    private String name;
    private double salary;

    public Staff(int id, String name, double salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }

    // Getters and Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getSalary() { return salary; }
    public void setSalary(double salary) { this.salary = salary; }

    @Override
    public String toString() {
        return String.format("Staff{id=%d, name='%s', salary=%.2f}", id, name, salary);
    }
}

public class FunctionalExercise2 {
    public static void main(String[] args) {
        List<Staff> techGuild = new ArrayList<>();
        techGuild.add(new Staff(101, "Liu Yifei", 11500.0));
        techGuild.add(new Staff(102, "Chen Kun", 14000.0));
        techGuild.add(new Staff(103, "Zhou Xun", 10800.0));
        techGuild.add(new Staff(104, "Deng Chao", 15500.0));

        BiFunction<Staff, Double, Staff> adjustSalary = (staff, percentage) -> {
            staff.setSalary(staff.getSalary() * (1 + percentage / 100));
            return staff;
        };

        techGuild.stream()
            .filter(s -> s.getSalary() < 12000)
            .forEach(s -> adjustSalary.apply(s, 10.0));

        techGuild.forEach(System.out::println);
    }
}

Method, Constructor, and Array References

Method/constructor references simplify lambdas by reusing existing methods/constructors. They are written with the :: operator.

Reference Types

  1. Object::instanceMethod: Reuse an instance method of an existing object
  2. Class::staticMethod: Reuse a static method of a class
  3. Class::instanceMethod: Reuse an instance method where the first lambda parameter is the method caller
  4. Class::new: Constructor reference
  5. Type[]::new: Array constructor reference

Examples

import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;

public class ReferenceDemo {
    public static void main(String[] args) {
        // 1. Class::staticMethod (Function<Double, Long>: Math::round)
        Function<Double, Long> rounder = Math::round;
        System.out.println("Rounded 12.7: " + rounder.apply(12.7));

        // 2. Object::instanceMethod (Consumer<String>: System.out::println)
        List<String> fruits = Arrays.asList("apple", "banana", "cherry");
        fruits.forEach(System.out::println);

        // 3. Class::instanceMethod (BiPredicate<String, String>: String::equals)
        Function<String, String> toLowerCase = String::toLowerCase;
        System.out.println("Lowercase 'HELLO': " + toLowerCase.apply("HELLO"));

        // 4. Class::new (Supplier<Staff>: Staff::new)
        Supplier<Staff> emptyStaffSupplier = Staff::new;
        System.out.println("Empty staff: " + emptyStaffSupplier.get());

        // 5. Type[]::new (Function<Integer, String[]>: String[]::new)
        Function<Integer, String[]> arrayCreator = String[]::new;
        String[] colors = arrayCreator.apply(3);
        colors[0] = "red";
        colors[1] = "green";
        colors[2] = "blue";
        System.out.println("Colors array: " + Arrays.toString(colors));
    }
}

// Add no-arg constructor to Staff for Supplier usage
class Staff {
    private int id;
    private String name;
    private double salary;

    public Staff() {}
    public Staff(int id, String name, double salary) {
        this.id = id;
        this.name = name;
        this.salary = salary;
    }

    // Getters, Setters, toString (same as before)
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public double getSalary() { return salary; }
    public void setSalary(double salary) { this.salary = salary; }

    @Override
    public String toString() {
        return String.format("Staff{id=%d, name='%s', salary=%.2f}", id, name, salary);
    }
}

Stream API

Stream API (java.util.stream) is a declarative, functional approach to processing collections/arrays. It supports parallel execution and lazy evaluation (intermediate operations are only executed when a terminal operation is triggered).

Stream Lifecycle

  1. Create: Generate a stream from a source (collection, array, Stream.of(), etc.)
  2. Intermediate: Transform the stream (returns a new stream; lazy)
  3. Terminal: Produce a result/side effect (closes the stream; eager)

Creation Examples

import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class StreamCreation {
    public static void main(String[] args) {
        // 1. Collection source
        List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9);
        Stream<Integer> listStream = numbers.stream();

        // 2. Array source
        IntStream intArrayStream = Arrays.stream(new int[]{2, 7, 1, 8});

        // 3. Stream.of()
        Stream<String> wordStream = Stream.of("java", "lambda", "stream");

        // 4. Infinite stream (limit to 5 elements)
        Stream<Integer> infiniteEven = Stream.iterate(0, n -> n + 2);
        infiniteEven.limit(5).forEach(System.out::println);
    }
}

Intermediate Operations

Common intermediate operations include:

  • filter(Predicate<T>): Exclude elements
  • distinct(): Remove duplicates (uses equals()/hashCode())
  • limit(long n): Truncate to first n elements
  • skip(long n): Skip first n elements
  • map(Function<T, R>): Transform each element
  • flatMap(Function<T, Stream<R>>): Flatten nested streams
  • sorted()/sorted(Comparator<T>): Sort elemenst

Terminal Operations

Common terminal operations include:

  • forEach(Consumer<T>): Iterate and process each element
  • count(): Return number of elements
  • allMatch()/anyMatch()/noneMatch(): Test conditions
  • findFirst()/findAny(): Retrieve elements
  • max(Comparator<T>)/min(Comparator<T>): Find extreme values
  • reduce(identity, BinaryOperator<T>): Aggregate elements
  • collect(Collector<T, A, R>): Collect elements into a container (e.g., List, Map)

Full Stream Example: Team Member Processing

Process two teams of members:

  1. TechGuild: Keep members with 3-character names, take first 3
  2. DevCircle: Keep members surnamed Zhang, skip first 2
  3. Merge teams, create Member objects, print
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

class Member {
    private String fullName;

    public Member(String fullName) {
        this.fullName = fullName;
    }

    public String getFullName() { return fullName; }
    public void setFullName(String fullName) { this.fullName = fullName; }

    @Override
    public String toString() {
        return "Member{fullName='" + fullName + "'}";
    }
}

public class StreamExercise {
    public static void main(String[] args) {
        List<String> techGuild = new ArrayList<>();
        techGuild.add("Liu Yifei");
        techGuild.add("Chen Kun");
        techGuild.add("Zhou Xun");
        techGuild.add("Deng Chao");
        techGuild.add("Sun Li");
        techGuild.add("Ma");
        techGuild.add("Wu");
        techGuild.add("Zheng");

        List<String> devCircle = new ArrayList<>();
        devCircle.add("Gulnazar");
        devCircle.add("Zhang Wuji");
        devCircle.add("Zhao Liying");
        devCircle.add("Zhang Sanfeng");
        devCircle.add("Nicholas Zhang");
        devCircle.add("Zhang Tianai");
        devCircle.add("Zhang Ergou");

        Stream<String> filteredTech = techGuild.stream()
            .filter(name -> name.split(" ")[0].length() == 3)
            .limit(3);

        Stream<String> filteredDev = devCircle.stream()
            .filter(name -> name.startsWith("Zhang"))
            .skip(2);

        List<Member> mergedMembers = Stream.concat(filteredTech, filteredDev)
            .map(Member::new)
            .collect(Collectors.toList());

        mergedMembers.forEach(System.out::println);
    }
}

JDK 9–17 Quick Highlights

Here’s a snapshot of key non-JDK-8 features from subsequent releases:

JDK 9

  • Project Jigsaw: Modular system for better encapsulation and smaller runtime images
  • JShell: Interactive REPL for quick Java testing
  • Stream API additions: Stream.iterate(predicate, generator) (terminating infinite streams) and Stream.ofNullable(T) (safe single-element/null streams)
  • Private interface methods: Allow reusable helper code in interfaces

JDK 10

  • Local Variable Type Inference (var): Compiler infers type for local variables
  • G1 Parallel Full GC: Improves G1 garbage collector’s full GC performance

JDK 11

  • HTTP Client (Standard): Modern, async-capable HTTP/2 client replaces HttpURLConnection
  • var for Lambda Parameters: Allows type annotations on lambda params
  • Removals: Java EE/CORBA modules, javah tool
  • Epsilon GC: No-op garbage collector for performance testing

JDK 12–16

  • Switch Expressions: Standardized in JDK 14; use -> and yield for concise, expression-like switches
  • Text Blocks: Standardized in JDK 15; multi-line strings without escape characters
  • Records: Standardized in JDK 16; immutable data classes with auto-generated equals(), hashCode(), and toString()
  • Sealed Classes/Interfaces: Previewed in 15/16, restrict which classes can extend/implement them

JDK 17 (LTS)

  • Sealed Classes/Interfaces Standardized: Full production support
  • Pattern Matching for switch (Preview): Test switch cases with patterns
  • Strong Encapsulation by Default: No longer allows illegal access to JDK internal APIs
  • Removals: Experimental AOT/JIT compiler, Applet API deprecated for removal
  • Context-Specific Deserialization Filters: Improve security against deserialization attacks

Tags: java JDK 8 JDK 17 Lambda Expressions Stream API

Posted on Tue, 26 May 2026 18:51:30 +0000 by sharke