Index

Lambda Expressions and Method References

Java Syntax

15.1 Lambda Syntax

Lambda expressions are one of the most powerful features introduced in Java 8, enabling developers to write concise, functional-style code. At their core, lambdas provide a simple syntax for defining anonymous functions—functions without a name—that can be passed around as values. This dramatically reduces boilerplate code, especially when working with collections or APIs designed for functional programming.

Basic Syntax of a Lambda Expression

A lambda expression in Java has the general form:

(parameters) -> expression

Examples of Lambda Expressions

No Parameters, Single Expression

Runnable r = () -> System.out.println("Hello, Lambda!");
r.run();

Here, the lambda takes no parameters (()), and its body is a single expression: printing a message. It implements the Runnable interface, which has a single abstract method run() with no parameters.

Single Parameter, Single Expression

Consumer<String> printer = message -> System.out.println(message);
printer.accept("Hello, World!");

Since there is only one parameter, the parentheses around message can be omitted. The lambda simply calls System.out.println with the given message. The Consumer<T> interface represents an operation that accepts a single input argument and returns no result.

Multiple Parameters, Single Expression

BinaryOperator<Integer> adder = (a, b) -> a + b;
System.out.println(adder.apply(5, 3));  // Output: 8

When there are multiple parameters, parentheses are required around the parameter list. This lambda takes two integers and returns their sum.

Multiple Parameters, Block Body

BiPredicate<String, String> startsWith = (str, prefix) -> {
    if (str == null || prefix == null) return false;
    return str.startsWith(prefix);
};

System.out.println(startsWith.test("Lambda", "Lam"));  // Output: true

A block body, enclosed in curly braces {}, allows multiple statements. In this case, we perform a null check before returning whether str starts with prefix.

Parameter Types and Type Inference

While parameter types can be explicitly declared, they are often inferred from the context:

// Explicit types
(BiFunction<Integer, Integer, Integer>) (Integer a, Integer b) -> a * b;

// Inferred types (more common)
(a, b) -> a * b;

In most cases, explicit types are unnecessary and would clutter code.

Returning Values

For single-expression lambdas, the expression's value is implicitly returned. For block bodies, you must use the return keyword:

// Single-expression (implicit return)
Function<String, Integer> length = s -> s.length();

// Block body (explicit return)
Function<String, Integer> lengthBlock = s -> {
    return s.length();
};

Why Use Lambdas?

Before lambdas, implementing behavior often meant creating anonymous classes:

Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello, Lambda!");
    }
};

This is verbose and harder to read. Lambdas express the same logic concisely.

Enabling Functional-Style Programming

Java collections and streams embrace lambdas to support functional programming:

List<String> names = Arrays.asList("Anna", "Bob", "Clara");

// Print all names using lambda
names.forEach(name -> System.out.println(name));

// Filter names starting with 'B'
List<String> filtered = names.stream()
    .filter(name -> name.startsWith("B"))
    .collect(Collectors.toList());

System.out.println(filtered);  // Output: [Bob]

Lambdas let you describe what to do with data, not how to iterate over it.

Click to view full runnable Code

import java.util.*;
import java.util.function.*;
import java.util.stream.Collectors;

public class LambdaExamples {

