Wednesday, October 24, 2018

Lambdas - More Syntactic Sugar

In the last discussion on lambdas, we learned what lambdas were and how the syntactic sugar they bring into play can be leveraged to make our development more flexible. We saw how we can define filters with a Predicate object, execute lambdas as void functions using the Consumer object and to execute value returning functions through the use of the Function object.

In this article, we are going to look at how to make use of generics to further add flexibility to our methods which accept lambdas. We will take a quick look at using named lambdas and we will finally look at the way we can use the bulk data aggregate functions provided in JDK8 to simplify our code.

We will continue to use the TestObject and ObjectTest classes in the GIT repository at java-blog-samples  to expound on the topic.

Example 9

Now we are going to introduce generics to processObjects() in order to extend what we can do with lambdas and also to further decrease the brittleness of our code.  We added the following method to cover this example:
    private static <T, U> void processObjects(Iterable<T> collection, Predicate<T> filter, Function<T, U> mapper, Consumer<U> voidFunction){
        for(T t : collection){
            if(filter.test(t)){
                U u = mapper.apply(t);
                voidFunction.accept(u);
            }
        }
    }
Dissecting the changes here, we see first that we set the first parameter to an Iterable of type <T>. Now we can pass any collection that is Iterable as the first parameter. In our case, this is the List<TestObject>. Second, we pass in our Predicate, also as type <T>. The mapper, the function returning a value, takes a type <T> input and returns it as a type <U> output. Finally, we have a Consumer that takes a type <U> input.

Now our processing of the object can take any type of collection in and perform a filter on it and each filtered object can be mapped to return a different type object and then consume that object with a void function.

We implement this in our code as:
        //Ex. 9 - Use of Generics To Add Further Flexibility
        println("Ex. 9 - Use of Generics To Add Further Flexibility", under);
        println();
        println("TestObject name where a < b");
        println();
        processObjects(objList, t -> t.getA() < t.getB(), t -> t.getName(), name -> println("TestOblject (" + name + ")"));
        println();
Running it, we see the following output:
Ex. 9 - Use of Generics To Add Further Flexibility
**************************************************

TestObject name where a < b

TestObject (Lefty)
TestObject (Windsurfer)
TestObject (Shortstop)
TestObject (Viper)
TestObject (Stingray)
TestObject (Snoopy)
TestObject (Archangel)
TestObject (Rainmaker)
TestObject (Boss Hog)
TestObject (Terror)
We can reuse this method for any iterable of any type and switch out our Predicate, Function and Consumer and we can do many things without having to code a new method to handle the conditions.

Example 10

So what we have done so far is to effectively handle void returning methods using lambdas. How often, though, do you find yourself in a situation where you need to have a sublist of your main data? Why not use a lambda filter and generics to return you a sublist of any type based on any filter?

We start by creating a method to do just this:
    private static <T, U> List<U> processObjects(Iterable<T> collection, Predicate<T> filter, Function<T,U> mapper){
        List<U> uList = new ArrayList<U>();
        for(T t : collection){
            if (filter.test(t)){
                uList.add(mapper.apply(t));
            }
        }
        return uList;
    }
Again, we can take any type of iterable as our collection to filter. We filter using the Predicate of the same type and then add the returned value from the Function, mapper, to an ArrayList of mapper's return type for return by processObjects.

Then we implement it in our code:
        //Ex. 10 - Use of Generics to Process Objects and Return Result Collection
        println("Ex. 10 - Use of Generics to Process Objects and Return Result Collection", under);
        println();
        println("TestObject count of names matched where a < b");
        println();
        List<String> results = processObjects(objList, t -> t.getA() < t.getB(), t -> t.getName());
        println("The number of results in List returned was " + results.size());
        println();
Finally, we see the following output when we run the code:
Ex. 10 - Use of Generics to Process Objects and Return Result Collection
************************************************************************

TestObject count of names matched where a < b

The number of results in List returned was 10
Note that even though the method returned a list, the display of count here is for some further examples which will do the same thing and I want to show the resulting list returned from each produces the same size list. We can confirm this is the correct count by looking back to Example 9's output and seeing that there were indeed ten TestObject objects in the list which displayed 10 objects meeting the criteria where the TestObject 'a' value was less than the TestObject 'b' value.

Example 11

Next lets look at the use of bulk data aggregate methods which can be used to simplify some of the processes we have shown. Our new method looks like this:
    private static <T> void processGenericObjects(Collection<T> collection, Predicate<T> criteria, Consumer<T> voidFunction){
        collection.stream().filter(criteria).forEach(voidFunction);
    }