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: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(); } }We implement the method in our code:
//Ex. 1 - Hardcoded Result println("Ex. 1: Hardcoded Result", under); println(); println("TestObject with a > 20 (hardcoded)"); println(); printObjectsWithAGreaterThanTwenty(objList); println();When run, we get the following result:
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: 82This works really great. Every time we need to know which objects contain an 'a' value greater than twenty, we have it! But that means every time we have to search we have to create a method. This makes our code very inflexible but we move on.
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.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(); } } }We even push its capability by adding the addition of an enum to determine which, if any, of the low and high limits should be included in the result.
private enum Inclusivity { START, END, BOTH, NONE }So we implement it in our code:
//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();and when we run it, we get this:
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: 70Again, we have made a good effort but we notice for every requirement, we are adding a new method to support the criteria being searched. What if the type of object in the result set is not a TestObject? Our code got better but it is still inflexible and can be broken easily.
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).// Functional Interfaces interface TestObjectValidator { boolean test(TestObject t); }We also create a method which takes in a TestObjectValidator.
// Functional Interfaces interface TestObjectValidator { boolean test(TestObject t); }We then create an inner class and implement the TestObjectValidator whenever we want to provide a test condition as we did when we needed to print the TestObjects when the name field starts with "s" (case insensitive) and where three times its 'a' value is less than or equal to its 'b' value.
//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();This then produces the output:
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: 15But this is still clunky and fragile. We still find ourselves writing extra inner classes for each test case.
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:
//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();Which still yields the same results as in Example 3:
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: 15This is better than the inner class because we code the anonymous class as we need it. It is only better when there is no other time that the same test is being performed in the span of the class or application. This is worse than the inner class as it is not maximizing reusability of the code.
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://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();Our lambda is this:
(TestObject t)->t.getName().startsWith("S") && t.getA() * 3 <= t.getB()What this says is "for a given TestObject, return true if the TestObject.name starts with 'S' and three times the value of TestObject.a is less than or equal to the value of TestObject.b otherwise, return false".
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: 15Our code is now becoming a bit more flexible. We gain the simplicity of not having to define the whole anonymous class. This is the first bit of "syntactic sugar" we see as a result of lambdas.
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::
private static void printTestObjectsWithPredicate(List<TestObject> list, Predicate<TestObject> filter){ for(TestObject t : list){ if(filter.test(t)) t.displayObject(); } }Now, our method can take any Predicate typed with a TestObject and a lambda can be passed without need for a method name and every object in the List of TestObject objects passed will be tested for the Predicate lambda and as our method says, it will call TestObject.displayObject() on that TestObject.
So we implement this new method in our code:
//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();When we run it, we find our Predicate has displayed each TestObject in the List that contained a case insensitive 's':
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: 47This makes our code less brittle but we are still bound to the one outcome for a given method definition. Or are we?
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:
private static void processTestObjectWithPredicateAndConsumer(List<TestObject> list, Predicate<TestObject> filter, Consumer<TestObject> voidFunction) { for (TestObject t : list){ if(filter.test(t)) voidFunction.accept(t); } }We then implement the method in our code:
//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();The Predicate lambda performs a case-insensitive test on each TestObject to see if the TestObject.name field ends with a 'y'. It then passes that TestObject to the Consumer. The Consumer lambda invokes the TestObject.displayObject() method on it.
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: 21We now have the flexibility to use any Predicate<TestObject> lambda and any Consumer<TestObject> lambda. We therefore changed the name from print... to process because any Consumer could be passed which may or may not print the TestObject.
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: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); } } }We see this method will perform the test defined by the Predicate Lambda and pass the TestObject match to the Function. The Function lambda defined must take a TestObject in and return a String. Finally, the String is passed to the Consumer which performs some void function on it.
Implementing this method in our code:
//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 Predicate lambda in this is looking for any TestObject in the list that has a TestObject.a value greater than or equal to its TestObject.b value. If the Predicate test is true, the TestObject has the TestObject.getName() method on it (which returns a string). That String is then passed to the Consumer wihch then calls the com.blogspot.howdoidothatinjava.utilities.PrintUtilities.println(String s) method.
The resultant output is:
Ex. 8 - Use of Predicate, Function and Consumer *********************************************** TestObject name where a >= b Pitbull AxeThe lambdas can be defined for any type of Predicate, Function and Consumer action desired making the method very versatile and simplifies reuse in many different situations. Currently, there is also available a BiFunction<T, R, S> functional interface which takes two input parameters and has the return type of S. I am not sure if this is going to be extended out to further functional interfaces with even greater numbers of parameters. There is certainly nothing preventing the developer from creating a function interface of their own that does this.