The Real Benefits of Java Lambdas

Java 8 introduced the concept of Lambda expressions.

They provide a concise way to represent functions as objects.

A lambda expression is essentially an anonymous function that can be treated as an object and passed around as an argument to methods or stored in variables.

Here is an example:

// Define a functional interface with a single method
@FunctionalInterface
interface MathOperation {
  int operation(int a, int b);
}

public class LambdaExample {
  public static void main(String[] args) {
    // Define a lambda expression and assign it to a variable of the functional interface type
    MathOperation addition = (int a, int b) -> a + b;
    MathOperation subtraction = (a, b) -> a - b;
  
    // Use the lambda expression
    System.out.println("10 + 5 = " + operate(10, 5, addition));
    System.out.println("10 - 5 = " + operate(10, 5, subtraction));
  }
  
  private static int operate(int a, int b, MathOperation mathOperation) {
    return mathOperation.operation(a, b);
  }
}

In this example, the MathOperation interface defines a single method that takes two int arguments and returns an int.

The operate method takes two int arguments and an instance of the MathOperation interface, and uses the operation method of the interface to perform the desired operation.

The lambda expressions (int a, int b) -> a + b and (a, b) -> a - b provide the implementation for the operation method.

The type of the arguments is inferred by the compiler, so it can be omitted.

These lambda expressions can be assigned to variables of type MathOperation and passed to the operate method.

We are essentially passing function as objects!

And what are the real benefits of treating function as objects?

Here are some key benefits:

  1. Concise:
    • Lambdas allow you to write less code than traditional anonymous inner classes.
  2. Readability:
    • Lambdas can make your code more readable by clearly separating the implementation of a behavior from the use of that behavior.
  3. Reusable:
    • Lambdas can be stored in variables and passed as arguments, making them easily reusable.
  4. Stream API:
    • Java 8 introduced the Stream API, which allows you to process collections of data in a functional style.
    • Lambdas are used heavily in the Stream API, allowing you to perform operations on collections of data in a concise and readable way.
  5. Improved performance:
    • By allowing you to pass behavior as an argument, lambdas can improve the performance of your code by allowing the JVM to optimize your code more effectively.

Now let us see an example for each of the above benefits:

1. Concise:

Let’s say you want to filter out even numbers from a given a list of numbers.

Here is how you can do it without and with lambda:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Without lambda
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
  if (number % 2 == 0) {
    evenNumbers.add(number);
  }
}

// With lambda
List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());

As you see without lambda we are using 6 lines of code.

And with lambda just one line!

2. Readability:

Let’s say you want to sort a list of employees according to their names.

Without using lambda you may have to create a comparator object and implement compare method of that class.

Inside the compare method you need to compare the name of two employees passed as arguments to the compare method.

But with lambda , you can get rid of the “Comparator” keyword , the “compare” keyword and write it using a single line of code which is more intuitive and easy to understand as shown below:

// Without lambda
Collections.sort(employees, new Comparator<Employee>() {
  @Override
  public int compare(Employee e1, Employee e2) {
    return e1.getName().compareTo(e2.getName());
  }
});

// With lambda
Collections.sort(employees, (e1, e2) -> e1.getName().compareTo(e2.getName()));

3. Reusable:

Can you use the same function to perform two different operations and decide which one you want to execute at run time?

You can’t.

But with lambdas , you can use the same reference to perform two different operations!

This way you can reuse lambdas.

Here is an example:

MathOperation addition = (a, b) -> a + b;
MathOperation subtraction = (a, b) -> a - b;

int result1 = operate(10, 5, addition);
int result2 = operate(10, 5, subtraction);

private static int operate(int a, int b, MathOperation mathOperation) {
  return mathOperation.operation(a, b);
}

In the above example , the “MathOperation” lambda is reused to perform two diferent math operations.

It is passed like an object to the function “operate”!

4. Stream API

One of the biggest benefits of lambdas is in Stream API.

You can perform stream operations in a more readable , concise and effective way using lambdas.

Here is the same example used before:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

// Without lambda
List<Integer> evenNumbers = new ArrayList<>();
for (Integer number : numbers) {
  if (number % 2 == 0) {
    evenNumbers.add(number);
  }
}

// With lambda
List<Integer> evenNumbers = numbers.stream().filter(n -> n % 2 == 0).collect(Collectors.toList());

As you see to find the even numbers from a given list of numbers without using lambda and streams ,

you use 6 lines of code .

With lambda , it is more fluent and concise.

5. Improved Performance:

In the same example above,

The code using the Stream API is likely to perform better because the JVM can optimize the code more effectively.

The Stream API is designed to take advantage of parallel processing, which can result in significant performance improvements for large data sets.

And here are a few more benefits:

  1. Improved testing:
    • By allowing you to pass behavior as an argument, lambdas can make it easier to write unit tests, as you can pass in different behaviors to test different scenarios.
  2. Improved maintainability:
    • By making your code more concise and readable, lambdas can make it easier to maintain your code, as changes to the code are easier to understand and implement.
  3. Improved flexibility:
    • By allowing you to pass behavior as an argument, lambdas can make your code more flexible, as you can change the behavior of your code without changing the underlying code.
  4. Improved modularity:
    • By making it easier to write reusable code, lambdas can make your code more modular, as you can separate different parts of your code into separate units that can be used in different parts of your code.

Here is an example for improved modularity:

Suppose you have a class that performs various operations on a list of integers, such as finding the sum, maximum, and minimum values. Without lambdas, you might write this code as follows:

import java.util.List;

public class IntegerOperations {
  public static int sum(List<Integer> integers) {
    int sum = 0;
    for (int i : integers) {
      sum += i;
    }
    return sum;
  }

  public static int max(List<Integer> integers) {
    int max = integers.get(0);
    for (int i : integers) {
      max = Math.max(max, i);
    }
    return max;
  }

  public static int min(List<Integer> integers) {
    int min = integers.get(0);
    for (int i : integers) {
      min = Math.min(min, i);
    }
    return min;
  }
}

With lambdas, you can refactor this code to be more modular and reusable, as follows:

import java.util.List;
import java.util.function.BinaryOperator;

public class IntegerOperations {
  public static int operate(List<Integer> integers, BinaryOperator<Integer> operator) {
    int result = integers.get(0);
    for (int i : integers) {
      result = operator.apply(result, i);
    }
    return result;
  }

  public static int sum(List<Integer> integers) {
    return operate(integers, Integer::sum);
  }

  public static int max(List<Integer> integers) {
    return operate(integers, Integer::max);
  }

  public static int min(List<Integer> integers) {
    return operate(integers, Integer::min);
  }
}




In this refactored code, the operate method is a more modular implementation of the operations on the list of integers.

This method takes a BinaryOperator as an argument, which represents a binary operation that takes two arguments of the same type and returns a result of the same type.

The sum, max, and min methods are now simple wrappers that pass in the appropriate binary operator. This makes it easy to add new operations or modify existing ones, as you can simply create a new BinaryOperator and pass it to the operate method.

The code is also more reusable, as you can use the operate method in other parts of your code to perform different operations on lists of integers.

And here is another larger real world use case explained with and without lambdas.

The article takes the reader step by step in the design process replacing traditional code with lamdas:


Posted

in

by

Comments

Leave a Reply

Discover more from The Full Stack Developer

Subscribe now to keep reading and get access to the full archive.

Continue reading