If you're interested in reading about lambda expressions and how they can improve your fortunes as a professional developer, read on. If you practice the exercises as you go, you will soon master lambda expressions.
Conventions Used in This Book
Because using lambda expressions is all about abstracting complexity into libraries, I'll introduce a bunch of common library goodies as I go along. The latter chapters are more complex, but they also teach you how to be a more complete programmer who can confidently use lambda expressions in your own designs.
Using Code Examples
Safari® Books Online
How to Contact Us
Acknowledgments
Introduction
Why Did They Need to Change Java Again?
You can write easier to read code - code that spends time expressing the intent of its business logic rather than the mechanics of how it is achieved. Easier to read code is also easier to maintain, more reliable and less error prone.
What Is Functional Programming?
Being able to pass functions around easily also makes it easier to write lazy code that only initializes values when necessary. You might not consider it the perfect example topic, but it is simple, and many of the code examples in this book will be similar to what you might see in your business domain.
Lambda Expressions
Your First Lambda Expression
What happens under the hood is that javac infers the variable event type from its context, here from the addActionListener signature. What this means is that you don't have to write down the type explicitly if it is obvious.
How to Spot a Lambda in a Haystack
Instead of the body of the lambda expression being just an expression, it's a complete block of code, surrounded by curly braces. The target type of a lambda expression is the type of context in which the lambda expression occurs—for example, a local variable to which it is assigned or a method parameter in which it is passed.
Using Values
For example, example 2-7 will fail to compile with the error message: Local variables referenced by the lambda expression must be final or actually final. To avoid such pointless discussion, I will call them "lambda expressions" in this book.
Functional Interfaces
The functional interface here takes one ActionEvent parameter and returns nothing (empty), but functional interfaces can come in many types. From now on, I'll use diagrams to represent the different types of functional interfaces you'll encounter.
Type Inference
It is still type checked and offers all the security you are used to, but you do not have to explicitly state the types. In both cases, we map the variables to a functional interface so it's easier to see what's going on.
Key Points
In these cases, instead of making a wild guess, it will just stop what it's doing and ask for help in the form of a compilation error. Consequently, our compiler thinks that its arguments and return values are all instances of java.lang.Ob ject.
Exercises
Streams
From External Iteration to Internal Iteration
You will need to rewrite each for loop individually to make them work in parallel. Filtering in this case means "keep only items that pass a test." The test is defined by a function which returns either true or false depending on whether the artist is from London.
What’s Actually Going On
So, if you ran Example 3-6 with the members of The Beatles as your list of artists, you would see Example 3-7 printed on your command line. This is how our counting example works, but it is the simplest case: only two operations.
Common Stream Operations
It is very easy to find out whether an operation is eager or lazy: see what it yields. If it gives you a Stream back, it's lazy; if it gives you back some other value or emptiness, then it is greedy.
Example 3-9 is the same example of converting a list of strings to their uppercase equivalents using the stream frames. The lambda expression passed to the map both takes a string as its only argument and returns a string.
If you're refactoring the legacy code, the presence of an if statement in the middle of a for loop is a pretty strong indication that you really want to use filter. Because this function does the same job as the if statement, it must return either true or false for a given value.
The stream to the filter has the elements of the stream before it, which evaluated to true. In each case, we replace the list with a stream using the stream method, and flatmap does the rest.
It is now possible for max to be called on an empty stream so that it returns what is known as an optional value. If our Stream is empty, it will not exist; if it is not empty, it will be.
A Common Pattern Appears
The accumulator starts with an initial Value and is then folded along with each element of the list by calling combine. To find the shortest value, our harvester returned the shortest trace of from the current element and the accumulator.
In the original example we used the first element in the list as our initialValue, but that doesn't have to be the case. In the imperative version we can see that the accumulator is a variable that we update on each loop iteration.
Putting Operations Together
It also encourages users of your domain class to write code in a more modern Java 8 style. Over time, you can rewrite your old code until you eventually remove all the getters that return a list or set.
Refactoring Legacy Code
We're really doing three things here: find only tracks over a minute long, get their names, and add their names to our Name Set. In summary, we took a piece of legacy code and refactored it to use idiomatic streams.
Multiple Stream Calls
It is less efficient because it requires eagerly creating new collection objects at each intermediate step. Of course, if you're writing your first few Stream-based examples, it's perfectly normal to write code that looks a bit like this.
Higher-Order Functions
Good Use of Lambda Expressions
This is actually a deliberate choice on behalf of the designers: an attempt to encourage people to use lambda expressions to capture values rather than capture variables. When passing lambda expressions into the higher-order functions on the Stream interface, you should try to avoid side effects.
Advanced Exercises
Libraries
Using Lambda Expressions in Code
What we actually want to be able to do is pass a lambda expression that generates a string to use as a message. Calling the get() method in this example is equivalent to calling the lambda expression that was passed to the method to be called.
Primitives
If the return type is a primitive, the interface is prefixed with To and the primitive type, as in ToL ongFunction (shown in Figure 4-1). It is a good idea to use the primitive specialized functions whenever possible for the performance benefits.
Overload Resolution
The lambda expression passed to the overloaded method is compatible with both the normal predicate and the IntPredicate. If there is only one possible target type, the lambda expression infers the type from the corresponding argument to the functional interface.
FunctionalInterface
Binary Interface Compatibility
Here's the guarantee that if you have source code in Java 1-7, it will compile in Java 8. The Stream method was added to the Collection interface in Java 8, which means that any class that implements Collection must also have this method on that.
Default Methods
Unfortunately, this change still breaks binary compatibility because it means that any class outside the JDK that implements Collection—for example, MyCustomList—must also have implemented the stream method. Aside from the addition of a new keyword, standard methods also have slightly different inheritance rules than regular methods.
Default Methods and Subclassing
OverridingParent is chosen over OverridingChild (code in Example 4-17), being a more specific type because it is a concrete method from a class rather than a default method (see Figure 4-5). If the default method wasn't guaranteed to be overridden by this addAll method, we could break the existing implementation.
Multiple Inheritance
Since it is not clear to Java which method to inherit, this will only cause the MusicalCarriage compile-error class to inherit unrelated rock() defaults from the Carriage and Jukebox types. Previously, super acted as a reference to the parent class, but by using the variant InterfaceName.super it is possible to specify a method from an in‐.
The Three Rules
If we have a situation where two interfaces compete to provide a standard method and one interface extends the other, the subclass wins. If the previous two lines don't give us an answer, the subclass must implement the method or declare it abstractly.
Tradeoffs
Static Methods on Interfaces
For example, if you want to create a simple value stream, you would expect the method to be located on the stream. Previously, this was impossible, and adding a very demanding API in terms of Stream finally motivated the addition of static methods on interfaces.
Optional
Both are shown in Example 4-23, along with the use of the isPre sent method (which indicates whether the Optional has a value). Your assignment is to restructure the getArtist method to return an Optional
Open Exercises
Advanced Collections and Collectors
Another thing to note here is that method references automatically support multiple parameters, as long as you have the right functional interface. We'll be using method references where appropriate from this point on, so you'll be seeing a lot more examples soon.
Element Ordering
If there is no meeting order on the input stream, there is no meeting order on the output stream. This can cause unexpected behavior, for example forEach makes no guarantees to encounter order if you use parallel streams.
Enter the Collector
However, most operations, such as filtering, mapping, and minification, can operate very efficiently on ordered flows. If you need an order guarantee in these situations, then forEachOr dered is your friend.
Into Other Collections
Later in this book, I'll talk about how you can use the streams library to perform data-parallel operations; collecting the results of parallel operations may require a different type of set to be produced than if there was no requirement for thread safety. For example, maybe you want to use a TreeSet instead of letting the framework determine what type of Set implementation you get.
To Values
You can do this by using the toCollection collector, which takes a function to build the collection as its argument (see Example 5-5). In fact, there is also a group of collectors that provide similar functionality, along the lines of averagingInt.
Partitioning the Data
There are also overloaded operations for double and long types, which allow you to convert ele‐. We can use these features to separate bands (artists with more than one member) from solo artists.
Grouping the Data
Strings
Our classifier is a feature - the same type we use for the normal map operation. Here we use a map to extract the artist names and then collect the stream using Collectors.joining.
Composing Collectors
We already know that we can group our albums by main artist using the groupingBy collector, but that would output a Map
Refactoring and Custom Collectors
Now that we delegate all the String joining behavior to a custom col‐. We've already wrapped our mutable container over a stream of values, but it's not quite the final value we want.
Reduction as a Collector
It is entirely possible that the final value you collect is the same as con‐. In fact, this is what happens when the final value you collect is a collection, such as with the toList collector.
Collection Niceties
Thankfully, a new forEach method has been introduced that takes a BiConsumer (two values in, nothing out) and produces easier-to-read code through internal iteration. Hint: you might want to start with the public class GroupingBy
Data Parallelism
Parallelism Versus Concurrency
The goal of parallelism is to reduce the execution time of a given task by dividing it into smaller components and executing them in parallel. Data parallelism is often contrasted with task parallelism, where each individual thread of execution can perform a completely different task.
Why Is Parallelism Important?
Parallel Stream Operations
Of course, it's important to make good use of parallelism in order to get the most out of your hardware, but the kind of data parallelism we get from the streams library is just one form. It is possible to get different performance numbers based on how you wrote your code and how many cores are available.
Simulations
The size of the input current is not the only factor to consider when deciding whether there is a parallel accelerator. It is also a good use for parallelism, as it is an implementation that gets good parallel acceleration.
Caveats
Performance
This involves mapping each integer to an int and also adding a quarter of the values in each thread. You can't easily break these down with the perfect amount of balance, but most of the time it is possible to do so.
Parallel Array Operations
The code in Example 6-11 multiplies each number in a list together and multiplies the result by 5. The code in Example 6-12 also calculates the sum of the squares of the numbers in a list.
Testing, Debugging, and Refactoring
Lambda Refactoring Candidates
In, Out, In, Out, Shake It All About
The Lonely Override
It also makes it much clearer to anyone trying to read the code what its purpose is. If you try to read aloud the words in the second example, you can easily hear what he is saying.
Behavioral Write Everything Twice
We will count the number of musicians, the number of songs and the duration of our order. As shown in Figure 7-1, it is parameterized by the argument type, so we use ToLongFunction
Unit Testing Lambda Expressions
All the lambda expression in this code does is call a core Java method directly. You really want to test for behavior specific to that code, but it's in a lambda expression and you have no way to reference it.
Using Lambda Expressions in Test Doubles
Lazy Evaluation Versus Debugging
Logging and Printing
However, this way has the disadvantage that we cannot continue to work on that stream, because streams can only be used once.
The Solution: peek
Midstream Breakpoints
In this case, the view can simply have an empty body in which you set a breakpoint. If you want to unit test a lambda expression of any complexity, cast it in a regular method.
Design and Architectural Principles
I will try to present some well-reasoned principles and patterns by which you can compose maintenance-free and reliable software - not just to use the shiny new JDK libraries, but to use lambda expressions in your own domain architecture and applications.
Lambda-Enabled Design Patterns
Command Pattern
Let's look at a concrete example of the command pattern and see how it is enhanced with lambda expressions. In fact, we can do this even better by recognizing that each of these lambda expressions executes a single method call.
Strategy Pattern
If we have a traditional implementation of the strategy pattern, then we can write client code that creates a new compressor with whatever strategy we want (Example 8-13). In this case, we can remove each of the concrete strategy implementations and refer to a method that implements the algorithm.
Observer Pattern
We have two concrete implementations of the LandingObserver class that represent the aliens' (example 8-17) and NASA's (example 8-18) views of the landing event. One thing to think about with both the observer and strategy patterns is that whether to go the lambda design route or the class route depends very much on the complexity of the strategy or observer code to be implemented.
Template Method Pattern
With lambda expressions and method references, we can think about the template method pattern in a different light and implement it differently as well. We can explicitly use a lambda expression to place the implementation in these classes, or use a method reference to the current class.
Lambda-Enabled Domain-Specific Languages
A specification is similar to a test method, an expectation is similar to an assertion, and a set is similar to a test class.
A DSL in Java
We can then describe the expectations about our object's behavior, all of which start with the prefix 'expect.that'.
How We Got There
You may have noticed one more detail that I've ignored so far that has nothing to do with lambda expressions. I've covered the parts of the DSL that interact with lambda expressions in order to give you a taste of how to implement this type of DSL.
Evaluation
There is a lot more work involved in implementing a complete BDD framework, but the purpose of this section is simply to show you how to use lambda expressions to create more fluid domain-specific languages.
Lambda-Enabled SOLID Principles
However, I will look at how the three principles can be applied in the context of lambda expressions. In the context of Java 8, some principles can be extended beyond their original limitations.
The Single Responsibility Principle
Each of the principles corresponds to a set of potential code smells that may exist in your code, and they offer a way out of the problems they cause. If we want to speed up the time it takes to perform this operation at the expense of using more CPU resources, we can use the parallelStream method without changing any of the other code (see Example 8-37).
The Open/Closed Principle
It implements the open/closed principle because we can get new behavior from ThreadLocal without changing it. Immutable objects implement the open/closed principle in the sense that because their internal state cannot be modified, it is safe to add new methods to them.
The Dependency Inversion Principle
We can use a Stream
Further Reading
Lambda-Enabled Concurrency
Why Use Nonblocking I/O?
So far I've avoided showing any code to demonstrate the ideas, because the concept of blocking vs. non-blocking I/O can be implemented in a number of different ways in terms of the API. The original version of NIO uses the concept of a selector, which allows a thread to manage multiple communication channels, such as the network socket used to write to your chat client.
Callbacks
The advantage of this approach is that the application does not control the threading model - the Vert.x framework. We will send this text message to the receiving user's chat client, which is done by writing the message to the TCP connection.
Message Passing Architectures
When it comes to the handler, we want to perform the same action we did earlier when we recorded dispatches: pass that message to the client (Example 9-6). We are looking at concurrency situations where we want to have many more units of I/O work, such as connected chat clients, than we have threads running in parallel.
The Pyramid of Doom
We have a withModule method that deploys the current Vert.x module, runs some code, and turns off the module. We also have a withConnection method that connects to the ChatVerticle and then disconnects when it's done with it.
Futures
Each of these login actions returns a Future
Completable Futures
At this point, it's worth reminding yourself that, just like with the Streams API, we don't actually do things; we are building a recipe that says how to do things. As shown in Example 9-12, to tell CompletableFuture that it is ready, you call the complete method.
Reactive Programming
Therefore, we need to replace each name with an Observable that represents one or more artists. Since an observable stream represents events, we need the ability to push multiple values.
When and Where
The first part of this exercise is to refactor the blocking return code to use a callback interface. Your mission, should you choose to accept it, is to modify BlockingArtistAnalyzer to implement ArtistAnalyzer (Example 9-19).
Moving Forward
If you already have your unit tests running under the Jenkins CI system, then it is very easy to run the same build under multiple Java versions. This could be an open source project you're interested in, or maybe even your working product if the trial deployment went well.
Index
About the Author
Colophon