Index

Java 8 Functional Programming Basics

Java Functional Programming

2.1 Lambda Expressions: Syntax and Usage

Lambda expressions, introduced in Java 8, provide a concise way to represent anonymous functions—blocks of code that can be passed around and executed later. They simplify what used to require verbose anonymous inner classes, making your code cleaner and easier to read.

Basic Syntax

A lambda expression consists of three parts:

General form:

(parameters) -> expression

or

(parameters) -> { statements; }

Examples of Syntax Variations

  1. No parameters If the lambda takes no arguments, use empty parentheses:
Runnable r = () -> System.out.println("Hello, world!");
r.run();

This prints Hello, world!. Here, the lambda implements the Runnable interface's single method run().

  1. Single parameter without parentheses If there is exactly one parameter, parentheses can be omitted:
Consumer<String> printer = name -> System.out.println("Name: " + name);
printer.accept("Alice");

This prints Name: Alice.

  1. Multiple parameters If there are multiple parameters, parentheses are required:
BiFunction<Integer, Integer, Integer> adder = (a, b) -> a + b;
System.out.println(adder.apply(3, 5)); // Outputs 8
  1. Block body with multiple statements When the body has more than one statement, use braces {} and explicit return if needed:
Function<Integer, String> converter = num -> {
    int doubled = num * 2;
    return "Result: " + doubled;
};
System.out.println(converter.apply(4)); // Outputs "Result: 8"

Type Inference

Java can usually infer parameter types from context, so explicit types are optional:

Predicate<String> isEmpty = s -> s.isEmpty();

But you can specify types if you prefer:

Predicate<String> isEmpty = (String s) -> s.isEmpty();

Variable Capture: Effectively Final

Lambdas can access variables from their enclosing scope, but only if those variables are effectively final—meaning their value does not change after assignment. This restriction prevents unexpected side effects and keeps lambdas predictable.

Example:

int factor = 2;
Function<Integer, Integer> multiplier = x -> x * factor;
System.out.println(multiplier.apply(5)); // Outputs 10

Here, factor is captured by the lambda. Trying to modify factor after the lambda is defined will cause a compilation error.

By using lambda expressions, you transform bulky anonymous classes into clean, expressive code blocks. They are a fundamental building block of Java's functional programming capabilities.

Index

2.2 Functional Interfaces: Function, Predicate, Consumer, Supplier

Java 8 introduced a set of standard functional interfaces in the java.util.function package. These interfaces define single abstract methods and serve as targets for lambda expressions and method references. The four core interfaces—Function, Predicate, Consumer, and Supplier—cover the most common functional programming use cases: transforming data, filtering, performing side effects, and supplying values.

FunctionT, R

A Function takes an input of type T and produces a result of type R. It represents a transformation or mapping operation.

Example: Mapping Strings to their lengths

Function<String, Integer> lengthFunction = s -> s.length();
List<String> names = List.of("Alice", "Bob", "Charlie");

List<Integer> lengths = names.stream()
                            .map(lengthFunction)
                            .toList();

System.out.println(lengths); // Output: [5, 3, 7]

Here, the Function maps each string to its length.

PredicateT

A Predicate tests a condition on an input of type T and returns a boolean indicating if the input matches the condition.

Example: Filtering even numbers

Predicate<Integer> isEven = num -> num % 2 == 0;
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6);

List<Integer> evens = numbers.stream()
                             .filter(isEven)
                             .toList();

System.out.println(evens); // Output: [2, 4, 6]

This Predicate filters the stream to include only even numbers.

ConsumerT

A Consumer performs an action on an input of type T but does not return a result. It’s typically used for side effects like printing or modifying external state.

Example: Printing each element

Consumer<String> printer = s -> System.out.println("Name: " + s);
List<String> fruits = List.of("Apple", "Banana", "Cherry");

fruits.forEach(printer);

This prints each fruit name prefixed by "Name:".

SupplierT

A Supplier produces or supplies a value of type T without taking any input. It’s useful for lazy value generation or deferred computation.

Example: Supplying a random number

Supplier<Double> randomSupplier = () -> Math.random();

System.out.println("Random number: " + randomSupplier.get());
System.out.println("Random number: " + randomSupplier.get());

Each call to get() generates a new random number.

Real-World Usage Scenario

Imagine processing a list of orders. You could use:

