Book Review: Java 8 In Action

Java 8 has been out for long enough – I needed to go learn the new features!  I read Java 8 In Action by Manning Publications.  (There’s a newer version, Modern Java in Action, covering up to Java 10, but I was strictly working on Java 8 projects)

This book has been a godsend!  After reading through the book and working through its many examples, I now have a strong grasp on the new features of Java 8.

Java 8 is a major language update, borrowing great ideas from other languages that help to keep Java “fresh”.  The biggest benefits of these updates are:

  • Parallelism – Java 8 makes it easy to take advantage of multi-core processors.
  • Conciseness – Java’s verbose boilerplate code can often be replaced with Java 8 syntactic sugar.
  • Ready for Big data – With streams you can work on larger datasets without loading all of them into memory at once (analogous to Map/Reduce)
  • Imperative, not Declarative – You can more often write “what” you want to do, rather than “how” you want it done.

These updates are accomplished through three major features:

  • Method reference – You can now pass methods (functions) to other functions.
  • Lambda – A lambda is a one-use function with a name.  Previous Java versions let you approximate Lambdas with anonymous classes, in a verbose way.
  • Streams – A stream processes collections using a database-like syntax.  You describe what you want and let the language figure out how to accomplish it.

Chapter 1 – Introduction

Chapter 1 introduces a few of these features with a simple and illuminating example: filtering files.  Many Java developers are familiar with the verbose boilerplate code in the first code block.

Pre-Java 8:

File [] hiddenFiles = new File(".").listFiles(new FileFilter() {
   public boolean accept(File file) {
      return file.isHidden()
   }
});

Java 8 using method references

File[] hiddenFiles = new File(".").listFiles(File::isHidden);

This is an exciting example to see where this book is going!

Chapter 2 – Behavior Parameterization

Chapters 1 and 2 further show the value of method references by doing more advanced filtering.  The old method took five lines for a filter, whereas the Java 8 method took only one line.  When multiple levels of filtering are needed, Java 8 really shines!  Pre-Java 8, if you wanted to filter apples on two conditions, you either wrote 5-line methods and chained them, or made one complex 5-line filtering method.  With method references and streams, you can do complex filtering simply.  Look at this Java 8 code for filtering a list of apples to collect only heavy, green apples:

List<Apple> inventory = getApples();
List<Apple> filteredApples = inventory.stream()
                              .filter(Apple::isGreenApple)
                              .filter(Apple::isHeavyApple)
                              .collect(toList());

Chapter 3 – Lambdas

Chapter 3 introduces Lambdas.  Lambdas have these characteristics:

  • Anonymous (no name)
  • Function (not associated with a class, but has parameters/body/return type/exceptions)
  • Passable (can be passed as argument)
  • Concise (lots of boilerplate is removed)

Lambdas can be used anywhere a @FunctionalInterface is accepted.  Think about all of the one-method interfaces you’re familiar with – FileFilter::accept as an example.  Each of those can now be replaced with a Lambda expression.

File::isHidden could be written as a lambda:

(File f) -> f.isHidden()

And then could be consumed as:

File[] hiddenFiles = new File(".").listFiles((File f) -> f.isHidden());

The authors are careful to mention that when a lambda gets long, you should replace it with a named function.

Out of all the Java 8 features, I’m most concerned about Lambdas.  They have the most potential for abuse.  I strongly prefer named methods for the following reasons:

  • The name gives a descriptive label
  • Named methods are more easily reused
  • Lambdas cannot be unit tested directly

A one-line lambda may be fine, but as a code reviewer I would be looking hard at code with many lambdas and try to find ways to reduce them.

The book had a wonderful table describing all of the @FunctionalInterface types in Java.  The most common ones are:

  • Predicate<T> – returning a Boolean for some object, for instance Apple::isHeavy is a predicate.
  • Consumer<T> – takes an object and returns void
  • Supplier<T> – takes no arguments and produces an object
  • Function<T, R> – takes an object of type T and returns and object of type R
  • There are also versions that take two parameters

This table helped me understand streams better.  A lot of stream operations are accomplished with Predicates and Functions.

Chapters 4-7 – All about streams

Chapter 1 got me excited about streams – this section sealed the deal.

Streams let you move from external iteration to internal iteration:

  • External iteration (imperative): you write the loop
  • Internal iteration (declarative): the loop is implied, you don’t write it

The first time I read that, it was not very exciting.  After all, I’m really good at writing for loops!  What made this amazing for me was how I could build a multi-level table from some raw data (essentially a pivot table).  I had a CSV file with SurveyResponses (data, questionId, count)

20210306,q1,53
20210306,q2,23
20210307,q1,15
20210307,q2,8