    public static void main(String[] args) {
        // No Parameters, Single Expression
        Runnable r = () -> System.out.println("Hello, Lambda!");
        r.run();

        // Single Parameter, Single Expression
        Consumer<String> printer = message -> System.out.println(message);
        printer.accept("Hello, World!");

        // Multiple Parameters, Single Expression
        BinaryOperator<Integer> adder = (a, b) -> a + b;
        System.out.println("5 + 3 = " + adder.apply(5, 3));

        // Multiple Parameters, Block Body
        BiPredicate<String, String> startsWith = (str, prefix) -> {
            if (str == null || prefix == null) return false;
            return str.startsWith(prefix);
        };
        System.out.println("Does 'Lambda' start with 'Lam'? " + startsWith.test("Lambda", "Lam"));

        // Parameter Types and Type Inference
        BiFunction<Integer, Integer, Integer> multiplier = (Integer a, Integer b) -> a * b;
        System.out.println("4 * 6 = " + multiplier.apply(4, 6));

        BiFunction<Integer, Integer, Integer> inferredMultiplier = (a, b) -> a * b;
        System.out.println("7 * 2 = " + inferredMultiplier.apply(7, 2));

        // Returning Values
        Function<String, Integer> length = s -> s.length();
        System.out.println("Length of 'Lambda': " + length.apply("Lambda"));

        Function<String, Integer> lengthBlock = s -> {
            return s.length();
        };
        System.out.println("Length of 'Expression': " + lengthBlock.apply("Expression"));

        // Functional-Style Programming with Lambdas
        List<String> names = Arrays.asList("Anna", "Bob", "Clara");

        System.out.println("\nNames:");
        names.forEach(name -> System.out.println("  " + name));

        List<String> filtered = names.stream()
            .filter(name -> name.startsWith("B"))
            .collect(Collectors.toList());

        System.out.println("Names starting with 'B': " + filtered);
    }
}

Reflection on Lambdas

Summary

Final Code Example

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

public class LambdaDemo {
    public static void main(String[] args) {
        List<String> fruits = Arrays.asList("Apple", "Banana", "Cherry", "Date");

        // Print all fruits
        fruits.forEach(fruit -> System.out.println(fruit));

        // Filter fruits with length > 5
        List<String> longFruits = fruits.stream()
            .filter(fruit -> fruit.length() > 5)
            .collect(Collectors.toList());

        System.out.println("Fruits with more than 5 letters: " + longFruits);
    }
}

Here, lambdas make the code concise, readable, and focused on what is done with the data, not how.

Index

15.2 Using Lambdas with Functional Interfaces

Lambda expressions in Java are tightly connected with functional interfaces—interfaces that declare exactly one abstract method. This single abstract method is the "target" for the lambda, meaning the lambda provides the implementation for that method.

What Is a Functional Interface?

A functional interface is an interface with only one abstract method (aside from any default or static methods). This design enables lambdas to be assigned directly to instances of that interface, dramatically simplifying code.

The Java standard library includes many common functional interfaces, such as:

Assigning Lambdas to Functional Interfaces

Let's start with some classic examples:

Runnable

Runnable task = () -> System.out.println("Task running...");
new Thread(task).start();

Here, the lambda () -> System.out.println("Task running...") implements the single abstract method run() from the Runnable interface. This replaces the older verbose anonymous class approach.

ComparatorString

Comparator<String> lengthComparator = (s1, s2) -> s1.length() - s2.length();

List<String> names = Arrays.asList("Anna", "Bob", "Clara");
Collections.sort(names, lengthComparator);

System.out.println(names);  // Output: [Bob, Anna, Clara]

This lambda implements the compare method, which accepts two strings and returns their length difference. It enables flexible, inline sorting logic without boilerplate.

Defining a Custom Functional Interface

You can define your own functional interfaces and assign lambdas to them. For clarity, you should annotate your interface with @FunctionalInterface (optional but recommended), which instructs the compiler to enforce the single-abstract-method rule.

@FunctionalInterface
interface StringProcessor {
    String process(String input);
}

Now, a lambda can implement this interface succinctly:

StringProcessor toUpperCase = s -> s.toUpperCase();
StringProcessor addExclamation = s -> s + "!";

System.out.println(toUpperCase.process("hello"));       // Output: HELLO
System.out.println(addExclamation.process("hello"));    // Output: hello!

This pattern allows you to create flexible, reusable behaviors without writing full class implementations.

Why Must There Be a Single Abstract Method?

The single abstract method ensures that the compiler knows exactly which method the lambda implements. Without this restriction, it would be ambiguous what the lambda corresponds to. This rule is central to lambda compatibility and type inference.

Interfaces with multiple abstract methods cannot be implemented with lambdas but can still be implemented with classes or anonymous classes.

