I have had some limited C# development and find that these can often obscure the way that the code can be understood and are very daunting when you are new to the language. This particularly becomes an issue when used in LinQ expressions which do not necessarily work in the way expected and often times not until the data is actually used.
In an attempt to put my bias aside, I attempted to grasp the JDK8 implementation of them and provide a simple to understand tutorial on them.
What Is A Lambda?
As simply as I can put it, a lambda is a syntactic style that allows you to provide an expression about an individual object type in order to do something with the data. It allows for filtering, applying functions to them, counting them, iterating through them and more in a style that is very simple to write. A typical lambda is written as:(argument list) -> body
If the argument list contains only one argument, the parenthesis can be omitted. The argument list can be empty also but in such a case, the parenthesis are still required. I will try to explain the lambdas demonstrated in the tutorial as needed.
Getting Started
If you have not set up your Eclipse to use JDK8, please see this post for detailed instructions. All code for this example can be found in my GIT repository at java-blog-samples.Why Use Lambdas?
To start our tutorial, I have created a TestObject class. This will be the object we will include in a List using the TestObject.generateTestObjectList() static method. The actual examples of the various implementations of below are found in the ObjectTest class.Example 1
Lets take our List of TestObjects. Suppose you need to find any objects in that list that hava an 'a' value greater than 20. We will be smart developers by creating a method because we know we will have to process multiple searches. So using conventional Java programing we create:173 174 175 176 177 178 | private static void printObjectsWithAGreaterThanTwenty(List<TestObject> list) { // get each element in the colection and display it based on criteria for (TestObject t : list){ if (t.getA() > 20 ) t.displayObject(); } } |
30 31 32 33 34 35 36 | //Ex. 1 - Hardcoded Result println( "Ex. 1: Hardcoded Result" , under); println(); println( "TestObject with a > 20 (hardcoded)" ); println(); printObjectsWithAGreaterThanTwenty(objList); println(); |
Ex. 1: Hardcoded Result *********************** TestObject with a > 20 (hardcoded) TestObject (Rainmaker): a: 21 b: 37 TestObject (Boss Hog): a: 34 b: 47 TestObject (Terror): a: 45 b: 58 TestObject (Pitbull): a: 79 b: 70 TestObject (Axe): a: 124 b: 82 |
Example 2
Next we next find some analyst added the requirement that we have to display all objects which have a 'b' value within a given range. So we again create a method to find the results but this time, we add parameters to our method.180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 | private static void printObjectsWithBInRange(List<TestObject> list, int low, int high, Inclusivity rule) { for (TestObject t : list){ switch (rule){ case BOTH: if (t.getB() >= low && t.getB() <= high) t.displayObject(); break ; case END: if (t.getB() > low && t.getB() <= high) t.displayObject(); break ; case START: if (t.getB() >= low && t.getB() < high) t.displayObject(); break ; case NONE: if (t.getB() > low && t.getB() < high) t.displayObject(); } } } |
262 263 264 | private enum Inclusivity { START, END, BOTH, NONE } |
38 39 40 41 42 43 44 | //Ex. 2 - Parameterized Range println( "Ex. 2 - Parameterized Range" , under); println(); println( "TestObject with b between 10 and 70 inclusive" ); println(); printObjectsWithBInRange(objList, 10 , 70 , Inclusivity.BOTH); println(); |
Ex. 2 - Parameterized Range *************************** TestObject with b between 10 and 70 inclusive TestObject (Viper): a: 3 b: 10 TestObject (Stingray): a: 5 b: 15 TestObject (Snoopy): a: 8 b: 21 TestObject (Archangel): a: 13 b: 28 TestObject (Rainmaker): a: 21 b: 37 TestObject (Boss Hog): a: 34 b: 47 TestObject (Terror): a: 45 b: 58 TestObject (Pitbull): a: 79 b: 70 |
Example 3
So we finally decide we need to refactor and realizing that we can use a nested inner class, we create a functional interface (an interface declaration with only one abstract method).258 259 260 261 | // Functional Interfaces interface TestObjectValidator { boolean test(TestObject t); } |
257 258 259 260 | // Functional Interfaces interface TestObjectValidator { boolean test(TestObject t); } |
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | //Ex. 3 - Use of Local Class println( "Ex. 3 - Use of Local Class" , under); println(); println( "TestObject with starting with 'S' and a*3 <= b" ); println(); class TestObjectForNameAndRange implements TestObjectValidator { public boolean test(TestObject t) { return t.getName().startsWith( "S" ) && t.getA() * 3 <= t.getB(); } } printTestObjects(objList, new TestObjectForNameAndRange()); println(); |
Ex. 3 - Use of Local Class ************************** TestObject with starting with 'S' and a*3 <= b TestObject (Shortstop): a: 2 b: 6 TestObject (Stingray): a: 5 b: 15 |
Example 4
Growing wiser, we realize further that there is no need to create the inner class now that JDK 8 allows anonymous classes. It's the answer to our problems! We immediately set to work refactoring.We change our code to use this anonymous class like this:
62 63 64 65 66 67 68 69 70 71 72 73 74 | //Ex. 4 - Use of Anonymous Class println( "Ex. 4 - Use of Anonymous Class" , under); println(); println( "TestObject with starting with 'S' and a*3 <= b" ); println(); printTestObjects(objList, new TestObjectValidator() { public boolean test(TestObject t) { return t.getName().startsWith( "S" ) && t.getA() * 3 <= t.getB(); } }); println(); |
Ex. 4 - Use of Anonymous Class ****************************** TestObject starting with 'S' and a*3 <= b TestObject (Shortstop): a: 2 b: 6 TestObject (Stingray): a: 5 b: 15 |
Example 5
So lets take what we know a step further. Lets introduce a lambda in place of the anonymous class. We know that we can pass a parameter into the lambda and it will produce an output. We make the following change to Example 4 above:76 77 78 79 80 81 82 83 | //Ex. 5 - Use of Lambda println( "Ex. 5 - Use of Lambda" , under); println(); println( "TestObject with starting with 'S' and a*3 <= b" ); println(); printTestObjects(objList, (TestObject t)->t.getName().startsWith( "S" ) && t.getA() * 3 <= t.getB()); println(); |
(TestObject t)->t.getName().startsWith( "S" ) && t.getA() * 3 <= t.getB() |
Why can this one expression substitute itself in place of the TestObjectValidator interface? Remember back in Example 3, we stated that a functional interface is an interface declaration with only one abstract method definition. It can have any number of default and static methods but only one abstract method to be considered a functional interface. Since there is only one abstract method, you do not need to include the name of the method in order to implement it. Because this is the case, you can substitute a lambda for the anonymous class as long as it returns the same type as the abstract method declares.
As we can see, the result of running our code is the same as we saw in Examples 3 and 4.
Ex. 5 - Use of Lambda ********************* TestObject with starting with 'S' and a*3 <= b TestObject (Shortstop): a: 2 b: 6 TestObject (Stingray): a: 5 b: 15 |
Example 6
Now lets introduce some of the java.util.function package classes added for the purpose of serving lambdas and their implementation.The first such class is the Predicate. The Predicate is a generic functional interface with only one abstract method, test(T t). Implementing the Predicate interface, the test method can do anything that results in a boolean response. Simply put, we can create a new method::
204 205 206 207 208 | private static void printTestObjectsWithPredicate(List<TestObject> list, Predicate<TestObject> filter){ for (TestObject t : list){ if (filter.test(t)) t.displayObject(); } } |
So we implement this new method in our code:
85 86 87 88 89 90 91 | //Ex. 6 - Use of Predicate in place of Lambda println( "Ex. 6 - Use of Predicate in place of Lambda" , under); println(); println( "TestObject with name containing 's', case insensitive" ); println(); printTestObjectsWithPredicate(objList, t -> t.getName().toLowerCase().contains( "s" )); println(); |
Ex. 6 - Use of Predicate in place of Lambda ******************************************* TestObject with name containing 's', case insensitive TestObject (Windsurfer): a: 1 b: 3 TestObject (Shortstop): a: 2 b: 6 TestObject (Stingray): a: 5 b: 15 TestObject (Snoopy): a: 8 b: 21 TestObject (Boss Hog): a: 34 b: 47 |
Example 7
Next we take a look at another generic functional interface, the Consumer. The Consumer can be viewed literally as the class that devours the lambda because the Consumer interface accepts a lambda which returns void. It has one abstract method, accept(T t).We define a new method which accepts a TestObject List, a Predicate and a Consumer:
210 211 212 213 214 215 | private static void processTestObjectWithPredicateAndConsumer(List<TestObject> list, Predicate<TestObject> filter, Consumer<TestObject> voidFunction) { for (TestObject t : list){ if (filter.test(t)) voidFunction.accept(t); } } |
93 94 95 96 97 98 99 | //Ex. 7 - Use of Predicate and Consumer Lambda println( "Ex. 7 - Use of Predicate and Consumer Lambda" , under); println(); println( "TestObject with name ending in 'y', case insensitive" ); println(); processTestObjectWithPredicateAndConsumer(objList, t -> t.getName().toLowerCase().endsWith( "y" ), t -> t.displayObject()); println(); |
This produces the output:
Ex. 7 - Use of Predicate and Consumer Lambda ******************************************** TestObject with name ending in 'y', case insensitive TestObject (Lefty): a: 0 b: 1 TestObject (Stingray): a: 5 b: 15 TestObject (Snoopy): a: 8 b: 21 |
Example 8
Lastly in this blog, we will cover the case where you may want to do more with your initial results and then do something on that before passing it on to the Consumer interface. This is possible with the Function<T,U> functional interface. Its interface defines one abstract method, U apply(T t). Function takes a lambda function of type T with a return type of U. So we start by defining a new method:217 218 219 220 221 222 223 224 225 | private static void processTestObjectWithPredicateFunctionAndConsumer(List<TestObject> list, Predicate<TestObject> filter, Function<TestObject,String> mapper, Consumer<String> voidFunction){ for (TestObject t : list){ if (filter.test(t)) { String out = mapper.apply(t); voidFunction.accept(out); } } } |
Implementing this method in our code:
101 102 103 104 105 106 107 | //Ex. 8 - Use of Predicate, Function and Consumer println( "Ex. 8 - Use of Predicate, Function and Consumer" , under); println(); println( "TestObject name where a >= b" ); println(); processTestObjectWithPredicateFunctionAndConsumer(objList, t->t.getA() >= t.getB(), t -> t.getName(), name -> println(name)); println(); |
The resultant output is:
Ex. 8 - Use of Predicate, Function and Consumer *********************************************** TestObject name where a >= b Pitbull Axe |