Skip to main content

Command Palette

Search for a command to run...

Suppliers and Lazy Evaluation

Updated
4 min read
Suppliers and Lazy Evaluation
J
Software Engineer for quite a few years. From C programmer to Java web programmer. Very interested in automated testing and functional programming, operating systems, embedded programming.

Introduction

In Java, a Supplier is a functional interface introduced in Java 8 as part of the functional programming API, as explained in my previous article.

It is an interface that takes no arguments and produces a result of a specific type. Its abstract method is called get().

@FunctionalInterface 
public interface Supplier<T> { 
    T get();
}

Usage:

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

But when is providing a value without taking any input useful?

Factory methods

public interface Shape {
   void draw();
}
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a circle");
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("Drawing a rectangle");
    }
}

Factory pattern using Supplier:

final static Map<String, Supplier<Shape>> map = new HashMap<>();
static {
    map.put("circle", () -> new Circle());  
    map.put("rectangle", () -> new Rectangle());
}

The whole factory class is:

public class ShapeFactory {
    final static Map<String, Supplier<Shape>> map = new HashMap<>();
    static {
        map.put("circle", Circle::new);
        map.put("rectangle", Rectangle::new);
    }

    public Shape getShape(String shapeType) {
        Supplier<Shape> shape = map.get(shapeType.toLowerCase());
        if (shape != null) {
            return shape.get();
        }
        throw new IllegalArgumentException("Invalid shape type: " + shapeType.toLowerCase());
    }
}

The drawback of this technique is that it does not scale well if the factory method getShape needs to take multiple arguments to pass on to the Shape constructors.

Stream API

The generate and iterate methods in the Stream API receive a Supplier as parameter:

Supplier<Double> randomSupplier = () -> Math.random();
Stream.generate(randomSupplier)
    .limit(10)
    .forEach(System.out::println);

It can be simplified using method references:

Stream.generate(Math::random)
    .limit(10)
    .forEach(System.out::println);

Lazy initialization

It is a design pattern where the creation of an object or computation of a value is deferred until it is needed. This technique improves performance and reduces memory usage, because it avoids unnecessary computations.

// Eager initialization
String eagerValue = "Eager Initialization";

// Lazy initialization using Supplier
Supplier<String> lazyValueSupplier = () -> "Lazy Initialization";

// Value is not created until get() is called
System.out.println("Before accessing lazy value");
System.out.println(lazyValueSupplier.get()); // Output: Lazy Initialization

The example is useless, but let's imagine a heavy computation task instead of a single String. For this case, it is useful to ensure the object is created only once (memoization).

Let's create a custom wrapper class for this:

public class Lazy<T> {
    private final Supplier<T> supplier;
    private T value;

    public Lazy(Supplier<T> supplier) {
        this.supplier = supplier;
    }

    public T get() {
        if (value == null) {
            value = supplier.get();
        }
        return value;
    }
}

It is called like that:

Lazy<String> lazyValue = new Lazy<>(() -> "Computed Value");

System.out.println("Before accessing lazy value");
System.out.println(lazyValue.get()); // Output: Computed Value

The syntax for creating a Lazy object in Java is verbose, since it's not a built-in class. In other JVM languages a variable can be declared lazy. For example, in Scala:

lazy val lazyValue = "Computed Value"

Generating an infinite list

This is an application of lazy evaluation. It's a quite common case.

 // Generate an infinite stream of random numbers
Stream<Double> infiniteStream = Stream.generate(Math::random);
// Take the first 5 elements and print them
infiniteStream.limit(5).forEach(System.out::println);
// Generate an infinite stream of integers starting from 1
Stream<Integer> infiniteStream = Stream.iterate(1, n -> n + 1);
// Take the first 5 elements and print them
infiniteStream.limit(5).forEach(System.out::println);

Scala's native support for laziness makes it more idiomatic for such tasks:

val infiniteList = LazyList.continually(scala.util.Random.nextDouble())
// Take the first 5 elements and print them
println(infiniteList.take(5).toList)

Lazy Evaluation in Kotlin

// Generate an infinite sequence of integers starting from 1
val infiniteSequence = generateSequence(1) { it + 1 }
// Take the first 5 elements and print them
println(infiniteSequence.take(5).toList())

In general, the sequence function is a builder needed for creating sequences lazily.

val randomNumbers = sequence {
    while (true) {
        yield(Random.nextDouble())
    }
}

// Take the first 5 random numbers and print them
println(randomNumbers.take(5).toList())

Custom extension functions can be defined to create infinite sequences for specific use cases. This allows Kotlin to mimic the functional Scala syntax:

fun Int.toInfiniteSequence(): Sequence<Int> = generateSequence(this) { it + 1 }

fun main() {
    // Create an infinite sequence starting from 10
    val infiniteSequence = 10.toInfiniteSequence()

    // Take the first 5 elements and print them
    println(infiniteSequence.take(5).toList()) 
}

Summary

Suppliers are an important part of functional programming in Java.

They have two main use cases:

  • To encapsulate the logic of value generation.

  • To allow separating the definition of how a value is generated from when that value is needed. This technique is called lazy evaluation and it's more easily implemented in Scala and Kotlin.

56 views