How Lambdas Bring Flexibility to Interface-Based Design

Before lambdas, to provide custom behavior you had to write concrete classes or anonymous inner classes implementing interfaces:

Comparator<String> comp = new Comparator<String>() {
    @Override
    public int compare(String s1, String s2) {
        return s1.length() - s2.length();
    }
};

Lambdas reduce this to a single expression, improving readability and maintainability.

Because lambdas work seamlessly with functional interfaces, Java's API design often exposes behavior points as functional interfaces, empowering developers to pass behavior easily.

Example: Higher-Order Function Using Custom Functional Interface

public class Processor {
    public static String applyOperation(String input, StringProcessor processor) {
        return processor.process(input);
    }

    public static void main(String[] args) {
        String result = applyOperation("java", s -> s + " is awesome");
        System.out.println(result);  // Output: java is awesome
    }
}

Here, the method applyOperation takes a string and a StringProcessor. Passing a lambda directly simplifies client code and enables flexible behavior injection.

Click to view full runnable Code

import java.util.*;

// Custom functional interface
@FunctionalInterface
interface StringProcessor {
    String process(String input);
}

public class LambdaInterfaceDemo {

    // Higher-order function using the custom interface
    public static String applyOperation(String input, StringProcessor processor) {
        return processor.process(input);
    }

    public static void main(String[] args) {

        // Runnable lambda
        Runnable task = () -> System.out.println("Task running...");
        new Thread(task).start();

        // Comparator lambda for sorting strings by length
        Comparator<String> lengthComparator = (s1, s2) -> s1.length() - s2.length();
        List<String> names = Arrays.asList("Anna", "Bob", "Clara");
        Collections.sort(names, lengthComparator);
        System.out.println("Sorted by length: " + names); // [Bob, Anna, Clara]

        // Custom StringProcessor implementations
        StringProcessor toUpperCase = s -> s.toUpperCase();
        StringProcessor addExclamation = s -> s + "!";
        System.out.println(toUpperCase.process("hello"));     // HELLO
        System.out.println(addExclamation.process("hello"));  // hello!

        // Using a lambda as a higher-order function argument
        String result = applyOperation("java", s -> s + " is awesome");
        System.out.println(result);  // java is awesome
    }
}

Functional Interfaces in the Standard Library

The Java API provides a rich set of functional interfaces in the java.util.function package, designed for common use cases:

Interface Description Abstract Method
Predicate<T> Tests a condition on T boolean test(T t)
Function<T,R> Transforms T into R R apply(T t)
Consumer<T> Accepts T but returns nothing void accept(T t)
Supplier<T> Provides a T without input T get()
UnaryOperator<T> Takes and returns the same type T T apply(T t)
BinaryOperator<T> Takes two Ts and returns T T apply(T t1, T t2)

You can easily assign lambdas to any of these, which makes them invaluable building blocks.

Reflection

Index

15.3 Method References (Class::method)

Method references are a concise and readable way to refer to existing methods or constructors in Java, introduced alongside lambdas in Java 8. They act as shorthand for certain lambda expressions by directly pointing to methods, allowing cleaner and more expressive code.

Types of Method References

There are three main types of method references in Java:

  1. Static Method References: ClassName::staticMethod
  2. Instance Method References of a Particular Object: instance::instanceMethod
  3. Constructor References: ClassName::new

Static Method References

Static method references point to a static method in a class. They are useful when the lambda just calls a static method.

Example: Sorting strings by length using a static helper method.

import java.util.Arrays;
import java.util.Comparator;

public class StaticMethodExample {

    // Static method to compare strings by length
    public static int compareByLength(String s1, String s2) {
        return s1.length() - s2.length();
    }

    public static void main(String[] args) {
        String[] names = {"Anna", "Bob", "Clara"};

        // Using lambda
        Arrays.sort(names, (a, b) -> StaticMethodExample.compareByLength(a, b));

        // Using method reference to static method (simpler!)
        Arrays.sort(names, StaticMethodExample::compareByLength);

        System.out.println(Arrays.toString(names));  // Output: [Bob, Anna, Clara]
    }
}

