Java Lambda Expressions

tech, java, programming,

References:

Background

What are functional interfaces?

A functional interface is an interface that contains only one abstract method. They can have only one functionality to exhibit. The following explains how to write functional interfaces:

@FunctionalInterface Annotation

This annotation is used to ensure that the functional interface can’t have more than one abstract method. In case more than one abstract methods are present, the compiler flags an ‘Unexpected @FunctionalInterface annotation’ message. However, it is not mandatory to use this annotation.

Example:

@FunctionalInterface
interface Square 
{ 
    int calculate(int x); 
} 

What is a Lambda Expression

Lambda expression is used to express instances of functional interfaces. Lambda expressions implement the only abstract function and therefore implement functional interfaces.

Syntax:

lambda operator -> body

where lamda operator can be:

  • zero parameter

    () -> return expression

  • one parameter

    (p) -> return expression

  • multi parameter

    (p1, p2,...) -> return expression

The following is an old-style anonymous function usage

FileFilter fileFilter = new FileFilter() {

    @Override
    public boolean accept(File file){
        return file.getName().endsWith(".java");
    }
}

New style Java 8 lambda expression

FileFilter filter = (File file) -> file.getName().endsWith(".java");

This expression can now be used with a collection and applied to it.

Sample Lambda on string streams

The following are example of stream lambda’s on string streams()

forEach

forEach() is simplest and most common operation; it loops over the stream elements, calling the supplied function on each element.

The method is so common that is has been introduced directly in Iterable, Map etc:

@Test public void whenIncrementSalaryForEachEmployee_thenApplyNewSalary() {
empList.stream().forEach(e -> e.salaryIncrement(10.0));

assertThat(empList, contains(
  hasProperty("salary", equalTo(110000.0)),
  hasProperty("salary", equalTo(220000.0)),
  hasProperty("salary", equalTo(330000.0))
)); } This will effectively call the salaryIncrement() on each element in the empList.

forEach() is a terminal operation, which means that, after the operation is performed, the stream pipeline is considered consumed, and can no longer be used. We’ll talk more about terminal operations in the next section.


map

map() produces a new stream after applying a function to each element of the original stream. The new stream could be of different type.

The following example converts the stream of Integers into the stream of Employees:

@Test public void whenMapIdToEmployees_thenGetEmployeeStream() { Integer[] empIds = { 1, 2, 3 };

List<Employee> employees = Stream.of(empIds)
  .map(employeeRepository::findById)
  .collect(Collectors.toList());

assertEquals(employees.size(), empIds.length); } Here, we obtain an Integer stream of employee ids from an array. Each Integer is passed to the function employeeRepository::findById() – which returns the corresponding Employee object; this effectively forms an Employee stream.

collect

We saw how collect() works in the previous example; its one of the common ways to get stuff out of the stream once we are done with all the processing:

@Test public void whenCollectStreamToList_thenGetList() { List employees = empList.stream().collect(Collectors.toList());

assertEquals(empList, employees); } collect() performs mutable fold operations (repackaging elements to some data structures and applying some additional logic, concatenating them, etc.) on data elements held in the Stream instance.

The strategy for this operation is provided via the Collector interface implementation. In the example above, we used the toList collector to collect all Stream elements into a List instance.


filter

Next, let’s have a look at filter(); this produces a new stream that contains elements of the original stream that pass a given test (specified by a Predicate).

Let’s have a look at how that works:

@Test public void whenFilterEmployees_thenGetFilteredStream() { Integer[] empIds = { 1, 2, 3, 4 };

List<Employee> employees = Stream.of(empIds)
  .map(employeeRepository::findById)
  .filter(e -> e != null)
  .filter(e -> e.getSalary() > 200000)
  .collect(Collectors.toList());

assertEquals(Arrays.asList(arrayOfEmps[2]), employees); } In the example above, we first filter out null references for invalid employee ids and then again apply a filter to only keep employees with salaries over a certain threshold.

findFirst

findFirst() returns an Optional for the first entry in the stream; the Optional can, of course, be empty:

@Test public void whenFindFirst_thenGetFirstEmployeeInStream() { Integer[] empIds = { 1, 2, 3, 4 };

Employee employee = Stream.of(empIds)
  .map(employeeRepository::findById)
  .filter(e -> e != null)
  .filter(e -> e.getSalary() > 100000)
  .findFirst()
  .orElse(null);

assertEquals(employee.getSalary(), new Double(200000)); } Here, the first employee with the salary greater than 100000 is returned. If no such employee exists, then null is returned.

toArray

We saw how we used collect() to get data out of the stream. If we need to get an array out of the stream, we can simply use toArray():

@Test public void whenStreamToArray_thenGetArray() { Employee[] employees = empList.stream().toArray(Employee[]::new);

assertThat(empList.toArray(), equalTo(employees)); } The syntax Employee[]::new creates an empty array of Employee – which is then filled with elements from the stream.

flatMap

A stream can hold complex data structures like Stream<List>. In cases like this, flatMap() helps us to flatten the data structure to simplify further operations:

@Test public void whenFlatMapEmployeeNames_thenGetNameStream() { List<List> namesNested = Arrays.asList( Arrays.asList("Jeff", "Bezos"), Arrays.asList("Bill", "Gates"), Arrays.asList("Mark", "Zuckerberg"));

List<String> namesFlatStream = namesNested.stream()
  .flatMap(Collection::stream)
  .collect(Collectors.toList());

assertEquals(namesFlatStream.size(), namesNested.size() * 2); } Notice how we were able to convert the Stream<List<String>> to a simpler Stream<String> – using the flatMap() API.

peek

We saw forEach() earlier in this section, which is a terminal operation. However, sometimes we need to perform multiple operations on each element of the stream before any terminal operation is applied.

peek() can be useful in situations like this. Simply put, it performs the specified operation on each element of the stream and returns a new stream which can be used further. peek() is an intermediate operation:

@Test public void whenIncrementSalaryUsingPeek_thenApplyNewSalary() { Employee[] arrayOfEmps = { new Employee(1, “Jeff Bezos”, 100000.0), new Employee(2, “Bill Gates”, 200000.0), new Employee(3, “Mark Zuckerberg”, 300000.0) };

List<Employee> empList = Arrays.asList(arrayOfEmps);

empList.stream()
  .peek(e -> e.salaryIncrement(10.0))
  .peek(System.out::println)
  .collect(Collectors.toList());

assertThat(empList, contains(
  hasProperty("salary", equalTo(110000.0)),
  hasProperty("salary", equalTo(220000.0)),
  hasProperty("salary", equalTo(330000.0))
)); } Here, the first peek() is used to increment the salary of each employee. The second peek() is used to print the employees. Finally, collect() is used as the terminal operation.