After reading these values into List<SurveyResponse> responses, it took only a few lines to build the pivot table.  How many lines would this team without streams?

Map<String, Map<String, Integer>> responsesByDateAndNode =
  responses.stream().collect(
      groupingBy(SurveyResponse::getDate,
      groupingBy(SurveyResponse::getQuestionId,
      reducing(0, SurveyResponse::getCount, Integer::sum)
  ))
); 

The key insights from the book on streams are:

  • Think of Streams as a fancy Iterator. 
  • Or think of a stream as a lazily-constructed collection.
  • Most operations on a stream are finding or grouping.
  • Elements in a stream are produced only as and when required. (Lazy evaluation!)
  • A stream can be consumed only once.

This section of the book has many great recipes on how to use streams.  Almost every recipe includes a non-streams code version for comparison.  It’s amazing how much code melts away when you use streams.  The declarative nature of streams programming means you spend more time thinking about what you want, and less time figuring out how to implement it in a bunch of for-loops and temporary variables.

I don’t want to reproduce the many examples from this book – buy the book for this section alone!

Chapter 8 – Refactoring, testing, and debugging

I loved this chapter – I would have liked even more discussion on these concepts.

The first key point was general guiding principles for refactoring code to the “Java 8 way”:

  • Anonymous classes -> Lambda Expressions
  • Lambda Expressions -> Method References
  • Imperative data processing (loops) -> Declarative data processing (streams)

It’s funny to me that Lambdas are on both the left and right side of these equations 😀

But, there are several design patterns are suitable for lambdas – each of which can be done more concisely with lambdas: strategy, template method, observer, chain of responsibility, factory.  Lambdas that become long should instead become named methods, passed as method reference.

Another key principle from the book is:

  • Removing verbosity improves readability

I would supplement that message

  • Removing verbosity usually improves readability

Java 8 code with lambdas can be so concise it almost becomes cryptic, at least for this Java 5 developer.  The book does not explore this point very hard, only urging readers to consider readability.  I urge caution in using some Java 8 features – make sure your code is still readable, don’t just force-fit as many Java 8 features as you can.

The key testing message around lambdas is that lambdas cannot be tested directly.  You can only test a function that uses/consumes a lambda, not the lambda itself.  This tells me it is critical not to make your lambdas “too big”.  From this alone, I’m suspicious of the value of any lambda that’s more than one or two lines long.

Chapter 10 – Using Optional as a better alternative to null

Optionals is an interesting Java 8 feature.  Optionals wrap nullable values.  Java code that uses null has many problems:

  • Source of error – NullPointerException is the most common exception seen in Java
  • Code bloat – every null check makes code more verbose
  • Breaks encapsulation – as an API consumer, you are always worried about pointers being null.
  • Type system is broken – null gives no hints what Object was supposed to be used
  • Exit complexity – Many null-checks in your code can introduce many possible exit points

It’s helpful to think of Optional as a Stream that has at most one value.  (It is not actually a Stream, Optional extends Object, but has many of the same methods.)

Even though Optional has Optional.empty(), or Optional.isPresent(), you start to reintroduce the same problems from null.  When possible, use methods that have default behavior.

If a Person object has an Optional<String> name field, you can do the following to return a person’s name if it exists, or “Unknown name” if it doesn’t. 

String name = person.getName().orElse(“Unknown name”);

You can also supply a function argument that consumes an Optional value only if it exists:

person.getName().ifPresent(System.out::println)

Other chapters

The remaining chapters had useful content but were not life-changing.

  • Chapter 9 – Default methods
  • Chapter 11 – CompletableFuture: composable asynchronous programming
  • Chapter 12 – New Date and Time API
  • Chapter 13 – Thinking Functionally
  • Chapter 14 – Functional programming techniques
  • Chapter 15 – Blending OOP and FP: comparing Java 8 and Scala
  • Chapter 16 – Conclusions and where next for Java
  • Appendix A – Miscellaneous Language Updates
  • Appendix B – Miscellaneous Library Updates
  • Appendix C – Performing multiple operations in parallel on a stream
  • Appendix D – Lambdas and JVM bytecode

These chapters were useful as references – these are mostly documentations of incremental changes in Java 8. 

Summary

This book made Java 8 clear to me.  With hundreds of annotated diagrams and code listings, I was finally able to appreciate the value of the new Java 8 features.  Thanks to this book, I can now read and appreciate Java 8 code without going to a search engine on every line.

I highly recommend Java 8 In Action, or the newer edition Modern Java in Action (covers Java 8, 9, and 10), for anyone who wants to understand the new Java 8 features.

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.