In my last lambda how to I introduced the basic syntax for lambda expressions and how they solve some common use cases in Java. This how-to builds upon that to show you how lambda expressions can improve your code. Lambdas should provides a means to better support the DRY (Don't Repeat Yourself) principle and make your code simpler and more readable.
A Common Query Use Case
A common use case for programs is to search through a collection of data to find items that match a specific criteria. In the excellent Jump-Starting Lambda presentation at JavaOne 2012, Stuart Marks and Mike Duigou walk though just such a use case. Given a list of people, various criteria are used to make robocalls (automated phone calls) to matching persons. So this tutorial is going to use that basic premise with some slight variations.
In this example, I decided that a phone call does not fully maximize the marketing potential of our robot. So in addition to calls, the robot can send e-mails and snail mail as well. Thus, using three different contact mediums fully maximizes marketing potential and customer satisfaction (or customer annoyance depending upon one's perspective :). Our message needs to get out to three different groups in the United States:
- Drivers - Persons over the age of 16
- Draftees - Male persons between the age of 18 and 25
- Pilots (Specifcally Commercial Pilots) - persons between the age of 23 and 65
The actual robot that does all this work has not arrived at our place of business yet. So instead of calling, mailing or emailing, a message is printed to the console. The message contains a person's name, age, and then information specific to the target medium (e.g., email address when e-mailing or phone number when calling).
Person Class
Each person in the test list is defined using the Person class with the following properties:
10 public class Person {
11 private String givenName;
12 private String surName;
13 private int age;
14 private Gender gender;
15 private String eMail;
16 private String phone;
17 private String address;
18
The Person
class uses a Builder
to create new objects. A sample list of people is created with the createShortList
method. Here is a short code fragment of that method. Note: All source code for this tutorial is included in a NetBeans project which is linked at the end of this page.
128 public static List<Person> createShortList(){
129 List<Person> people = new ArrayList<>();
130
131 people.add(
132 new Person.Builder()
133 .givenName("Bob")
134 .surName("Baker")
135 .age(21)
136 .gender(Gender.MALE)
137 .email("bob.baker@example.com")
138 .phoneNumber("201-121-4678")
139 .address("44 4th St, Smallville, KS 12333")
140 .build()
141 );
142
143 people.add(
144 new Person.Builder()
145 .givenName("Jane")
146 .surName("Doe")
147 .age(25)
148 .gender(Gender.FEMALE)
149 .email("jane.doe@example.com")
150 .phoneNumber("202-123-4678")
151 .address("33 3rd St, Smallville, KS 12333")
152 .build()
153 );
154
155 people.add(
156 new Person.Builder()
157 .givenName("John")
158 .surName("Doe")
159 .age(25)
160 .gender(Gender.MALE)
161 .email("john.doe@example.com")
162 .phoneNumber("202-123-4678")
163 .address("33 3rd St, Smallville, KS 12333")
164 .build()
165 );
166
A First Attempt
With a Person
class defined along with the search criteria, it is time to write a
RoboContactsMethods.java
1 package com.example.lambda;
2
3 import java.util.List;
4
5 /**
6 *
7 * @author MikeW
8 */
9 public class RoboContactMethods {
10
11 public void callDrivers(List<Person> pl){
12 for(Person p:pl){
13 if (p.getAge() >= 16){
14 roboCall(p);
15 }
16 }
17 }
18
19 public void emailDraftees(List<Person> pl){
20 for(Person p:pl){
21 if (p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE){
22 roboEmail(p);
23 }
24 }
25 }
26
27 public void mailPilots(List<Person> pl){
28 for(Person p:pl){
29 if (p.getAge() >= 23 && p.getAge() <= 65){
30 roboMail(p);
31 }
32 }
33 }
34
35
36 public void roboCall(Person p){
37 System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
38 }
39
40 public void roboEmail(Person p){
41 System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
42 }
43
44 public void roboMail(Person p){
45 System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
46 }
47
48 }
You can see from the names, callDrivers
, emailDraftees
, and mailPilots
, the methods describe what kind of behavior is taking place. The search criteria is clear with an appropriate call to each Robo action that takes place. However, there are some negatives with this design:
- The DRY principle is not followed.
- Each method repeats a looping mechanism.
- The selection criteria has to be rewritten for each method
- A large number of methods would be required to implement each use case.
- The code is inflexible. If the search criteria were to change, it would require a number of code changes to fix. Thusly, the code is not very maintainable.
Refactor the Methods
So how can the class be fixed? The search criteria would seem to be a good place to start. If test conditions can be isolated in separate methods, that would be an improvement.
RoboContactMethods2.java
1 package com.example.lambda;
2
3 import java.util.List;
4
5 /**
6 *
7 * @author MikeW
8 */
9 public class RoboContactMethods2 {
10
11 public void callDrivers(List<Person> pl){
12 for(Person p:pl){
13 if (isDriver(p)){
14 roboCall(p);
15 }
16 }
17 }
18
19 public void emailDraftees(List<Person> pl){
20 for(Person p:pl){
21 if (isDraftee(p)){
22 roboEmail(p);
23 }
24 }
25 }
26
27 public void mailPilots(List<Person> pl){
28 for(Person p:pl){
29 if (isPilot(p)){
30 roboMail(p);
31 }
32 }
33 }
34
35 public boolean isDriver(Person p){
36 return p.getAge() >= 16;
37 }
38
39 public boolean isDraftee(Person p){
40 return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
41 }
42
43 public boolean isPilot(Person p){
44 return p.getAge() >= 23 && p.getAge() <= 65;
45 }
46
47 public void roboCall(Person p){
48 System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
49 }
50
51 public void roboEmail(Person p){
52 System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
53 }
54
55 public void roboMail(Person p){
56 System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
57 }
58
59 }
Well that is better. Now the search criteria is encapsulated in a method. The test conditions can be reused and if a change is made, it would flow back throughout the class. But there still is a lot of repeated code and a separate method is still required for each use case. Hmmm, is there a better way to pass the search criteria to the methods?
Using Anonymous Classes
Before Lambda expressions, an anonymous inner classes might do the trick. An interface could be written, call it MyTest.java
, that has one test
method that returns a boolean (a functional interface). That way, the desired search criteria could be passed when the method is called. So the interface would look like this:
6 public interface MyTest<T> {
7 public boolean test(T t);
8 }
And, the updated robot class would look like this:
RoboContactsAnon.java
9 public class RoboContactAnon {
10
11 public void phoneContacts(List<Person> pl, MyTest<Person> aTest){
12 for(Person p:pl){
13 if (aTest.test(p)){
14 roboCall(p);
15 }
16 }
17 }
18
19 public void emailContacts(List<Person> pl, MyTest<Person> aTest){
20 for(Person p:pl){
21 if (aTest.test(p)){
22 roboEmail(p);
23 }
24 }
25 }
26
27 public void mailContacts(List<Person> pl, MyTest<Person> aTest){
28 for(Person p:pl){
29 if (aTest.test(p)){
30 roboMail(p);
31 }
32 }
33 }
34
35 public void roboCall(Person p){
36 System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
37 }
38
39 public void roboEmail(Person p){
40 System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
41 }
42
43 public void roboMail(Person p){
44 System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
45 }
46
47 }
That is definitely another improvement. Now only three methods are needed to peform robotic operations. However, there is a slight problem with ugliness when the methods are called. Check out the test class used for this class:
RoboCallTest03.java
1 package com.example.lambda;
2
3 import java.util.List;
4
5 /**
6 * @author MikeW
7 */
8 public class RoboCallTest03 {
9
10 public static void main(String[] args) {
11
12 List<Person> pl = Person.createShortList();
13 RoboContactAnon robo = new RoboContactAnon();
14
15 System.out.println("\n==== Test 03 ====");
16 System.out.println("\n=== Calling all Drivers ===");
17 robo.phoneContacts(pl,
18 new MyTest<Person>(){
19 @Override
20 public boolean test(Person p){
21 return p.getAge() >=16;
22 }
23 }
24 );
25
26 System.out.println("\n=== Emailing all Draftees ===");
27 robo.emailContacts(pl,
28 new MyTest<Person>(){
29 @Override
30 public boolean test(Person p){
31 return p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
32 }
33 }
34 );
35
36
37 System.out.println("\n=== Mail all Pilots ===");
38 robo.mailContacts(pl,
39 new MyTest<Person>(){
40 @Override
41 public boolean test(Person p){
42 return p.getAge() >= 23 && p.getAge() <= 65;
43 }
44 }
45 );
46
47
48 }
49 }
This is a great example of the "vertical" problem in practice. This code is a little difficult to read. In addition, we are back to writing custom search criteria for each use case.
Lambda Expressions Get it Just Right
Lambda expressions can be used to solve all the problems encountered so far. But first a little housekeeping.
java.util.function
In the previous example, the MyTest
functional interface was used to pass anonymous classes to methods. It turns out, I did not need to write that interface. Java 8 provides the java.util.function
package with a number of standard functional interfaces. In this case, the Predicate
interface meets our needs.
3 public interface Predicate<T> {
4 public boolean test(T t);
5 }
The test
method takes a generic class and returns a boolean result. Just what is needed to make selections. So here is the final version of the robot class.
RoboContactsLambda.java
1 package com.example.lambda;
2
3 import java.util.List;
4 import java.util.function.Predicate;
5
6 /**
7 *
8 * @author MikeW
9 */
10 public class RoboContactLambda {
11 public void phoneContacts(List<Person> pl, Predicate<Person> pred){
12 for(Person p:pl){
13 if (pred.test(p)){
14 roboCall(p);
15 }
16 }
17 }
18
19 public void emailContacts(List<Person> pl, Predicate<Person> pred){
20 for(Person p:pl){
21 if (pred.test(p)){
22 roboEmail(p);
23 }
24 }
25 }
26
27 public void mailContacts(List<Person> pl, Predicate<Person> pred){
28 for(Person p:pl){
29 if (pred.test(p)){
30 roboMail(p);
31 }
32 }
33 }
34
35 public void roboCall(Person p){
36 System.out.println("Calling " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getPhone());
37 }
38
39 public void roboEmail(Person p){
40 System.out.println("EMailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getEmail());
41 }
42
43 public void roboMail(Person p){
44 System.out.println("Mailing " + p.getGivenName() + " " + p.getSurName() + " age " + p.getAge() + " at " + p.getAddress());
45 }
46
47 }
With this approach only 3 methods are needed, one for each contact method. The lambda expression passed to the method selects the Person
instances that meet the test conditions.
Verical Problem Solved
Using lambda expressions solves the vertical problem and allows the easy reuse of any expression. Take a look at the new test class updated for lambda expressions.
RoboCallTest04.java
1 package com.example.lambda;
2
3 import java.util.List;
4 import java.util.function.Predicate;
5
6 /**
7 *
8 * @author MikeW
9 */
10 public class RoboCallTest04 {
11
12 public static void main(String[] args){
13
14 List<Person> pl = Person.createShortList();
15 RoboContactLambda robo = new RoboContactLambda();
16
17 // Predicates
18 Predicate<Person> allDrivers = p -> p.getAge() >= 16;
19 Predicate<Person> allDraftees = p -> p.getAge() >= 18 && p.getAge() <= 25 && p.getGender() == Gender.MALE;
20 Predicate<Person> allPilots = p -> p.getAge() >= 23 && p.getAge() <= 65;
21
22 System.out.println("\n==== Test 04 ====");
23 System.out.println("\n=== Calling all Drivers ===");
24 robo.phoneContacts(pl, allDrivers);
25
26 System.out.println("\n=== Emailing all Draftees ===");
27 robo.emailContacts(pl, allDraftees);
28
29 System.out.println("\n=== Mail all Pilots ===");
30 robo.mailContacts(pl, allPilots);
31
32 // Mix and match becomes easy
33 System.out.println("\n=== Mail all Draftees ===");
34 robo.mailContacts(pl, allDraftees);
35
36 System.out.println("\n=== Call all Pilots ===");
37 robo.phoneContacts(pl, allPilots);
38
39 }
40 }
Notice that a Predicate
is set up for each group: allDrivers
, allDraftees
, and allPilots
. Any one of these can be passed to one of the contact methods. The code is compact, easy to read and is not repetitive.
Are lambda expressions cool or what? :)
Resources
The NetBeans project and all the course code is included in the following zip file.