Java SE 8 in Practice

Версия 4

    by Trisha Gee

     

    Get a new tool set for easily performing common operations.

     

    I've been giving a presentation called "Java 8 in Anger." No, it's not that the most recent release of Java has me particularly enraged; in anger is (apparently) a British phrase meaning "in practice" or "in the real world."

     

    The aim of the talk is to demo, with live code, how to use some of the Java SE 8 features such as lambda expressions and the Stream API to solve some of the coding problems you might come across in your day job as a developer.

     

    I'm not going to transcribe the whole talk for this article; it'd probably work best (if you're interested) to watch the video instead. What I do want to do is to use the application from the presentation to explore some specific Java SE 8 features in more detail. If you're interested in the wider context, I've got links to the video and source code on my blog.

     

    Method References and New Methods in Java SE 8

     

    The first scenario in which we will start using some of the new methods available in Java SE 8 is when we're building up a leaderboard of top tweeters. This leaderboard, which is shown in Figure 1, will be a table of the Twitter users who have tweeted the most, in descending order.

     

    f1.png

    Figure 1. Twitter leaderboard

     

    We store every Twitter handle we see as a key in a Map, and for the value we have a TwitterUser (an object to represent this particular user). When we see a new Twitter handle, we want to create a TwitterUser for this person and store it in the map, keyed against this Twitter handle.

       

    Before Java SE 8, we'd write something like Listing 1:

     

     private final Map<String, TwitterUser> allTwitterUsers = new HashMap<>();      
         public void onMessage(String twitterHandle) {         
         TwitterUser twitterUser = allTwitterUsers.get(twitterHandle);         
         if (twitterUser == null) {             
         twitterUser = new TwitterUser(twitterHandle);             
         allTwitterUsers.put(twitterHandle, twitterUser);         
    }        
     // then do stuff..     
    }
    

     

    Listing 1. Pre-Java SE 8 code for storing Twitter handles

     

    But Java SE 8 has not only given us lambdas and streams, it has added new methods to some of our favorite classes.  Map now has a computeIfAbsent() method (see Listing 2) that allows you to define what to do if an item doesn't exist in the map.

     

    private final Map<String, TwitterUser> allTwitterUsers = new HashMap<>();      
         public void onMessage(String twitterHandle) {         
    TwitterUser twitterUser = allTwitterUsers.computeIfAbsent(twitterHandle,                                                                    
    
    TwitterUser::new);         
    // then do stuff...     
    }
    

     

    Listing 2. Using the computeIfAbsent() method

     

    Listing 2 does the same thing as Listing 1 and is clearly far fewer lines of code, especially because it also makes use of method references, which usually work out to be even shorter than lambda expressions. As a long-term Java programmer with limited exposure to other languages, although this may be shorter, learning to write code this way is going to take me a while to get used to.

     

    Fortunately for me and my old-school Java approach, IntelliJ IDEA can help me come to grips with the new syntax. When I first came to Java SE 8, I found it more understandable (although much more code) to call these sorts of methods using an anonymous inner class rather than a lambda expression or a method reference. IntelliJ IDEA can help create this anonymous inner class and convert it into something more Java SE 8-ish, as shown in Figure 2:

     

    ReplaceWithMethodReference.gif

    Figure 2. Converting code to a method reference

     

    Method references are going to take me a while to grasp instinctively, but I am starting to see how the succinct syntax could be more expressive once I get used to it. computeifAbsent() is going to call a constructor on TwitterUser to create a new user if one doesn't already exist in the map.

     

    Streams

     

    Next, we need to increment the number of times we've seen this TwitterUser, and then figure out how this impacts our leaderboard of tweeters. We can use the Stream API to turn our map of all Twitter users into a list of the top ten tweeters.

     

    First, we need to sort our TwitterUsers according to the number of times they've tweeted. There's a sorted() method on Stream that takes a Comparator, so it seems logical to use this (see Listing 3).

     

    public void onMessage(String twitterHandle) {         
         TwitterUser twitterUser = allTwitterUsers.computeIfAbsent(twitterHandle, TwitterUser::new);         
         twitterUser.incrementCount();          
         allTwitterUsers.values().stream()                        
         .sorted(new Comparator<TwitterUser>() {                            
         @Override                            
         public int compare(TwitterUser o1, TwitterUser o2) {                                
         return o2.getNumberOfTweets() - o1.getNumberOfTweets();                           
          }                        
    });     
    }
    
    

    Listing 3. Using a Comparator

     

    In the past, I've always found comparators a little hard to work with; I'm never quite sure if I'm supposed to be subtracting the first object from the second or vice versa.  Fortunately, once again, Java SE 8 makes things a little easier. You can use the new Comparator.comparing() method, which you can combine with a method reference to state which method on TwitterUser you want to sort by:

     

    allTwitterUsers.values().stream()                    
    .sorted(Comparator.comparing(TwitterUser::getNumberOfTweets));
    

     

    For a leaderboard that shows the most active tweeters at the top, we need to sort in descending order, which is also simple and descriptive using the comparing() method:

     

       allTwitterUsers.values().stream()                    
    .sorted(comparing(TwitterUser::getNumberOfTweets).reversed());
    

     

    Note that I've added a static import for comparing to make the code a bit shorter.

     

    We've sorted all the values in the map, so now we need to get the top ten. Because we're using streams, it's easy to chain together all the operations we want to perform on our collection. So we add a call to limit (see Listing 4):

     

     allTwitterUsers.values().stream()                    
    .sorted(comparing(TwitterUser::getNumberOfTweets).reversed())                    
    .limit(10);
    
    

    Listing 4. Adding a call to limit

     

    So far all we've done is build up a recipe for the operations we want to perform. We need to somehow execute all of these. Streams have intermediate operations that return a Stream so that we can perform further operations (for example, both sorted() and limit() in Listing 4) and terminal operations that will return a concrete value. In this case, what we want is a new list with the top ten tweeters. Telling the collect method to put the results into a List will do just this:

     

     List<TwitterUser> topTen = allTwitterUsers.values().stream()                                         
    .sorted(comparing(TwitterUser::getNumberOfTweets).reversed())                                               
    .limit(10)                                              
    .collect(Collectors.toList());
    

     

    With this set of stream operations, we're effectively querying our map of all users. If we were querying a database instead of a map, we might have written something like the following:

     

       SELECT * from TwitterUsers     ORDER BY numberOfTweets DESC     LIMIT 10

     

    Figure 3 shows how we built the whole operation:

     

    Streams.gif

    Figure 3. Using streams

     

    Creating Our Own Streams

     

    Another part of this application requires a bar chart, where each bar corresponds to a minute in time over a period of ten minutes, as shown in Figure 4.

     

    f4.png

    Figure 4. Bar chart widget

     

    When the application starts for the first time, each of these bars needs to be set to an initial value of zero.

     

    This is not an unusual situation—we've probably all written code that has to loop for some number of iterations and initialize some value.  We would probably usually do it using something like Listing 5:

     

      public HappinessChartData() {         
         int nowMinute = LocalDateTime.now().getMinute();          
         for (int i = nowMinute; i < nowMinute + 10; i++) {            
          initialiseBarToZero(i);        
          }     
    }
    

    Listing 5. Traditional way of coding a loop

     

    In Listing 5, we also use a very small part of the new Date and Time API.  In the Java SE 8 world, we have more options for iterating over values.  In this case, we can create our own IntStream from a range of values and perform some operation for each of these values, as shown in Listing 6:

     

        public HappinessChartData() {         
           int nowMinute = LocalDateTime.now().getMinute();          
           IntStream.range(nowMinute, nowMinute + 10)                  
          .forEach(value -> initialiseBarToZero(value));     
    }
    

     

    Listing 6. Creating our own stream and using a lambda expression to specify what to do with each of the values

     

    We can use a lambda expression to specify what to do with each of the values, as shown in Listing 6.  As before, we can also simplify this lambda expression to a method reference, as shown in Listing 7:

    IntStream.range(nowMinute, nowMinute + 10)                  
         .forEach(this::initialiseBarToZero);
    

    Listing 7. Creating our own stream and using a method reference to specify what to do with each of the values

     

    Be aware that the performance of the two approaches is not the same, however. In this example, where the loop is very small (just 10 iterations), the traditional for loop should perform faster because of how simple it is for the compiler to convert it to bytecode. The streams approach has the additional overhead of creating the stream and calling library code methods. However, using the Streams API has two potential advantages:

     

    • It's marginally less code and, once we get used to the new syntax, arguably more readable.
    • It's parallelizable. If the initialization method is a complex operation or long-running method call, or if the loop were to iterate over many more elements, adding a parrallel() declaration on the stream might provide performance gains that overcome the for loop's benefits.

     

    As with any design choice, you'll have to decide which approach to use based on your team's preferences for syntax, the performance requirements of your application, and the characteristics of the problem you're trying to solve. If in doubt, write a performance test that uses production-like data to see what the impact is.

     

    Figure 5 shows how to write a loop by creating a stream instead of using a traditional for loop.

     

    CreateStream.gif

    Figure 5. Creating a stream

     

    File Handling in Java SE 8

     

    Java SE 7 introduced some really nice features for file handling. Working with files becomes even more intuitive when you combine these Java SE 7 features with streams.

     

    In Listing 8, we're parsing a file that contains tweets recorded from the Twitter firehose.  Each line contains a different tweet, and what we want to do is filter out the noise (there are lines that contain only the single "OK" string as a response from Twitter) and then publish the remaining tweets via WebSockets.

     

    try (Stream<String> lines = Files.lines(filePath)) {             
                 lines.filter(s -> !s.equals("OK"))                  
              .forEach(tweetsEndpoint::onMessage);         
         } catch (IOException e) {             
    //error handling here         
    }
    

    Listing 8. Parsing a file that contains tweets

     

    Here, I'm not tempted to show the Java SE 6 version of the code, because the Java SE 8 version is much simpler.  The first line in Listing 8 uses a try-with-resources statement to obtain a stream of Strings, one string per line in our file.  Then we use the filter() method to exclude the lines that aren't tweets, and finally publish all the remaining tweets via our WebSocket server endpoint.

     

    Figure 6 shows how Java SE 8 streams combined with Java SE 7 I/O work nicely together to make reading files simpler.

     

    Java7And8.gif

    Figure 6. File handling with Java SE 8

     

    Advanced Streams and More Collectors

     

    The last part of the application that I'd like to show in more depth is the mood analyzer.  The aim of this small service is to take a single tweet and figure out, in a very rough fashion, the mood of that tweet.  The service will generally decide if a single tweet is happy, sad, or happy and sad. Then it returns a comma-separated string stating the mood in uppercase, as shown in the following examples:

     

    • "Today was a great day!" would be classified as "HAPPY".
    • "I'm irritated I have to work tomorrow" would be classified as "SAD".
    • "The start of this week was fantastic, but today sucks" contains both good and bad news, so it would be classified as "HAPPY,SAD".

     

    If I had an automated system for applying moods to Twitter messages, I'd be retired on a beach somewhere, because this kind of analysis is very much in demand.  In my application, I settle instead for a very crude method of classifying the messages: we're going to parse the message and look for words that might imply that the tweeter was happy or sad.

     

    We start with a Map, which maps words to their associated mood (represented as an enum), as in Listing 9:

     

      ["happy": HAPPY,      
    "fantastic": HAPPY,      
    "awesome": HAPPY,      ...     
    "sad": SAD,      
    "terrible": SAD,      
    "sucks": SAD]
    

     

    Listing 9. Mapping words to moods

     

    There are a number of approaches to this problem, and this is not a definitive answer. I will demonstrate with my selected method how you can chain together stream operations to get the output that you need.

     

    First, we create a stream in which each item is a word in the original Twitter message.  We use String's split() method to create an array of words, and use Stream.of() to turn this into a Stream of Strings.

     

        Stream.of(twitterMessage.split("\\s+"))     

     

    Next, we're going to convert all these words to lowercase, because our map of words to moods only has lowercase keys.

     

        Stream.of(twitterMessage.split("\\s+"))           
         .map(String::toLowerCase)

     

    The map() method takes one value and turns it into another.  In this case, we want to turn a string value such as "Awesome" into its lowercase version: "awesome".  It's easy to pass in String's toLowerCase() method as a method reference to do the conversion.

     

    Now we have a string in the correct case, so we can search our map of words-to-moods for the mood that corresponds to this word.

     

        Stream.of(twitterMessage.split("\\s+"))           
         .map(String::toLowerCase)          
         .map((lowerCaseWord) -> WORD_TO_MOOD.get(lowerCaseWord))          

     

    Not every word is going to have a corresponding mood in the map, so we're going to pass only non-null moods to the next step of the pipeline:

     

        Stream.of(twitterMessage.split("\\s+"))           
         .map(String::toLowerCase)          
         .map((lowerCaseWord) -> WORD_TO_MOOD.get(lowerCaseWord))          
         .filter(mood -> mood != null)

     

    Now what I have is a Mood, which is an enum value of either HAPPY or SAD. Our requirements state that for each tweet, we need to return one of the following: "HAPPY", "SAD", or "HAPPY,SAD". We don't need an indication of whether there are multiple happy words or multiple sad words in the same tweet, so we need only one instance of each mood. This is easily done by requiring the stream to contain only distinct values.

     

     

      Stream.of(twitterMessage.split("\\s+"))        

      .map(String::toLowerCase)        

      .map((lowerCaseWord) -> WORD_TO_MOOD.get(lowerCaseWord))        

       .filter(mood -> mood != null)         

      .distinct()

     

    According to our requirements, we need to convert our enum value to a String:

     

    Stream.of(twitterMessage.split("\\s+"))         

    .map(String::toLowerCase)        

    .map((lowerCaseWord) -> WORD_TO_MOOD.get(lowerCaseWord))        

    .filter(mood -> mood != null)        

    .distinct()        

    .map((mood) -> mood.name())

     

    And, finally, we want to merge together all the moods found for this message into a comma-separated string of moods.

     

    In our previous stream example, we used Collectors.toList() to return a List of TwitterUsers. This time, we're going to use another built in collector, Collectors.joining() (see Listing 10), and give it the characters we want to use to separate our results.

     

    joining() will return a String of the results, separated by a chosen delimiter. This means that now in Java SE 8, we can easily create comma-separated strings without having to iterate over a list and figure out if we're supposed to be putting a comma after each item or not.

     

    Stream.of(twitterMessage.split("\\s+"))        

    .map(String::toLowerCase)        

    .map((lowerCaseWord) -> WORD_TO_MOOD.get(lowerCaseWord))        

    .filter(mood -> mood != null)        

    .distinct()        

    .map((mood) -> mood.name())        

    .collect(Collectors.joining(","));

     

    Listing 10. Using Collectors.joining()

     

    Finally, let's simplify the lambdas by using method references, where possible, and wrap Listing 10 in a method (Listing 11) that can be called:

     

    public static String identifyMood(String twitterMessage) {      

    return Stream.of(twitterMessage.split("\\s+"))                   

    .map(String::toLowerCase)                   

    .map(WORD_TO_MOOD::get)                   

    .filter(mood -> mood != null)                   

    .distinct()                   

    .map(Enum::name)                   

    .collect(joining(","));  

    }

     

    If we called identifyMood with the message "Yesterday I was sad sad sad, but today is awesome," we'll get the String "SAD,HAPPY" in return.

     

    Figure 7 shows an example of building up stream operations to map more words to moods.

     

    AdvancedStreams.gif

    Figure 7. Advanced streams

     

    With the example presented in this section, we have seen how you can chain multiple operations to perform fairly complex processing with limited lines of code.

     

    Conclusion

     

    Java SE 8 is more than just a bit of extra syntax to learn or a couple of new methods that might be useful. It can change the way we approach solving common problems. With lambda expressions, streams, and some of the other new methods in Java SE 8, you can use easily perform common operations (for example, computeIfAbsent() and joining()), and you gain a new set of tools, particularly for manipulating data sets.

     

    See Also

     

     

    About the Author

     

    Trisha has developed Java applications for a range of industries, including finance, manufacturing, and non-profit, for companies of all sizes.  She has expertise in Java high-performance systems, is passionate about enabling developer productivity, and dabbles with open source development. Trisha blogs regularly on subjects that she thinks developers and other humans should care about, and she is a leader of the Sevilla Java User Group, a key member of the London Java Community, a MongoDB Master, and a Java Champion. She believes we shouldn't have to make the same mistakes again and again, and as a Developer Advocate for JetBrains, she can share all the cool stuff she's discovered so far.

     

    Join the Conversation

     

    Join the Java community conversation on Facebook, Twitter, and the Oracle Java Blog!