Lambda Usage with Collections and Method References

Version 2

    by Mohan Basavarajappa

     

    Explore how to use lambda expressions with existing Java APIs and with method referencing to increase code reuse and improve your productivity.

     

    This article is in continuation of the “Getting Started with Lambda Expressions” article, which explains the basics of lambda expressions and their usage. Here, we will explore other aspects of using lambda expressions, such as how to use them with the Java collection classes and with method referencing.

     

    Using Lambda Expressions with Collection Classes

    As of Java 8, the Java utility framework supports streaming functionality for processing the elements of a collection. The Stream API is new feature introduced with the Java Collections Framework to support and take advantage of a functional programming style with lambda expressions. The package java.util.function defines several functional interfaces that are used by collection classes and can be readily used in day-to-day programming. Collection classes and stream references obtained on collections expose methods that accept lambda expressions for performing operations on collections.

     

    Streams are an extremely interesting addition that help process the elements of a collection in various ways and support parallel operations. This topic demands explicit exploration in itself (in a separate article).

     

    Collection classes expose methods such as removeIf(Predicate condition), anyMatch(Predicate condition), allMatch(Predicate condition), and others that support lambda expressions for processing collection data.

     

    Listing 1 defines the class FruitList to hold a collection and perform operations on collection classes.

     

    package com.j2se8.samples.lambda.collections;

     

    import java.util.ArrayList;

    import java.util.List;

    import java.util.function.Predicate;

    import java.util.stream.Stream;

     

    public class FruitList {

     

       private List<String> fruits = null;

      

       public FruitList() {

          fruits = new ArrayList<String>();

       }

     

       public List<String> getFruits() {

          return fruits;

       }

     

       public void setFruits(List<String> fruits) {

          this.fruits = fruits;

       }

      

       public void addToBasket(String fruit) {

          fruits.add(fruit);

       }

      

       public boolean fruitExists(Predicate<String> fruitExists) {

         

          if(fruitExists==null) {

             new NullPointerException("null predicate");

          }

         

          Stream<String> listStream = fruits.stream();

          boolean exists = listStream.anyMatch(fruitExists);

         

          return exists;

       }

      

       public boolean sameFruits(Predicate<String> sameFruits) {

          if(sameFruits==null) {

             new NullPointerException("null predicate");

          }

         

          Stream<String> listStream = fruits.stream();

          boolean allSame = listStream.allMatch(sameFruits);

         

          return allSame;

       }

      

    }

    Listing 1. Class FruitList holds a collection and performs operations on collection classes

     

    Now we will see how we can add elements to a collection and make use of lambda expressions to verify whether a collection contains a specific value or whether an entire collection consists of the same value. In Listing 1, method fruitExists(Predicate<String> fruitExists) and method sameFruits(Predicate<String> sameFruits) obtain stream references to a collection. Listing 2 demonstrates client code that performs a required operation on the collection.

     

    package com.j2se8.samples.lambda.collections;

     

    import java.util.function.Predicate;

     

    public class CollectionElementCheckDemo {

     

       public void evaluate() {

          Predicate<String> apples = (p) -> {

             return p.equalsIgnoreCase("apple");

          };

     

         

          FruitList fruits = new FruitList();

          fruits.addToBasket("apple");

          fruits.addToBasket("peach");

          fruits.addToBasket("orange");

          fruits.addToBasket("orange");

          fruits.addToBasket("pineapple");

          fruits.addToBasket("banana");

         

          boolean appleAvailable = fruits.fruitExists(apples);

          boolean allApples = fruits.sameFruits(apples);

         

          System.out.println("Apple in list? : "+ appleAvailable);

          System.out.println("All same fruits? : "+ allApples);

          System.out.print("\n");

      

          // clearing previous list entries

          fruits.getFruits().clear();

          // adding new entries to list

          fruits.addToBasket("avocado");

          fruits.addToBasket("avocado");

          fruits.addToBasket("avocado");

          fruits.addToBasket("avocado");

         

          boolean avocadoAvailable = fruits.fruitExists(a -> a.equalsIgnoreCase("avocado"));

          boolean allAvocados = fruits.sameFruits(a -> a.equalsIgnoreCase("avocado"));

         

          System.out.println("Avocados in list? : "+ avocadoAvailable);

          System.out.println("All same fruits? : "+ allAvocados);

       }

     

       public static void main(String... args) {

         

          CollectionElementCheckDemo demo = new CollectionElementCheckDemo();

          demo.evaluate();

       }

    }

    Listing 2. Client code that performs a required operation on the collection

     

    The code in Listing 2 adds data to a collection, defines a predicate, and passes the predicate to methods that accept a lambda expression. The output shown below is obtained by executing CollectionElementCheckDemo in Listing 2. The condition to be evaluated and met is passed by the client code, whereas, going through each element of the list and evaluating the elements is taken care of by the corresponding list implementation.

     

    Apple in list? : true

    All same fruits? : false

     

    Avocados in list? : true

    All same fruits? : true

     

    The client code in Listing 3 demonstrates the implementation of code for removing elements from a collection using a lambda expression. After the removal, the collection data is printed using the forEach method of the Stream API. For printing each element of a collection, we’re using the java.util.function.Consumer functional interface. A lambda expression defined with the Consumer API obtains a reference to the collection data from each element one by one so we can perform the required operation on the collection data. The Predicate API helps to evaluate conditional expressions and returns a boolean outcome.

     

    package com.j2se8.samples.lambda.collections;

     

    import java.util.function.Predicate;

     

    public class CollectionElementRemovalDemo {

      

       public void evaluate() {

          Predicate<String> exists = (p) -> {

             return p.equalsIgnoreCase("apple");

          };

         

          FruitList fruits = new FruitList();

          fruits.addToBasket("apple");

          fruits.addToBasket("peach");

          fruits.addToBasket("orange");

          fruits.addToBasket("orange");

          fruits.addToBasket("pineapple");

          fruits.addToBasket("banana");

         

          boolean remove = fruits.getFruits().removeIf(exists);

         

          System.out.println("Apple removed from list? : "+ remove);

          System.out.print("\n");

          fruits.getFruits().forEach(c -> System.out.println(c));

       }

     

       public static void main(String... args) {

         

          CollectionElementRemovalDemo demo = new CollectionElementRemovalDemo();

          demo.evaluate();

       }

    }

    Listing 3. Code for removing elements from a collection

     

    Here is the output from Listing 3:

     

    Apple removed from list? : true

     

    peach

    orange

    orange

    pineapple

    banana

     

    The code in Listing 4 demonstrates another usage of the java.util.functional.Consumer interface as a lambda expression. The expression that is defined evaluates every element to check for the specific country type and then uses an appropriate country-specific greeting. This implementation shows that the Consumer API can be used for complex operations not just printing elements to an output console as was shown in Listing 3.

     

    package com.j2se8.samples.collections;

     

    import java.util.ArrayList;

    import java.util.function.Consumer;

     

    public class CollectionElementProcessDemo {

     

       public void evaluate() {

          Consumer<String> listEntries = (l) -> {

             System.out.println("processing : "+ l);  

             switch (l) {

             case "USA":

                System.out.println("Hello " + l);

                break;

             case "Spain":

                System.out.println("Hola " + l);

                break;

             case "France":

                System.out.println("Bonjour " + l);

                break;

             case "India":

                System.out.println("Namaste " + l);

                break;

             case "China":

                System.out.println("你好 " + l);

                break;

             }

             System.out.print("\n");

          };

     

          ArrayList<String> countryList = new ArrayList<String>();

          countryList.add("India");

          countryList.add("USA");

          countryList.add("Thailand");

          countryList.add("France");

          countryList.add("Spain");

          countryList.add("China");

          countryList.add("abc");

     

          System.out.print("\n"); // evaluates each element of collection

          countryList.stream().forEach(listEntries);

       }

     

       public static void main(String... args) {

     

          CollectionElementProcessDemo demo = new CollectionElementProcessDemo();

          demo.evaluate();

       }

    }

    Listing 4. Using the Consumer API to evaluate every element

     

    Here is the output from Listing 4:

     

    processing : India

    Namaste India

     

    processing : USA

    Hello USA

     

    processing : Thailand

     

    processing : France

    Bonjour France

     

    processing : Spain

    Hola Spain

     

    processing : China

    你好 China

     

    processing : abc

     

    The code in Listing 2, Listing 3, and Listing 4 describes an interesting phenomenon, where a data structure API holds data, knows how to navigate through the data, and evaluates each element of the data against a specified condition, whereas before Java 8, an API held the data and exposed methods for retrieving the data elements and then the programmer wrote code to retrieve and evaluate the elements. Evaluating every element of a collection for a specific condition constitutes a generic use case and having such a generic implementation with the data/API helps other users take advantage of the implementation and reuse it.

     

    Using Lambda Expressions and Method References

    As we already know, lambda expressions are a way of expressing anonymous method implementations. Method references are a new feature introduced in Java 8 and they complement lambda expressions. Methods references define syntax for invoking an already defined method. Invoking a method is the common ground on which lambda expressions (which invoke an anonymous method) and method references (which invoke a named method) are built, making method referencing a substitute for methods that accept lambda expressions, for example, methods accepting an instance of a functional interface.

     

    The code in Listing 5 demonstrates the usage of method referencing as an alternative to lambda expressions, and it highlights the different ways that a method accepting a functional interface as a parameter can be called.

     

    package com.j2se8.samples.lambda.methodref;

     

    import java.util.Comparator;

     

    public class FruitComparator implements Comparator<String> {

     

       @Override

       public int compare(String s1, String s2) {

     

          int compare = s1.compareToIgnoreCase(s2);

          return compare;

       }

     

    }

     

    package com.j2se8.samples.lambda.methodref;

     

    public class NonFunctionalFruitComparator {

     

       public int compare2(String s1, String s2) {

          int compare = s1.compareToIgnoreCase(s2);

          return compare;     

       }

    }

     

    package com.j2se8.samples.lambda.methodref;

     

    import java.util.Arrays;

    import java.util.List;

     

    public class LambdaMethodReferenceDemo {

     

       public static void main(String[] args) {

     

          String[] fruits = { "apple", "mango", "banana", "peach", "pomegranate" };

          FruitComparator fruitComparator = new FruitComparator();

    NonFunctionalFruitComparator nonFuntionalComparator = new NonFunctionalFruitComparator();

          List<String> fruitsList = null;

     

          // pre-java 8 way of using comparator

          Arrays.sort(fruits, fruitComparator);

          fruitsList = Arrays.asList(fruits);

          System.out.print("pre java8 way :- ");

          fruitsList.forEach(s -> System.out.print(s + " "));

          System.out.println();

     

          // inline comparator using lambda expression syntax

          Arrays.sort(fruits, (s1, s2) -> s1.compareToIgnoreCase(s2));

          fruitsList = Arrays.asList(fruits);

          System.out.print("lambda exp use :- ");

          fruitsList.forEach(s -> System.out.print(s + " "));

          System.out.println();

     

          // Using method referencing syntax. Here we are using the syntax of the instance

          // variable name, followed by ::, followed by an abstract method name

          // defined in a functional interface

          Arrays.sort(fruits, fruitComparator::compare);

          fruitsList = Arrays.asList(fruits);

          System.out.print("method ref use :- ");

          fruitsList.forEach(s -> System.out.print(s + " "));

     

          // Using method referencing syntax. Here we are using the syntax of the instance

          // variable name, followed by ::, followed by an existing method name

          // defined in the class NonFunctionalFruitComparator class.

          Arrays.sort(fruits, nonFuntionalComparator::compare2);

          fruitsList = Arrays.asList(fruits);

          System.out.print("method ref use :- ");

          fruitsList.forEach(s -> System.out.print(s + " "));

       }

    }

    Listing 5. Using method referencing as an alternative to lambda expressions

     

    The code in Listing 5 shows that method referencing allows code reuse in a clean way. Looking at the usage of the Comparator interface, it might be confusing to someone using functional interfaces, because more than one method is defined. If so, refer to the documentation for FunctionalInterface and Comparator.

     

    In Listing 5, there are two method referencing cases: one invoking fruitComparator::compare, and another invoking nonFuntionalComparator::compare2. Although different methods on different objects were called, the result is the same.

     

    It seems misleading that an instance of the NonFunctionalFruitComparator class worked as a comparator for the Arrays.sort method, but this is where lambda expression rules come into play. Though class NonFunctionalFruitComparator doesn’t follow or implement a contract defined by the Comparator interface, the compare2 method’s implementation is treated as an anonymous implementation, per the lambda rules.

     

    This section demonstrated how to leverage existing implementations while using functional interfaces and also how to use method references as a substitute for lambda expressions.

     

    Conclusion

    In this article, I have tried to cover use cases where lambda expressions can be used with existing Java APIs to write better code and take advantage of implementations that are readily available with Java 8. I also covered the use of method referencing in cases where functional interfaces are accepted as parameters to inject your existing implementation for libraries that use functional programming. We can use these techniques to our advantage to reuse code and improve programming productivity.

     

    See Also

    Part 1 of this series: “Getting Started with Lambda Expressions

     

    About the Author

    Mohan Basavarajappa is a technical architect at Infosys. His areas of interest include Java SE, Java EE, and enterprise content management technologies, such as Oracle WebCenter Content and Oracle WebCenter Sites. Previously, he held the role of a reuse practitioner aiming to increase productivity and reduce time to market by leveraging open source technologies and building organization-wide reusable libraries. He is an open source enthusiast and loves to explore open source frameworks and libraries. He is an active Oracle Technology Network member and you can catch him at https://community.oracle.com/people/Mohan%20Basavarajappa.