For online information and to order this and other Manning books, visit www.manning.com. Recognizing the importance of preservation, it is Manning's policy to print the books we publish on acid-free paper, and we make every effort to do so.
Fundamentals
Java 8: why should you care?
Why is Java still changing?
- Java’s place in the programming language ecosystem
- Stream processing
- Passing code to methods with behavior parameterization
- Parallelism and shared mutable data
- Java needs to evolve
We introduce them in a slightly different order than the rest of the book to allow for a Unix-based analogy and to expose the "here's why this is needed" dependencies in Java 8's new parallelism for multicore. We now introduce the new concepts in Java 8 one by one, highlighting in more detail how the chapters cover these concepts.
Functions in Java
- Methods and lambdas as first-class citizens
Values, as mentioned earlier, are first-class citizens of Java, but various other Java concepts, such as methods and classes, exemplify second-class citizens. Experiments in other languages such as Scala and Groovy found that allowing concepts such as methods to be used as first-class values made programming easier by adding to the set of tools available to programmers.
Lambdas—anonymous functions
- Passing code: an example
- From passing methods to lambdas
- Streams
- Multithreading is difficult
- Default methods
- Other good ideas from functional programming
- Summary
- Coping with changing requirements
- First attempt: filtering green apples
- Second attempt: parameterizing the color
- Third attempt: filtering with every attribute you can think of
- Behavior parameterization
- Fourth attempt: filtering by abstract criteria
Try to imagine how you could code this algorithm in Java (even for an index smaller than Google's, you would have to use all the cores in your computer). Here, Scala's syntax expr match corresponds to Java's switch (expr); don't worry about this code for now—you'll read more about pattern matching in Chapter 14.
Passing code/behavior
Multiple behaviors, one parameter
- Tackling verbosity
- Anonymous classes
- Fifth attempt: using an anonymous class
- Sixth attempt: using a lambda expression
- Seventh attempt: abstracting over List type
- Real-world examples
- Sorting with a Comparator
- Executing a block of code with Runnable
- GUI event handling
- Summary
- Lambda expressions
- Lambdas in a nutshell
- Where and how to use lambdas
For example, by using a lambda expression, you can create a custom comparator object in a more concise way. We explain in the next section exactly where and how you can use lambda expressions.
Note
Function descriptor
Predicate
Putting lambdas into practice: the execute around pattern
- Step 1: Remember behavior parameterization
- Step 2: Use a functional interface to pass behaviors
- Step 3: Execute a behavior!
- Step 4: Pass lambdas
You can now reuse the processFile method and process files in different ways by passing different lambdas. So far, we've shown how you can use functional interfaces to pass lambdas.
Using functional interfaces
- Predicate
- Consumer
- Function
The interface java.util.function.Consumer
Primitive specializations
Type checking, type inference, and restrictions
- Type checking
- Same lambda, different functional interfaces
- Type inference
- Using local variables
The type expected for the lambda expression within the context (for example, a method parameter it is passed to or a local variable to which it is assigned) is called the target type. But lambda expressions may also use free variables (variables that are not parameters and are defined in an outer scope), just as anonymous classes can.
Restrictions on local variables
Method references
- In a nutshell
For example, Apple::getWeight is a method reference to the getWeight method defined in the Apple class. You can think of method references as syntactic sugar for lambdas that only refer to a single method because you write less to express the same thing.
Recipe for constructing method references
Constructor references
If you have a constructor with an Apple signature (integer weight), it fits the signature of the Function interface, so you can do this. If you have a two-argument constructor, Apple(String color, integer weight), it fits the signature of the BiFunction interface, so you can do this.
Putting lambdas and method references into practice!
- Step 1: Pass code
- Step 2: Use an anonymous class
- Step 3: Use lambda expressions
- Step 4: Use method references
The signature of the abstract method (called the function descriptor) can describe the signature of a lambda expression. We explained that the Java compiler could infer the type of the parameters of a lambda expression using the context in which the lambda appears.
Useful methods to compose lambda expressions
- Composing Comparators
It's not just because it's shorter; it's also clear what it means, and the code reads like the problem statement "sort inventory by comparing the weight of apples".
Reversed order
So you can simply modify the previous example to sort the apples by decreasing the weight by reusing the original Comparator:.
Chaining Comparators
Composing Predicates
From simpler lambda expressions, you can imagine more complicated lambda expressions that still read like the problem statement.
Composing Functions
Similar ideas from mathematics
- Integration
- Connecting to Java 8 lambdas
As we mentioned earlier, Java 8 uses the notation (double x) -> x+10 (a lambda expression) for exactly this purpose; hence you can write. As a side note, it's a bit of a shame that you have to write f.apply(a) instead of just f(a) as in math, but Java just can't escape the view that everything is an object - rather than the idea that a function is truly independent.
Summary
Functional-style data processing
Introducing streams
- What are streams?
- Getting started with streams
- Streams vs. collections
- Traversable only once
- External vs. internal iteration
- Stream operations
- Intermediate operations
- Terminal operations
- Working with streams
- Summary
In the next chapter, we will explore in detail the stream operations available for expressing complex data processing queries. Note that this is an incomplete list of operations provided by the Streams API; you will see more of them in the next chapter.
Working with streams
- Filtering and slicing
- Filtering with a predicate
- Filtering unique elements
- Truncating a stream
- Skipping elements
- Mapping
- Applying a function to each element of a stream
- Flattening streams
If you have a list of words, you would like to return a list of the number of characters for each word. So the stream returned by the map method is actually of type Stream
Using flatMap
Finding and matching
- Checking to see if a predicate matches at least one element
- Checking to see if a predicate matches all elements
The allMatch method works similarly to anyMatch, but will check whether all the elements of the stream match the given predicate. For example, you can use it to find out if the menu is healthy (that is, all dishes are under 1000 calories):
Finding an element
Optional in a nutshell
Finding the first element
If you don't care which element is returned, use findAny because it's less restrictive when using parallel streams.
Reducing
- Summing the elements
Next, the lambda is called again with the accumulated value and the next element of the stream, 5, which produces the new accumulated value, 9. Next, the lambda is called again with the accumulated value and the next element, 3, which produces 12.
No initial value
Maximum and minimum
If you add the necessary synchronization, you'll likely find that thread contention robs you of all the performance parallelism is supposed to give you. It certainly is in many applications, as you saw in the previous examples.
Putting it all into practice
- The domain: Traders and Transactions
- Solutions
We now provide the solutions in the following code listings, so that you can verify your understanding of what you have learned so far.
Numeric streams
- Primitive stream specializations
Each of these interfaces brings new methods to perform common numerical reductions, such as sum to calculate the sum of a numerical stream and max to find the maximum element. The thing to remember is that these specializations are not more complexity around streams, but instead more complexity caused by boxing - the (efficiency-based) difference between int and Integer and so on.
Mapping to a numeric stream
Converting back to a stream of objects
In the next section, you'll learn that framing is especially useful when you're dealing with numeric ranges that need to be bounded into a general stream.
Default values: OptionalInt
Numeric ranges
It produces a stream so you can chain the filter method to select only even numbers. Because count is a terminal operation, it will process the stream and return the result 50, which is the number of even numbers from 1 to 100, inclusive.
Putting numerical streams into practice: Pythagorean triples
Note that if you used IntStream.range(1, 100) instead, the result would be 49 even numbers, because the range is exclusive.
Pythagorean triple
Representing a triple
Filtering good combinations
Generating tuples
Generating b values
Generating a values
The FlatMap method performs the mapping and also flattens all the generated streams of triples into a single stream.
Running the code
Can you do better?
Building streams
- Streams from values
- Streams from arrays
- Streams from files
- Streams from functions: creating infinite streams!
You can create a stream with clear values using the static Stream.of method, which can take any number of parameters. For example, in the following code you create a stream of strings directly using Stream.of.
Iterate
As a result, the iteration method produces a stream of all even numbers: the first element of the stream is the initial value 0. Your task is to use the iteration method to generate the first 20 elements of the sequence of Fibonacci tuples.
Generate
Summary
You can combine all the elements of a stream iteratively to produce a result using the reduce method, for example, to calculate the sum or find the maximum of a stream. Some operations such as sort and distinct also preserve state because they must store all elements of a stream before returning a new stream.
Collecting data with streams
- Collectors in a nutshell
- Collectors as advanced reductions
- Predefined collectors
- Reducing and summarizing
- Finding maximum and minimum in a stream of values
- Summarization
- Joining Strings
- Generalized summarization with reduction
Finally, in Section 6.5 you will learn more about the Collector interface before exploring (Section 6.6) how you can create your own custom collectors to use in those cases not covered by the factory methods of the Collectors class. For the remainder of this chapter, we assume that you have imported all of the static factory methods from the Collectors class.
Collection framework flexibility: doing the same operation in different ways
But here it just means that the type of the collector's accumulator is unknown, or in other words, the accumulator itself can be of any type. We have used it here to report exactly the signature of the method as originally defined in the Collector class, but in the rest of the chapter we avoid any wildcard notation to keep the discussion as simple as possible.
Choosing the best solution for your situation
Grouping
- Multilevel grouping
- Collecting data in subgroups
Here you pass a function (expressed in the form of a method reference) to the groupingBy method that extracts the corresponding Dish.Type for each Dish in the stream. In the previous section, you saw that it is possible to pass a second groupBy collector to the outer one to achieve a multilevel grouping.
Adapting the collector result to a different type
The collectors are represented by the dashed lines, sogroupingBy is the outermost one and groups the menu flow into three sub-flows according to the different dish types. The reduction operation on the partial streams is then performed by the reducing collector, but it.
Other examples of collectors used in conjunction with groupingBy
Partitioning
- Advantages of partitioning
- Partitioning numbers into prime and nonprime
Partitioning has the advantage of keeping both lists of the stream elements for which applying the partitioning function returns true or false. Reduce the stream to a single value, starting from an initial value used as an accumulator, and iteratively combine it with each element of the stream using a BinaryOperator.
The Collector interface
- Making sense of the methods declared by Collector interface
It is the accumulator type, the object on which the partial result will be accumulated during the aggregation process. R is the object type (usually, but not always, collection) that results from the collection operation.
Making a new result container: the supplier method
For example, you can implement a ToListCollector
Adding an element to a result container: the accumulator method
Applying the final transformation to the result container: the finisher method
These first three methods are enough to perform a successive reduction of the current which, at least from a logical point of view, can proceed as figure 6.7. The implementation details are a bit trickier in practice due to both the lazy nature of the stream, which may require a pipeline of other intermediate operations to run before the collect operation, and the possibility, in theory, to parallelize the reduction to execute.
Merging two result containers: the combiner method
At this point, all substreams can be processed in parallel, each using the sequential reduction algorithm shown in Figure 6.7. This is done by combining the results corresponding to the substreams associated with each division of the original stream.
Characteristics method
Putting them all together
Finally, all partial results are combined pairwise using the function returned by the combiner. Note that this implementation is not the same as the one returned by the Collectors .toList method, but differs only in some minor optimizations.
Performing a custom collect without creating a Collector implementation
- Developing your own collector for better performance
- Divide only by prime numbers
- Defining the Collector class signature
- Implementing the reduction process
- Making the collector work in parallel (if possible)
- The finisher method and the collector’s characteristic method
- Comparing collectors’ performances
This means that when you test whether a given candidate number is prime or not, you do not have access to the list of the other primes found so far. In this method, you call the isPrime method and pass to it (along with the number for which you want to test whether it is prime or not) the list of the primes found so far (these are the values indexed by the true key in the accumulating card).