Java.next: Java 8 as Java.next

Evaluating Java 8 as a suitable Java replacement

This installment of Java.next investigates the Java™ 8 release as a reasonable candidate for your next programming language. Find out how lambda blocks and the streaming API upgrade Java to a modern language.

Share:

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks

Photo of Neal fordNeal Ford is Director, Software Architect, and Meme Wrangler at ThoughtWorks, a global IT consultancy. He is also the designer and developer of applications, instructional materials, magazine articles, courseware, and video/DVD presentations, and he is the author or editor of books spanning a variety of technologies, including the most recent Presentation Patterns. He focuses on designing and building large-scale enterprise applications. He is also an internationally acclaimed speaker at developer conferences worldwide. Check out his website.



28 April 2014

Also available in Chinese Russian Japanese

The original mandate of this series was to compare and contrast three newer JVM languages to help you evaluate which one is the likely successor to the Java language. But in the interim, the Java language underwent its most significant change since the addition of generics. Now, Java itself exhibits many of the desirable characteristics of Groovy, Scala, and Clojure. In this installment, I consider Java 8 as a Java.next language and include examples that show how effectively the language's programming paradigms have been augmented.

Higher-order functions at last

About this series

The Java legacy will be the platform, not the language. More than 200 languages run on the JVM, and it's inevitable that one of them will eventually supplant the Java language as the best way to program the JVM. This series explores three next-generation JVM languages: Groovy, Scala, and Clojure, comparing and contrasting new capabilities and paradigms, to provide Java developers a glimpse into their own near future.

Higher-order functions take other functions as arguments or return other functions as results. Java — perhaps the last holdout among popular languages — finally has higher-order functions, in the form of lambda blocks. Rather than merely bolt higher-order functions onto the language, the Java 8 engineers made clever choices to enable older interfaces to take advantage of functional features.

In the "Functional coding styles" installment, I demonstrated implementations in the Java.next languages for a problem that traditionally would be solved imperatively. The problem posits an input list of names with the requirement to remove single-character entries and return a comma-delimited list with each name capitalized. The imperative Java solution appears in Listing 1.

Listing 1. Imperative name transformation
public String cleanNames(List<String> listOfNames) {
    StringBuilder result = new StringBuilder();
    for(int i = 0; i < listOfNames.size(); i++) {
        if (listOfNames.get(i).length() > 1) {
            result.append(capitalizeString(listOfNames.get(i))).append(",");
        }
    }
    return result.substring(0, result.length() - 1).toString();
}

public String capitalizeString(String s) {
    return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());
}

Iteration is the norm in previous Java versions, but with Java 8, the job is performed more elegantly with streams — an abstraction that acts like a cross between a collection and UNIX® pipe. Listing 2 uses streams.

Listing 2. Name transformation in Java 8
public String cleanNames(List<String> names) {
    return names
            .stream()
            .filter(name -> name.length() > 1)
            .map(name -> capitalize(name))
            .collect(Collectors.joining(","));
}

private String capitalize(String e) {
    return e.substring(0, 1).toUpperCase() + e.substring(1, e.length());
}

The iterative version in Listing 1 must conflate filter, transform, and concatenate tasks within a single for loop — because it's inefficient to loop through the collection for each task. With streams in Java 8, you can chain and compose functions together until you call a function that generates output (called a terminal operation), such as collect() or forEach().

