Functional Java: It’s good to be lazy

Reading Time: 5 minutes

In Java, we are often tempted towards writing a code which is executed eagerly. There’s a good reason for that – eager code is easy to write and to reason about. But delaying commitments until the last responsible moment is a good agile practice. When executing the code, we can gain performance by being just a little lazy.  In this blog, we will try to cover a few aspects of being lazy in Java.

“Eager is simple, but lazy is efficient. “

Delayed Initialization
In object-oriented programming, we ensure that objects are well constructed before any method calls. We encapsulate, ensure proper state transitions, and preserve the object’s invariants. This works well most of the time, but when parts of an object’s internals are heavyweight resources, we’ll benefit if we postpone creating them. This can speed up object creation, and the program doesn’t expend any effort creating things that may not be used.

The design decision to postpone creating part of an object should not burden the object’s users—it should be seamless. Let’s explore lazy initialization.

Let’s start with a Holder class that needs some heavyweight resources. Creating an instance of this class may take significant time and memory due to the resources it depends on. To address this we can move the heavyweight resources into another class say, Heavy. Then an instance of Holder will keep a reference to an instance of Heavy and route calls to it as appropriate.

public class Heavy {
    public Heavy() { System.out.println("Heavy created"); }
    public String toString() { return "quite heavy"; }
}

This class represents a hypothetical heavyweight resource. In its constructor, we print a message to tell us when it’s created. Now, Let’s create the Holder class.

public class Holder {
    private Supplier<Heavy> heavy = () -> createAndCacheHeavy();
    public Holder() {
        System.out.println("Holder created");
    }
    public Heavy getHeavy() {
        return heavy.get();
    }
}

The indirection we added in this example comes from a Supplier<T> class. This is a functional interface in the JDK, with one abstract method named get() that returns an instance. In other words, this is a factory that keeps on giving without expecting anything as input, kind of like a mother’s love. In the most rudimentary form, a Supplier will return an instance.

The field heavy in this example is an instance of the Supplier<Heavy>. We assign it to a lambda expression and the Java compiler synthesizes from it an instance with the expected get() method. The implementation simply routes the call to a createAndCacheHeavy() method, which we’ll implement soon. The getHeavy() method returns the same thing the Supplier ’s get method returns.

When an instance of Holder is created, as we can see, an instance of Heavy is not created. This design achieves the goal of lazy initialization. We also need a non-draconian solution to thread safety. This is where the createAndCacheHeavy() method comes in.

private synchronized Heavy createAndCacheHeavy() {
    class HeavyFactory implements Supplier<Heavy> {
        private final Heavy heavyInstance = new Heavy();
        public Heavy get() { return heavyInstance; }
    }
    if(!HeavyFactory.class.isInstance(heavy)) {
        heavy = new HeavyFactory();
    }
    return heavy.get();
}

We’ve taken care of the race condition, but since the instance has been created lazily, we no longer need to be so protective. Now that heavy has been replaced with HeavyFactory, subsequent calls to the getHeavy() method will go directly to the HeavyFactory ’s get() method and will not incur any synchronization overhead.

Lazy Evaluations
Java already uses lazy execution when evaluating logical operations. For example, in fn1() || fn2() , the call fn2() is never performed if fn1() returns a boolean true . While Java uses lazy or normal order when evaluating logical operators, it uses an eager or applicative order when evaluating method arguments. All the arguments to methods are fully evaluated before a method is invoked. If the method doesn’t use all of the passed arguments, the program has wasted time and effort executing them. We can use lambda expressions to postpone the execution of select arguments.
Let’s start with a method evaluate() that takes quite a bit of time and resources to run.

public class Evaluation {
    public static boolean evaluate(final int value) {
        System.out.println("evaluating ..." + value);
        simulateTimeConsumingOp(2000);
        return value > 100;
    }
    //...
}

