Earlier we saw how lazy Streams are. They don’t do any real work until we ask them for the results—kinda like my kids. We can rely on that laziness to easily create a lazy, infinite collection.
When we create a Stream, from a collection or though other means, we quickly receive a façade that has the potential to return an infinite list. But it’s wickedly clever; it returns to us only as many elements as we ask for, produc- ing the elements just in time. We can use that capability to express an infinite collection and generate as many (finite) elements as we like from that list.
Let’s see how.
The Stream interface has a static method iterate() that can create an infinite Stream. It takes two parameters, a seed value to start the collection, and an instance of a UnaryOperator interface, which is the supplier of data in the collection. The Stream the iterate() method returns will postpone creating the elements until we ask for them using a terminating method. To get the first element, for example, we could call the findFirst() method. To get ten elements we could call the limit() method on the Stream, like so: limit(10).
Let’s see how all these ideas shape up in code.
lazy/fpij/Primes.java public class Primes {
private static int primeAfter(final int number) { if(isPrime(number + 1))
return number + 1;
else
return primeAfter(number + 1);
}
public static List<Integer> primes(final int fromNumber, final int count) { return Stream.iterate(primeAfter(fromNumber - 1), Primes::primeAfter)
.limit(count)
.collect(Collectors.<Integer>toList());
} //...
}
We first defined a convenience method, primeAfter(), that returns a prime number that’s after the given number. If the number next to the given number is prime, it is immediately returned; otherwise, the method recursively asks for the prime number that follows. The code that deals with the infinite series is in the primes() method. It’s quite short for what it does; the real complexity is hidden within the iterate() method and the Stream.
The primes() method will create an infinite series of prime numbers, starting with the first prime greater than or equal to the number given as parameter.
In the call to the iterate() method, the first parameter provides the seed for the infinite series. If the given number is prime, it’s used as the seed. Otherwise the first prime after the number is used. The second parameter, a method ref- erence, stands in for a UnaryOperator that takes in a parameter and returns a value. In this example, since we refer to the primeAfter() method, it takes in a number and returns a prime after the number.
The result of the call to the iterate() method is a Stream that caches the UnaryOp- erator it’s given. When we ask for a particular number of elements, and only then, the Stream will feed the current element (the given seed value is used as the first element) to the cached UnaryOperator to get the next element, and then feed that element back to the UnaryOperator to get the subsequent element. This sequence will repeat as many times as necessary to get the number of elements we asked for, as we see in the next figure.
Stream limit(5)
UnaryOperator
apply--- call primeAfter(number) feed current
element
get next element
Execute only on demand
Figure 8—Creating an infinite Stream of prime numbers
Let’s call the primes() method first to get ten primes starting at 1, and then five primes starting at 100.
lazy/fpij/Primes.java
System.out.println("10 primes from 1: " + primes(1, 10));
System.out.println("5 primes from 100: " + primes(100, 5));
Chapter 6. Being Lazy
•
118The primes() method creates a Stream of an infinite collection of primes, starting at the given input. To get a particular number of elements from the collection we call the limit() method. Then we convert the returned collection of elements into a list and print it. This call to collect() triggers the evaluation of the sequence. The method limit() is also an intermediate operation that lazily notes the number of elements needed for later evaluation! Let’s look at this code’s output.
10 primes from 1: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
5 primes from 100: [101, 103, 107, 109, 113]
The code produced two series of primes, one starting at 1 and the other starting at 100. These were extracted from the infinite series we created so succinctly within the primes() method, thanks to the laziness of Streams and the power of lambda expressions/method references.
We saw how lambda expressions and the Stream implementations work in tandem to make the execution quite efficient. While lambda expressions and method references make code elegant, expressive, and concise, the real per- formance gains in Java 8 applications will come from Streams. Lambda expressions are the gateway drug to Java 8, but Streams are the real addiction
—be ready to get hooked on them as you develop Java 8 applications.
We got quite a lot done within just a few lines of code; it’s perfectly fine to take a few minutes to admire the power of lambda expressions, functional interfaces, and the efficiency of Streams. In the next chapter, we’re ready to take the use of lambda expressions up another notch to make recursions more efficient.
Recap
Efficiency got a boost in Java 8; we can be lazy and postpone execution of code until we need it. We can delay initialization of heavyweight resources and easily implement the virtual proxy pattern. Likewise, we can delay evalu- ation 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.
In the next chapter we’ll look at the roles lambda expressions play in optimiz- ing recursions.
CHAPTER 7
Divide each difficulty into as many parts as is feasible and necessary to resolve it.
➤ René Descartes
Optimizing Recursions
Recursion is a powerful and charming way to solve problems. It’s highly expressive—using recursion we can provide a solution to a problem by applying the same solution to its subproblems, an approach known as divide and conquer. Various applications employ recursion, such as for finding the shortest distances on a map, computing minimum cost or maximum profit, or reducing waste.
Most languages in use today support recursion. Unfortunately, problems that truly benefit from recursion tend to be fairly large and a simple implementation will quickly result in a stack overflow. In this chapter we’ll look at the tail-call optimization (TCO) technique to make recursions feasible for large inputs.
Then we’ll look into problems that can be expressed using highly recursive overlapping solutions and examine how to make them blazingly fast using the memoization technique.