Welcome to another exciting lesson in our advanced course on Functional Programming with Java! Previously, we explored dynamic type handling with generics. Today, we’ll dive into Lazy Evaluation—a technique that can help you write more efficient and modular code by deferring computation until it's actually needed. Let's get started!
In this lesson, we will cover:
Supplier
interface in Java.By the end of this lesson, you’ll know how to implement lazy evaluation in Java, enabling your programs to become more efficient and modular by computing values only when necessary.
Lazy evaluation is a powerful technique where the computation of a value is deferred until the moment it is actually needed. Instead of computing everything upfront, you delay the execution of the computation until its result is required, which can significantly improve the efficiency and modularity of your programs.
Java's Stream API is a prime example of lazy evaluation in action. When you work with streams, most operations (such as filtering, mapping, and sorting) are intermediate operations that are lazily executed. This means they don't perform any computation until a terminal operation (like collect
, forEach
, or reduce
) is invoked.
Here’s an example to illustrate this:
Java1import java.util.Arrays; 2import java.util.List; 3 4public class Main { 5 public static void main(String[] args) { 6 List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David"); 7 8 // Creating a stream and applying intermediate operations 9 names.stream() 10 .filter(name -> { 11 System.out.println("Filtering: " + name); 12 return name.startsWith("C"); 13 }) 14 .map(name -> { 15 System.out.println("Mapping: " + name); 16 return name.toUpperCase(); 17 }) 18 .forEach(name -> System.out.println("Final output: " + name)); 19 } 20}
In this example:
filter
and map
. The filter
operation filters out names that don't start with "C", and the map
operation converts the remaining names to uppercase.forEach
operation is the terminal operation that triggers the execution of the stream.Key Point: The intermediate operations (filter
and map
) are lazily executed, meaning they are not actually run until the terminal operation (forEach
) is invoked. This lazy execution ensures that operations are only performed when absolutely necessary, optimizing the performance and efficiency of the program.
Let's go through the output sequence to understand how the stream processes each element lazily, executing operations only when necessary:
Stream Creation: The stream is created from the names
list, but no processing occurs until a terminal operation is invoked.
Lazy Intermediate Operations:
filter
and map
are set up but not executed immediately. They wait until the terminal operation (forEach
) is called.Terminal Operation - forEach
: The stream starts processing elements one by one:
"Alice":
filter
checks if "Alice" starts with "C" and outputs: Filtering: Alice
.map
is skipped."Bob":
Filtering: Bob
.map
is skipped."Charlie":
Filtering: Charlie
.map
executes and outputs: Mapping: Charlie
.forEach
outputs: Final output: CHARLIE
."David":
Filtering: David
.map
is skipped.Key Point: The stream processes elements one at a time, executing only the operations needed for each element. This behavior showcases the efficiency of lazy evaluation in streams.
While streams provide a built-in mechanism for lazy evaluation, you can also implement lazy evaluation in your own code using Java’s Supplier
interface. Here’s how:
Java1import java.util.function.Supplier; 2 3public class Main { 4 public static void main(String[] args) { 5 Supplier<Integer> lazyValue = () -> { 6 System.out.println("Computing..."); 7 return 42; 8 }; 9 10 System.out.println("Before using lazyValue"); 11 System.out.println("Value: " + lazyValue.get()); // Outputs "Computing..." then "Value: 42" 12 } 13}
In this example:
Lazy Value Creation: The Supplier
interface is used to define a computation that returns 42
. However, this computation is not executed immediately. Instead, it is deferred until the get()
method is called.
Triggering the Computation: When lazyValue.get()
is called, the deferred computation is executed, printing "Computing..." and returning the value 42
.
This simple example illustrates the core idea behind lazy evaluation: computations are performed only when their results are needed, not before.
Lazy evaluation offers several advantages that make it a valuable technique in functional programming. By deferring computations until their results are needed, you can optimize performance, improve code modularity, and make your applications more responsive and efficient.
By understanding and applying lazy evaluation, you can make your programs not only more efficient but also more adaptable to different contexts and requirements.