Java 8: Collections and Lambda Expressions

My previous post introduced the Function interface and finished up examples of basic lambda expression syntax. This post reviews how lambda expressions improve the Collection classes.

Lambda Expressions and Collections

In all the examples created so far, the collections classes have been used quit a bit. However, there are a number of new lambda expression features that change the way collections are worked with. This post shows you a few features you have been missing so far.

Improvements to Person

Before diving into the specific lambda collection features, some changes have been made to the Person class for this set of examples. First, a Map has been added to the Person class to store all the printing functions from the last post. Previously all lambda expressions have been ad hoc, so using a Map should make it possible to reuse the expressions any time a Person needs to be printed.

First, the Map must be defined and initialized.

Person.java
13 public class Person {
14   private String givenName;
15   private String surName;
16   private int age;
17   private Gender gender;
18   private String eMail;
19   private String phone;
20   private String address;
21   private final Map<String, Function> printMap = new HashMap<>();
22 

So a String is used to identify each lambda Function. We can then use the string to retrieve the print style when needed.

The printing styles are initialized and retrieved as follows.

Person.java
 74   private void initPrintMap(){
 75     // Print name and phone western style
 76     Function<Person, String> westernNameAgePhone = p -> 
 77             "\n" + p.getGivenName() + " " + p.getSurName() + "\n" +
 78             "Age: " + p.getAge() + "\n" +
 79              "Phone: " + p.getPhone();
 80     
 81     Function<Person, String> gangnamNameAgePhone = p ->
 82             "\n" + p.getSurName() + " " + p.getGivenName() + "\n" +
 83             "Age: " + p.getAge() + "\n" +
 84             "Phone: " + p.getPhone();
 85 
 86     Function<Person, String> fullWestern = p -> {
 87       return "\nName: " + p.getGivenName() + " " + p.getSurName() + "\n" +
 88              "Age: " + p.getAge() + "  " + "Gender: " + p.getGender() + "\n" +
 89              "EMail: " + p.getEmail() + "\n" + 
 90              "Phone: " + p.getPhone() + "\n" +
 91              "Address: " + p.getAddress();
 92     };
 93     
 94     Function<Person, String> fullGangnam =  p -> "\nName: " + p.getSurName() + " " 
 95             + p.getGivenName() + "\n" + "Age: " + p.getAge() + "  " + 
 96             "Gender: " + p.getGender() + "\n" +
 97             "EMail: " + p.getEmail() + "\n" + 
 98             "Phone: " + p.getPhone() + "\n" +
 99             "Address: " + p.getAddress();
100     
101     printMap.put("westernNameAgePhone", westernNameAgePhone);
102     printMap.put("gangnamNameAgePhone", gangnamNameAgePhone);
103     printMap.put("fullWestern", fullWestern);
104     printMap.put("fullGangnam", fullGangnam);
105     
106   }
107   
108   public Function<Person, String> getPrintStyle(String functionName){
109     return printMap.get(functionName);
110   }
111   
112   

The names have changed slightly, but the print interfaces from the last post in the series is essentially the same. Each Function is stored in the Map and is easily retrieved using the getPrintStyle method.

In addition to the print behavior, the group selection logic needed to be encapsulated as well. So the drivers, pilots, and draftees have all been included in the following class.

SearchCriteria.java
   1 package com.example.lambda;
   2 
   3 import java.util.HashMap;
   4 import java.util.Map;
   5 import java.util.function.Predicate;
   6 
   7 /**
   8  *
   9  * @author MikeW
  10  */
  11 public class SearchCriteria {
  12 
  13   private final Map<String, Predicate> searchMap = new HashMap<>();
  14 
  15   private SearchCriteria() {
  16     super();
  17     initSearchMap();
  18   }
  19 
  20   private void initSearchMap() {
  21     Predicate<Person> allDrivers = p -> p.getAge() >= 16;
  22     Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
  23     Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;
  24 
  25     searchMap.put("allDrivers", allDrivers);
  26     searchMap.put("allDraftees", allDraftees);
  27     searchMap.put("allPilots", allPilots);
  28 
  29   }
  30 
  31   public Predicate<Person> getCriteria(String PredicateName) {
  32     Predicate<Person> target;
  33 
  34     target = searchMap.get(PredicateName);
  35 
  36     if (target == null) {
  37 
  38       System.out.println("Search Criteria not found... ");
  39       System.exit(1);
  40     
  41     }
  42       
  43     return target;
  44 
  45   }
  46 
  47   public static SearchCriteria getInstance() {
  48     return new SearchCriteria();
  49   }
  50 }

All the Predicate based search criteria is stored in this class and available for out test methods.

Looping

The first feature to look at is the new foreach method available to any collection class. Here are a couple of examples that print out a Person list.

Test01ForEach.java
   1 package com.example.lambda;
   2 
   3 import java.util.List;
   4 import java.util.Map;
   5 import java.util.function.Function;
   6 
   7 /**
   8  *
   9  * @author MikeW
  10  */
  11 public class Test01ForEach {
  12   
  13   public static void main(String[] args) {
  14     
  15     List<Person> pl = Person.createShortList();
  16     
  17     System.out.println("\n=== Western Phone List ===");
  18     pl.forEach(p -> { 
  19       p.printl(p.getPrintStyle("westernNameAgePhone"));
  20     });
  21     
  22     System.out.println("\n=== Gangnam Phone List ===");
  23     pl.forEach(p -> { 
  24       p.printl(p.getPrintStyle("gangnamNameAgePhone"));
  25     });
  26     
  27   }
  28 
  29 }

Any collection can be iterated through this way. The basic structure is very similar to the enhanced for loop. However, there are a number of potential benefits to including the iteration mechanism within the class. This is explored in the next section.

Chaining and Filters

In addition to looping through the contents of a collection, you can chain together methods to focus in on the results you want. The first method to look at is filter which take a Predicate interface as a parameter.

The following example loops though a List after first filtering the results.

Test02Filter.java
   1 package com.example.lambda;
   2 
   3 import java.util.List;
   4 
   5 /**
   6  *
   7  * @author MikeW
   8  */
   9 public class Test02Filter {
  10   
  11   public static void main(String[] args) {
  12 
  13     List<Person> pl = Person.createShortList();
  14     
  15     SearchCriteria search = SearchCriteria.getInstance();
  16     
  17     System.out.println("\n=== Western Pilot Phone List ===");
  18     
  19     pl.stream().filter(search.getCriteria("allPilots"))
  20       .forEach(p -> { 
  21         p.printl(p.getPrintStyle("westernNameAgePhone"));
  22       });
  23     
  24     System.out.println("\n=== Gangnam Phone List ===");
  25     pl.forEach(p -> { 
  26       p.printl(p.getPrintStyle("gangnamNameAgePhone"));
  27     });
  28    
  29     System.out.println("\n=== Gangnam Draftee Phone List ===");
  30     pl.stream().filter(search.getCriteria("allDraftees"))
  31       .forEach(p -> { 
  32         p.printl(p.getPrintStyle("gangnamNameAgePhone"));
  33       });
  34     
  35   }
  36 }

The first and last loops demonstrate how the List is filtered based on the search criteria defined above. The output from the last loop looks like following.

=== Gangnam Draftee Phone List ===

Baker Bob
Age: 21
Phone: 201-121-4678

Doe John
Age: 25
Phone: 202-123-4678
Getting Lazy

So these features and nice, but why add them to the collections classes when we already have a perfectly good for loop? Well the main reason is that by moving iteration features into the library it allows the developers of Java to do more code optimizations. To explain further, a couple of terms need definitions.

By making looping part of the collections library, code can be better optimized for "lazy" operations when the opportunity arises. When a more eager approach makes sense (e.g., computing a sum or an average), eager operations are still applied. This is a much more efficient and flexible approach than always using eager operations.

The stream Method

In the code above, notice that the steam method is called before filtering and looping begin. This method takes a Collection as input and returns a java.util.stream.Stream interface as the output. A Stream represents a sequence of elements on which various methods can be chained. By default, once elements are consumed they are no longer available from the stream. So a chain of operations can only occur once on a particular Stream. In addition, a Stream can be serial(default) or parallel depending up on the method called. An example of a parallel stream is included in the last section of this post.

Mutation and Results

To repeat what stated above, a Stream is disposed of after its use. So the elements in a collection cannot be changed or mutated with a Stream. But what if you want to keep elements returned from your chained opeations? You can save then to a new collection, here is some code that shows how to do just that.

Test03toList.java

   1 package com.example.lambda;
   2 
   3 import java.util.List;
   4 import java.util.stream.Collectors;
   5 
   6 /**
   7  *
   8  * @author MikeW
   9  */
  10 public class Test03toList {
  11   
  12   public static void main(String[] args) {
  13     
  14     List<Person> pl = Person.createShortList();
  15     
  16     SearchCriteria search = SearchCriteria.getInstance();
  17     
  18     // Make a new list after filtering.
  19     List<Person> pilotList = pl
  20             .stream()
  21             .filter(search.getCriteria("allPilots"))
  22             .collect(Collectors.<Person>toList());
  23     
  24     System.out.println("\n=== Western Pilot Phone List ===");
  25     pilotList.forEach( p -> {
  26       p.printl(p.getPrintStyle("westernNameAgePhone"));
  27     });
  28 
  29   }
  30 
  31 }

The collect method is called with one parameter, the Collectors class. The Collectors class is able to return a List or Set based on the results of the stream. The example shows how the result of the stream is assigned to a new List which is iterated over.

Calulating with map

A commonly used method use with filter is map. The method is used to take a property from a class and do something with it. The following example demonstrates this by performing calculations based on age.

Test04Map.java

   1 package com.example.lambda;
   2 
   3 import java.util.List;
   4 import java.util.OptionalDouble;
   5 
   6 /**
   7  *
   8  * @author MikeW
   9  */
  10 public class Test04Map {
  11 
  12   public static void main(String[] args) {
  13     List<Person> pl = Person.createShortList();
  14     
  15     SearchCriteria search = SearchCriteria.getInstance();
  16     
  17     // Calc average age of pilots old style
  18     System.out.println("== Calc Old Style ==");
  19     int sum = 0;
  20     int count = 0;
  21     
  22     for (Person p:pl){
  23       if (p.getAge() >= 23 && p.getAge() <= 65 ){
  24         sum = sum + p.getAge();
  25         count++;
  26       }
  27     }
  28     
  29     long average = sum / count;
  30     System.out.println("Total Ages: " + sum);
  31     System.out.println("Average Age: " + average);
  32     
  33     
  34     // Get sum of ages
  35     System.out.println("\n== Calc New Style ==");
  36     long totalAge = pl
  37             .stream()
  38             .filter(search.getCriteria("allPilots"))
  39             .map(p -> p.getAge())
  40             .sum();
  41 
  42     // Get average of ages
  43     OptionalDouble averageAge = pl
  44             .parallelStream()
  45             .filter(search.getCriteria("allPilots"))
  46             .map(p -> p.getAge())
  47             .average();
  48 
  49     System.out.println("Total Ages: " + totalAge);
  50     System.out.println("Average Age: " + averageAge.getAsDouble());    
  51     
  52   }
  53   
  54 }

And the output from the class is:

== Calc Old Style ==
Total Ages: 150
Average Age: 37

== Calc New Style ==
Total Ages: 150
Average Age: 37.5

The program calculates the average age of pilots in our list. The first loop demonstrates the old style of calculating the number using a for loop. The second loop uses the map method to get the age of each person in a serial stream. Note that totalage is a long. The map method returns an IntSteam object which contains a sum method which returns a long.

Note: To compute the average the second time, calculating the sum of ages is unecessary. However, I thought it was instructive show an example of using the sum method.

The last loop computes the average age from the stream. Notice that the parallelStream method is used to get a parallel stream so the values can be computed concurrently. The return type is a bit different here as well.

Resources

All the source code for these examples can be found in the following zip file.

LambdaCollectionExamples.zip

For more information on Lambda Expressions and Collections see the State of the Collections post.