Lambda Expressions and the Stream API in Java | Generated by AI

Home PDF

Okay, let’s learn about Lambda Expressions and the Stream API in Java. These are powerful features introduced in Java 8 that enable a more functional programming style, leading to more concise and expressive code.

1. Lambda Expressions

What is a Lambda Expression?

A lambda expression is essentially an anonymous (unnamed) function. It allows you to define a function inline without explicitly declaring a separate method. Lambda expressions are particularly useful for passing behavior as data, making your code more flexible and readable, especially when working with functional interfaces.

Syntax of a Lambda Expression:

A lambda expression has the following basic syntax:

(parameter list) -> expression body

Functional Interfaces:

Lambda expressions in Java are used to implement methods defined by functional interfaces. A functional interface is an interface that contains only one abstract method. It can have default methods and static methods, but only one abstract method.

Examples of built-in functional interfaces in Java include:

Examples of Lambda Expressions:

Let’s look at some examples to understand how lambda expressions work:

Method References:

Method references are a shorthand syntax for lambda expressions that simply call an existing method. They make your code even more concise. There are four kinds of method references:

  1. Reference to a static method: ClassName::staticMethodName

    java.util.function.Function<String, Integer> stringToInt = Integer::parseInt;
    int number = stringToInt.apply("123"); // number will be 123
    
  2. Reference to an instance method of a particular object: instance::instanceMethodName

    String message = "Hello";
    java.util.function.Consumer<String> printLength = message::length; // Incorrect - Consumer takes one arg
    java.util.function.Supplier<Integer> getLength = message::length;
    int len = getLength.get(); // len will be 5
    

    Correction: The Consumer example should take an argument. Here’s a better example:

    String message = "Hello";
    java.util.function.Consumer<String> printContains = s -> message.contains(s);
    printContains.accept("ll"); // This will execute message.contains("ll")
    

    For a Supplier, it’s more like:

    String message = "Hello";
    java.util.function.Supplier<Integer> getLength = message::length;
    int len = getLength.get(); // len will be 5
    
  3. Reference to an instance method of an arbitrary object of a particular type: ClassName::instanceMethodName

    java.util.function.BiPredicate<String, String> checkStartsWith = String::startsWith;
    boolean starts = checkStartsWith.test("Java", "Ja"); // starts will be true
    
  4. Reference to a constructor: ClassName::new

    java.util.function.Supplier<String> createString = String::new;
    String emptyString = createString.get(); // emptyString will be ""
    
    java.util.function.Function<Integer, int[]> createIntArray = int[]::new;
    int[] myArray = createIntArray.apply(5); // myArray will be an int array of size 5
    

2. Stream API

What is the Stream API?

The Stream API, introduced in Java 8, provides a powerful and elegant way to process collections of data. A stream represents a sequence of elements that supports various aggregate operations. Streams are different from collections; collections are about storing data, while streams are about processing data.

Key Concepts of the Stream API:

Creating Streams:

You can create streams in various ways:

Intermediate Operations:

These operations transform or filter the stream and return a new stream. Common intermediate operations include:

Terminal Operations:

These operations produce a result or a side effect and consume the stream. Common terminal operations include:

3. Relationship Between Lambdas and Streams

Lambda expressions are heavily used with the Stream API. They provide a concise way to define the behavior for many of the intermediate and terminal operations. For example, the Predicate in filter(), the Function in map(), and the Consumer in forEach() are often implemented using lambda expressions.

Examples Combining Lambdas and Streams:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LambdaStreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

        // Filter names starting with 'A' and convert them to uppercase
        List<String> aNamesUppercase = names.stream()
                .filter(name -> name.startsWith("A")) // Lambda for filtering
                .map(String::toUpperCase)             // Method reference for mapping
                .collect(Collectors.toList());

        System.out.println("Names starting with 'A' in uppercase: " + aNamesUppercase);
        // Output: Names starting with 'A' in uppercase: [ALICE]

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

        // Find the sum of squares of even numbers
        int sumOfSquaresOfEvens = numbers.stream()
                .filter(n -> n % 2 == 0)       // Lambda for filtering even numbers
                .map(n -> n * n)              // Lambda for squaring
                .reduce(0, Integer::sum);     // Method reference for summing

        System.out.println("Sum of squares of even numbers: " + sumOfSquaresOfEvens);
        // Output: Sum of squares of even numbers: 220

        List<Person> people = Arrays.asList(
                new Person("Alice", 30),
                new Person("Bob", 25),
                new Person("Charlie", 35)
        );

        // Get a list of names of people older than 28
        List<String> olderPeopleNames = people.stream()
                .filter(person -> person.getAge() > 28) // Lambda for filtering
                .map(Person::getName)                 // Method reference for getting name
                .collect(Collectors.toList());

        System.out.println("Names of people older than 28: " + olderPeopleNames);
        // Output: Names of people older than 28: [Alice, Charlie]
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }
}

4. Parallel Streams

The Stream API also supports parallel processing. You can easily process elements of a stream in parallel using the parallelStream() method on a collection or by calling the parallel() method on a sequential stream. This can significantly improve performance for certain operations on large datasets.

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

// Calculate the sum of squares in parallel
int sumOfSquaresParallel = numbers.parallelStream()
        .map(n -> n * n)
        .reduce(0, Integer::sum);

System.out.println("Sum of squares (parallel): " + sumOfSquaresParallel);

Conclusion

Lambda expressions and the Stream API are powerful additions to Java that enable a more functional and concise way of writing code, especially when dealing with collections of data. By using lambdas with streams, you can perform complex data processing operations in a declarative and readable manner. Understanding these features is essential for modern Java development. Remember to practice using them to become more comfortable and proficient.


Back 2025.03.27 Donate