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.
- Laziness - In programming, laziness refers to only processing the objects you want to process when you need to process them. So in the example above, the last loop is "lazy" because it only loops through the two
Person
objects left after theList
has been filtered. The the code should be more efficient because the final processing step only occurs on the desired objects. - Eagerness - Code that performs operations on every object in a list would be considered "eager". So if an enhanced
for
loop iterates through an entire list to process two objects, this is considered a more "eager" approach.
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.
For more information on Lambda Expressions and Collections see the State of the Collections post.