I use a bit of syntactic sugar for creating the lambda blocks in Listing 2:.filter(name -> name.length() > 1) is short for filter((name) -> name.length() > 1. For single parameters, the superfluous set of parentheses is optional.

The filter() method in Listing 2 is the same filter method that's common in functional languages (see "Java.next: Overcome synonym suffering"). The filter() method accepts a higher-order function that returns a Boolean value, which is used as the filtering criterion: true indicates inclusion in the filtered collection, and false indicates absence from the collection.

The filter() method accepts a type of Predicate<T>, which is a method that returns a Boolean value. You can explicitly create predicate instances if you like, as shown in Listing 3.

Listing 3. Creating a predicate manually
Predicate<String> p = (name) -> name.startsWith("Mr");
List<String> l = List.of("Mr Rogers", "Ms Robinson", "Mr Ed");
l.stream().filter(p).forEach(i -> System.out.println(i));

In Listing 3, I create a predicate by assigning the filtering lambda block to it. When I call the filter() method on the third line, I pass the predicate as the expected parameter.

In Listing 2, the map() method works as expected, applying the capitalize() method to each element in the collection. Finally, I call the collect() method, which is a terminal operation — a method that generates values from the stream. The collect() method performs the familiar reduce operation: combining elements to produce a (typically) smaller result, sometimes a single value (for example, a sum operation). Java 8 has a reduce() method, but collect() is preferred in this case because it works efficiently with mutable containers such as a StringBuilder.

By adding awareness of functional constructs such as map and reduce to existing classes and collections, Java faces the issue of efficient collection updates. For example, a reduce operation is much less useful if you can't use it on typical Java collections such as ArrayList. Many of the collection libraries in Scala and Clojure are immutable by default, which enables the runtime to generate efficient operations. Java 8 cannot force developers to change collections, and many of the existing collection classes in Java are mutable. Therefore, Java 8 includes methods that perform mutable reduction for collections such as ArrayList and StringBuilder that update existing elements rather than replace the result each time. Although reduce() will work in Listing 2, collect() works more efficiently for the collection returned in this instance.

One of the advantages of functional languages that I covered in the "Contrasting concurrency" installment is the ease with which you can parallelize collections, often by adding a single modifier. Java 8 offers the same advantage, as shown in Listing 4.

Listing 4. Parallel processing in Java 8
public String cleanNamesP(List<String> names) {
    return names 
            .parallelStream() 
            .filter(n -> n.length() > 1) 
            .map(e -> capitalize(e)) 
            .collect(Collectors.joining(","));
}

As in Scala, I can make stream operations operate in parallel in Listing 4 by adding a parallelStream() modifier. Functional programming defers implementation details to the runtime so that you can work at a higher abstraction level. The ease with which threading can be applied to collections exemplifies this advantage.

The differences among reducer types in Java 8 illustrates the difficulty of adding a deep paradigm such as functional programming to existing language constructs. The Java 8 team has done a great job of adding functional constructs in a mostly seamless way. A good example of this integration is the addition of functional interfaces.


Functional interfaces

A common Java idiom is an interface with a single method — called a SAM (single abstract method) interface — such as Runnable or Callable. In many cases, SAMs are used primarily as a transport mechanism for portable code. Portable code is best implemented in Java 8 with a lambda block. A clever mechanism called functional interfaces enables lambdas and SAMs to interact in useful ways. A functional interface is one that includes a single abstract method (and can include several default methods). Functional interfaces augment existing SAM interfaces to enable substitution of traditional anonymous inner classes with a lambda block. For example, the Runnable interface can now be flagged with the @FunctionalInterface annotation. This optional annotation tells the compiler to verify that Runnable is an interface (and not a class or enum) and that the annotated type meets the requirements of a functional interface.

As an example of the substitutability of lambda blocks, I can create a new thread in Java 8 by passing a lambda block in lieu of a Runnable anonymous inner class:

new Thread(() -> System.out.println("Inside thread")).start();

Functional interfaces can seamlessly integrate with lambda blocks in myriad useful places. Functional interfaces are a notable innovation because they work well with established Java idioms.

Default methods

With Java 8, you can also declare default methods on interfaces. A default method is a public non-abstract, non-static method (with a body), declared in an interface type and marked with the default keyword. Each default method is automatically added to classes that implement the interface — a convenient way to decorate classes with default functionality. For example, the Comparator interface now includes more than a dozen default methods. If I create a comparator by using a lambda block, I can trivially create the reverse comparator, as shown in Listing 5.

Listing 5. Comparator's default methods
List<Integer> n = List.of(1, 4, 45, 12, 5, 6, 9, 101);
Comparator<Integer> c1 = (x, y) -> x - y;
Comparator<Integer> c2 = c1.reversed();
System.out.println("Smallest = " + n.stream().min(c1).get());
System.out.println("Largest = " + n.stream().min(c2).get());

In Listing 5, I create a Comparator instance wrapped around a lambda block. Then, I can create a reverse comparator by calling the reversed() default method. The ability to attach default methods to interfaces mimics a common use of mixins (see the "Mixins and traits" installment) and is a nice addition to the Java language.

Optional

Notice the trailing call to get() in the terminal calls in Listing 5. Calls to built-in methods such as min() return an Optional rather than a value. This behavior mimics the Java.next option feature, shown in Scala in "Common ground in Groovy, Scala, and Clojure, Part 3." Optional prevents method returns from conflating null as an error with null as a legitimate value. For example, terminal operations in Java 8 can use the ifPresent() method to execute a code block only if a legitimate result exists. For example, this code prints the result only if a value is present:

n.stream()
    .min((x, y) -> x - y)
    .ifPresent(z -> System.out.println("smallest is " + z));

An orElse() method also exists that I can use if I want to take additional action. Browsing the Comparator interface in Java 8 is an illuminating look at how much power default methods add.


More about streams

The stream interface and related facilities in Java 8 are a well-thought-out set of extensions that will breathe new life into the Java language.

The stream abstraction in Java 8 makes many advanced functional features possible. Streams act in many ways like collections, but with key differences:

  • Streams do not store values, but rather act as a pipeline from an input source to a destination through a terminal operation.
  • Streams are designed to be functional rather than stateful. For example, the filter() operation returns a stream of filtered values without modifying the underlying collection.
  • Stream operations try to be as lazy as possible (see "Java.next: Memoization and functional synergy" and "Functional thinking: Laziness, Part 1"). A lazy collection performs work only if values must be retrieved.
  • Streams can be unbounded (or infinite). For example, you can construct a stream that returns all numbers and use methods such as limit() and findFirst() to gather subsets.
  • Like Iterators, streams are consumed upon use and must be regenerated before subsequent reuse.

Stream operations are either intermediate or terminal operations. Intermediate operations return a new stream and are always lazy. For example, using a filter() operation on a stream doesn't actually filter the stream but rather creates a stream that returns the filtered values only when traversed by a terminal operation. Terminal operations traverse the stream, producing values or side effects (if you write functions that produce side effects, which is discouraged).

Streams already include many useful terminal operations. For example, take the number-classifier example from my Functional thinking series (which also reappears in previous two Java.next installments). Listing 6 implements the classifier in Java 8.

Listing 6. Number classifier in Java 8
public class NumberClassifier {

    public static IntStream factorsOf(int number) {
        return range(1, number + 1)
                .filter(potential -> number % potential == 0);
    }

    public static boolean isPerfect(int number) {
        return factorsOf(number).sum() == number * 2;
    }

    public static boolean isAbundant(int number) {
        return factorsOf(number).sum() > number * 2;
    }

    public static boolean isDeficient(int number) {
        return factorsOf(number).sum() < number * 2;
    }

}

If you're familiar with my number-classifier versions in other languages (see "Functional thinking: Thinking functionally"), you'll notice that Listing 6 lacks a sum() method declaration. In all the other implementations of this code in alternate languages, I had to write the sum() method myself. Java 8 includes sum() as a terminal operation, freeing me from writing it. By hiding moving parts, functional programming reduces the possibility of developer error. If I don't need to implement sum(), I can't make a mistake in the implementation. The stream interface and related facilities in Java 8 are a well-thought-out set of extensions that will breathe new life into the Java language.

In other versions of the number classifier, I showed an optimized version of the factors() method that traverses potential factors only up to the square root and generates the symmetrical factors. The optimized version of the Java 8 factors() methods appears in Listing 7.

Listing 7. Optimized classifier in Java 8
    public static List fastFactorsOf(int number) {
        List<Integer> factors = range(1, (int) (sqrt(number) + 1))
                .filter(potential -> number % potential == 0)
                .boxed()
                .collect(Collectors.toList());
        List factorsAboveSqrt = factors
                .stream()
                .map(e -> number / e).collect(toList());
        factors.addAll(factorsAboveSqrt);
        return factors.stream().distinct().collect(toList());
    }

The factorsOf() method in Listing 7 cannot just combine two streams into a single result, even though streams can be concatenated. However, once a stream has been traversed, it is exhausted (like an Iterator) and must be regenerated before reuse. In Listing 7, I create two collections using streams and concatenate the results, adding a call to distinct() to handle the edge case of duplicates due to whole-number square roots. Streams in Java 8 have an impressive amount of power, including the ability to compose streams.


Conclusion

In this installment, I investigated Java 8 as a Java.next language, and it scores well. The well-designed stream libraries and clever extension mechanisms such as default methods make it possible for huge amounts of existing Java code to benefit from the new features with little effort.

In the next installment, I wrap up the series with some thoughts on choosing languages.

Resources

Learn

  • Lambda Expressions: Take a look at this tutorial to learn more about some new Java 8 features.
  • "Java 8 language changes" (Dennis Sosnoski, developerWorks, 2014): Get a critical look at lambdas and interface changes in the Java language.
  • Functional Programming in Java (Venkat Subramaniam, Pragmatic Bookshelf, 2014): Check out this great resource.
  • Functional Thinking (Neal Ford, O'Reilly Media, 2014): Learn more about numerous Java 8 topics in this book by the series author.
  • Functional thinking article series: Explore functional programming in Neal Ford's column series on developerWorks.
  • Language designer's notebook: In this developerWorks series, Java Language Architect Brian Goetz explores some of the language design issues that have presented challenges for the evolution of the Java language in Java SE 7, Java SE 8, and beyond.
  • "Introduction to Java multitenancy" (Graeme Johnson and Michael Dawson, developerWorks, September 2013): Learn about a new feature for cloud systems in the IBM Java 8 beta.
  • Scala: Scala is a modern, functional language on the JVM.
  • Groovy: Groovy is a dynamic variant of the Java language, with updated syntax and capabilities.
  • Clojure: Clojure is a modern, functional Lisp that runs on the JVM.
  • developerWorks Java technology zone: Find hundreds of articles about every aspect of Java programming.

Get products and technologies

Discuss

  • Get involved in the developerWorks community. Connect with other developerWorks users as you explore the developer-driven blogs, forums, groups, and wikis.

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Java technology on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=969772
ArticleTitle=Java.next: Java 8 as Java.next
publish-date=04282014