If we know that some arguments may not be used during the execution of a method, we can design the method’s interface to facilitate the delayed execution of some or all arguments. The arguments can be evaluated on demand, like in this lazyEvaluator() method

public static void lazyEvaluator(
        final Supplier<Boolean> input1, final Supplier<Boolean> input2) {
    System.out.println("lazyEvaluator called...");
    System.out.println("accept?: " + (input1.get() && input2.get()));
}

Rather than taking two boolean parameters, the method receives references to the Supplier instances. This JDK functional interface will return an instance, Boolean in this case, in response to a call to its get() method. The logical and operation we use within the lazyEvaluator() method will invoke the get() methods only on demand. If we pass two calls to evaluate() as arguments to the lazyEvaluator() method, the second will be evaluated only if the first call returned a boolean true. Let’s run the method to see this.

lazyEvaluator(() -> evaluate(1), () -> evaluate(2));

Each Supplier makes a call to the evaluate() method, but not until the lazyEvaluator() method is invoked. The evaluation is lazy and optional, determined by the flow of execution within the lazyEvaluator() method. The arguments are not evaluated before we enter the lazyEvaluator() method. The second call to evaluate() was skipped in this version. We saw the cost savings of the lazy evaluation. This technique is quite helpful when we have to evaluate a large number of methods or if method evaluations are time/resource consuming.

Leveraging the Laziness of Streams
The lazy evaluation of Streams is quite powerful. First, we don’t have to do anything special to derive their benefits. In fact, we’ve used them many times already! Second, they can postpone not just one, but a sequence of evaluations so that only the most essential parts of the logic are evaluated, and only when needed. Let’s look at how lazy Streams are and how we benefit from that.
Methods like map() and filter() are intermediate; calls to them return immediately and the lambda expressions provided to them are not evaluated right away. The core behavior of these methods is cached for later execution and no real work is done when they’re called. The cached behavior is run when one of the terminal operations, like findFirst() and reduce(), is called. Not all the cached code is executed, however, and the computation will complete as soon as the desired result is found.
Suppose we’re given a collection of names and are asked to print in all caps the first name that is only three letters long. We can use Stream ’s functional- style methods to achieve this.

public static void main(final String[] args) {
    List<String> names = Arrays.asList("Brad", "Kate", "Kim", "Jack", "Joe",
            "Mike", "Susan", "George", "Robert", "Julia", "Parker", "Benson");
    final String firstNameWith3Letters =
            names.stream()
                    .filter(name -> length(name) == 3)
                    .map(name -> toUpper(name))
                    .findFirst()
                    .get();
    System.out.println(firstNameWith3Letters);
}

We started with a list of names, transformed it into a Stream, filtered out only names that are three letters long, converted the selected names to all caps, and picked the first name from that set. At first glance, it appears the code is doing a lot of work transforming collections, but it’s deceptively lazy.

Conclusion
Efficiency got a boost in Java 8. We can be lazy and postpone the execution of code until we need it. We can delay the initialization of heavyweight resources and easily implement the virtual proxy pattern. Likewise, we can delay the evaluation of method arguments to make the calls more efficient. The real heroes of the improved JDK are the Stream interface and the related classes. We can exploit their lazy behaviors to create infinite collections with just a few lines of code. That means highly expressive, concise code to perform complex operations that we couldn’t even imagine in Java before.

I hope, you have liked my blog. If you have any doubt or any suggestions to make please drop a comment. Thanks!

References:
Functional Programming book
Knoldus-Scala-Spark-Services

Written by 

Vinisha Sharma is a software consultant having more than 6 months of experience. She thrives in a fast pace environment and loves exploring new technologies. She has experience with the languages such as C, C++, Java, Scala and is currently working on Java 8. Her hobbies include sketching and dancing. She believes Optimism and a learning attitude is the key to achieve success in your life

Leave a Reply

Knoldus Pune Careers - Hiring Freshers

Get a head start on your career at Knoldus. Join us!