Here, StaticMethodExample::compareByLength replaces the lambda (a, b) -> StaticMethodExample.compareByLength(a, b) without changing behavior.

Instance Method References of a Particular Object

You can reference an instance method of a particular object, often useful when passing existing object behavior.

Example: Using a PrintStream object's println method.

import java.util.Arrays;

public class InstanceMethodExample {
    public static void main(String[] args) {
        String[] messages = {"Hello", "World", "!"};

        // Using lambda to print each message
        Arrays.stream(messages).forEach(s -> System.out.println(s));

        // Using method reference to instance method of System.out
        Arrays.stream(messages).forEach(System.out::println);
    }
}

Here, System.out::println is a method reference to the instance method println on the specific PrintStream object System.out.

Constructor References

Constructor references refer to a constructor and can be used to instantiate new objects, replacing lambdas that call constructors.

Example: Creating Person objects via constructor references.

import java.util.function.Function;

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person: " + name;
    }
}

public class ConstructorReferenceExample {
    public static void main(String[] args) {
        Function<String, Person> personFactory = Person::new; // Constructor reference

        Person p = personFactory.apply("Alice");
        System.out.println(p);  // Output: Person: Alice
    }
}

Here, Person::new is equivalent to the lambda name -> new Person(name) but is cleaner and more intuitive.

How Method References Simplify Lambda Expressions

Method references improve code clarity by removing redundancy. Instead of writing:

x -> someObject.someMethod(x)

You write:

someObject::someMethod

Or instead of:

(a, b) -> SomeClass.staticMethod(a, b)

You write:

SomeClass::staticMethod

And for constructors:

arg -> new ClassName(arg)

becomes:

ClassName::new

This eliminates boilerplate, making your code easier to read and maintain.

When Should You Prefer Method References?

When Not to Use Method References

Summary

Method Reference Type Syntax Example Replaces Lambda
Static method ClassName::method Math::max (a, b) -> Math.max(a, b)
Instance method (object) instance::method System.out::println x -> System.out.println(x)
Constructor ClassName::new Person::new name -> new Person(name)

Method references in Java provide a powerful, succinct way to refer to methods and constructors, enabling cleaner and more expressive functional programming patterns. They improve readability and maintainability, especially when working with APIs designed around functional interfaces.

Index

15.4 Constructor References

Constructor references are a special kind of method reference introduced in Java 8 that allow you to refer directly to a class constructor. Instead of writing a lambda expression that explicitly creates a new object, you can use a concise constructor reference to make the code cleaner and easier to read.

Basic Syntax of Constructor References

The syntax for a constructor reference is:

ClassName::new

This works similarly to other method references but points to a constructor instead of a method.

Using Constructor References with Functional Interfaces

Constructor references are typically used with functional interfaces from java.util.function like:

Example 1: Using Supplier with a No-Arg Constructor

Suppose you have a simple class Car:

class Car {
    public Car() {
        System.out.println("Car created!");
    }
}

You can use a Supplier<Car> to create new instances of Car with a constructor reference:

import java.util.function.Supplier;

public class ConstructorReferenceDemo {
    public static void main(String[] args) {
        Supplier<Car> carSupplier = Car::new;  // Constructor reference

        Car myCar = carSupplier.get();  // Calls Car()
    }
}

Here, Car::new is shorthand for the lambda () -> new Car(). When you call carSupplier.get(), it creates a new Car instance.

Click to view full runnable Code

import java.util.function.Supplier;

public class SupplierConstructorDemo {

    // Simple Car class with a no-arg constructor
    static class Car {
        public Car() {
            System.out.println("Car created!");
        }
    }

    public static void main(String[] args) {
        // Using constructor reference with Supplier
        Supplier<Car> carSupplier = Car::new;

        // Create a new Car using the supplier
        Car myCar = carSupplier.get();  // Output: Car created!
    }
}