Together, these interfaces let you build expressive, modular, and reusable pipelines that handle data transformation, filtering, side effects, and deferred generation elegantly.

These core functional interfaces are foundational to Java’s functional programming style and integrate seamlessly with streams and other APIs. Mastering their use will empower you to write concise and powerful code.

Index

2.3 Method References and Constructor References

Method references are a shorthand syntax in Java for writing lambda expressions that simply call an existing method. They improve code readability by eliminating boilerplate when the lambda's body is just a method call. Introduced in Java 8, method references are closely related to lambdas and can be used wherever a lambda expression is expected.

Syntax Overview

The general syntax of a method reference is:

ClassName::methodName

This replaces a lambda like x -> ClassName.methodName(x) when the method call matches the functional interface's signature.

There are four main types of method references:

Static Method Reference

Syntax: ClassName::staticMethod

Equivalent Lambda: x -> ClassName.staticMethod(x)

Example:

Function<String, Integer> parseInt = Integer::parseInt;
System.out.println(parseInt.apply("42")); // Output: 42

This is equivalent to:

Function<String, Integer> parseInt = s -> Integer.parseInt(s);

Instance Method of a Particular Object

Syntax: instance::instanceMethod

Example:

Consumer<String> printer = System.out::println;
printer.accept("Hello, method reference!"); // Output: Hello, method reference!

Equivalent lambda:

Consumer<String> printer = s -> System.out.println(s);

Instance Method of an Arbitrary Object of a Particular Type

Syntax: ClassName::instanceMethod

Used when the instance is provided at runtime.

Example:

List<String> names = List.of("bob", "alice", "carol");
names.sort(String::compareToIgnoreCase);
System.out.println(names); // Output: [alice, bob, carol]

Equivalent lambda:

names.sort((a, b) -> a.compareToIgnoreCase(b));

Constructor Reference

Syntax: ClassName::new

Used when you want to instantiate a class using a lambda.

Example:

Supplier<List<String>> listSupplier = ArrayList::new;
List<String> myList = listSupplier.get();
myList.add("Item");
System.out.println(myList); // Output: [Item]

Equivalent lambda:

Supplier<List<String>> listSupplier = () -> new ArrayList<>();

By replacing simple lambdas with method references, you can make your code cleaner and easier to understand. As you use more functional constructs in Java, method references will become a natural tool for writing expressive and concise code.

Index

2.4 Example: Simple Calculator Using Lambdas

In this section, we’ll build a simple calculator using Java's functional programming features. We'll use lambda expressions, functional interfaces, and method references to model arithmetic operations like addition, subtraction, multiplication, and division. The calculator will allow dynamic selection of operations by passing functions as parameters.

This approach demonstrates how functional programming promotes flexibility and clean separation of logic by treating operations as first-class functions.

Here is a complete, runnable Java program:

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.function.BiFunction;

public class LambdaCalculator {

    public static void main(String[] args) {
        // Define arithmetic operations using lambdas and method references
        Map<String, BiFunction<Double, Double, Double>> operations = new HashMap<>();

        // Using lambdas
        operations.put("+", (a, b) -> a + b);
        operations.put("-", (a, b) -> a - b);
        // Using method reference for multiplication
        operations.put("*", LambdaCalculator::multiply);
        // Division with lambda and error check
        operations.put("/", (a, b) -> {
            if (b == 0) {
                throw new ArithmeticException("Cannot divide by zero.");
            }
            return a / b;
        });

        Scanner scanner = new Scanner(System.in);
        System.out.print("Enter first number: ");
        double x = scanner.nextDouble();

        System.out.print("Enter operator (+, -, *, /): ");
        String op = scanner.next();

        System.out.print("Enter second number: ");
        double y = scanner.nextDouble();

        BiFunction<Double, Double, Double> operation = operations.get(op);

        if (operation != null) {
            try {
                double result = operation.apply(x, y);
                System.out.println("Result: " + result);
            } catch (ArithmeticException ex) {
                System.out.println("Error: " + ex.getMessage());
            }
        } else {
            System.out.println("Unsupported operation: " + op);
        }

        scanner.close();
    }

    // Method to use as a method reference
    public static double multiply(double a, double b) {
        return a * b;
    }
}

Highlights:

This example shows how lambdas and functional interfaces can simplify even classic programming tasks like building a calculator.

Index