Example 2: Using Function with a Parameterized Constructor

If the constructor takes parameters, you can use Function or other functional interfaces with arguments.

class Person {
    private String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person: " + name;
    }
}

Now, use a Function<String, Person> to create new Person objects:

import java.util.function.Function;

public class ConstructorReferenceDemo {
    public static void main(String[] args) {
        Function<String, Person> personFactory = Person::new;

        Person alice = personFactory.apply("Alice");
        Person bob = personFactory.apply("Bob");

        System.out.println(alice);  // Output: Person: Alice
        System.out.println(bob);    // Output: Person: Bob
    }
}

The Person::new constructor reference replaces the lambda name -> new Person(name).

Click to view full runnable Code

import java.util.function.Function;

public class ConstructorReferenceDemo {

    // Simple Person class
    static class Person {
        private String name;

        public Person(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Person: " + name;
        }
    }

    public static void main(String[] args) {
        // Using constructor reference with Function
        Function<String, Person> personFactory = Person::new;

        Person alice = personFactory.apply("Alice");
        Person bob = personFactory.apply("Bob");

        System.out.println(alice);  // Output: Person: Alice
        System.out.println(bob);    // Output: Person: Bob
    }
}

Example 3: Using BiFunction with Two Parameters

For constructors with two parameters, BiFunction fits naturally.

class Point {
    private int x, y;

    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "(" + x + ", " + y + ")";
    }
}

Using a constructor reference with BiFunction:

import java.util.function.BiFunction;

public class ConstructorReferenceDemo {
    public static void main(String[] args) {
        BiFunction<Integer, Integer, Point> pointFactory = Point::new;

        Point p1 = pointFactory.apply(3, 4);
        Point p2 = pointFactory.apply(7, 9);

        System.out.println(p1);  // Output: (3, 4)
        System.out.println(p2);  // Output: (7, 9)
    }
}
Click to view full runnable Code

import java.util.function.BiFunction;

public class ConstructorReferenceDemo {

    // Point class with a two-argument constructor
    static class Point {
        private int x, y;

        public Point(int x, int y) {
            this.x = x;
            this.y = y;
        }

        @Override
        public String toString() {
            return "(" + x + ", " + y + ")";
        }
    }

    public static void main(String[] args) {
        BiFunction<Integer, Integer, Point> pointFactory = Point::new;

        Point p1 = pointFactory.apply(3, 4);
        Point p2 = pointFactory.apply(7, 9);

        System.out.println(p1);  // Output: (3, 4)
        System.out.println(p2);  // Output: (7, 9)
    }
}

Why Use Constructor References?

Real-World Use Case: Creating Objects from Streams

Imagine processing a list of strings and converting them to Person objects:

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

public class ConstructorReferenceDemo {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Anna", "Brian", "Catherine");

        List<Person> people = names.stream()
                                  .map(Person::new)  // Constructor reference
                                  .collect(Collectors.toList());

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

Without constructor references, the map would look like .map(name -> new Person(name)). Using Person::new simplifies the code and emphasizes clarity.

Reflection: Constructor References in Modern Java

Constructor references bring flexibility to interface-driven designs by reducing verbosity and focusing on what matters—the creation of new objects. They fit naturally into functional programming paradigms, such as streams and callbacks, where object instantiation needs to be concise and expressive.

By combining functional interfaces with constructor references, Java lets you build elegant factory-like mechanisms without cluttering your code with anonymous classes or explicit lambdas. This makes your programs easier to maintain and understand.

Summary

Functional Interface Constructor Reference Example Lambda Equivalent
Supplier<T> Car::new () -> new Car()
Function<T, R> Person::new name -> new Person(name)
BiFunction<T,U,R> Point::new (x, y) -> new Point(x, y)

Constructor references provide a powerful and succinct way to instantiate objects, boosting code clarity and promoting a functional style in Java applications.

Next, we will explore how method references and constructor references can be combined with advanced features like streams and collectors for even more expressive and compact Java code.

Index