Important notice about this site

I graduated from the University of Texas at Austin in December 2020. This blog is archived and no updates will be made to it.

The other side of Java: exploring lambdas and streams

June 7, 2019

Recently, I've been trying to get acclimated with functional programming practices, especially lambda expressions. I'm still scared of diving directly into a purely functional language like Haskell, so I've decided to take it easy and deal with tamer, more common languages where lambda expressions (or anonymous functions) have been introduced. In this post, I'll talk about how we can use streams and lambda expressions to work with collections in a functional paradigm instead of a procedural paradigm. This train of thought will allow for a different way to process data that allows for asynchronous programming in Java, which is helpful for when callbacks or futures are needed.

Many web developers use anonymous functions in JavaScript, sometimes without knowing they're using a functional programming practice. In my case, I first learned how to use anonymous functions when dealing with asynchronous functions and callbacks in JavaScript a few years ago, but I did not realize this was a functional programming practice.

Recently, I've realized that one way to simplify code is to move away from a procedural paradigm and start borrowing elements of functional programming. The first way to do so in a non-functional programming language is through lambda expressions. At the same time, I've now found the need to learn how to use lambdas in Java 8 and beyond because I'm dealing with a Java library that uses callbacks, so it makes life easier to use lambdas throughout the codebase.

First, what are lambda expressions? They are anonymous functions that take in parameters, do something, and potentially return something. Whenever you're able to write a one-line method in Java with a return statement and something being done in the return statement, this is probably a perfect place to use a lambda expression.

Lambda expressions can implement methods in interfaces. In other words, the expressions define what ought to be done whenever the expression is actually being used instead of before it's called.

Let's say we have an interface with a method header:

interface Greeting {
    public String hello(String fullName);
}

Instead of creating a whole other class, let's just implement this method hello on the fly, specifically for each instance of Greeting. So, what hello() does for Greeting g1 might be different from how hello() is implemented in Greeting g2. Also, since there's only one method in this interface, we can call it a functional interface, and this gives it a special property: whenever we define the lambda expression for each Greeting object, we won't have to specify it's for the hello() method, we just specify it anonymously. Cool!

The on-the-fly implementation will be used only once, so it doesn't have to be named. Hence, it's an anonymous function. Here's an example of an implementation of hello() using a lambda expression:

class DoSomething {
    public static void main(String[] args) {
        Greeting greeting1 = (name) -> "Hello, " + name; // creating a new Greeting object here,
                                                        // and we're just defining hello() inline
        Greeting greeting2 = (name) -> "Yo, 'sup " + name + "?"; // another implementation of hello() implemented through a lambda expression
        String name1 = "John";
        System.out.println(greeting1.hello(name1)); // passes in 'John' into the anonymously defined lambda expression in the greeting1 object.
                                                   // this will print out "Hello, John"
        System.out.println(greeting2.hello(name1)); // passes in 'John' into the anonymously defined lambda expression in the greeting2 object.
                                                   // because hello() was defined differently for greeting2, this will print out "Yo, 'sup John?"
    }
}

But let's say after each greeting, we want to add on more things to it. The first greeting might say "Hello, John" but we might want to have a function that adds ", how are you?" to the end of that. This chaining is called currying. We'll use this concept to deal with processing things step by step.

So now, let's say we have a lot of people to say hello to. We'll store their names in a list. Great. Instead of using a for loop to go through this bit by bit, let's use streams and process to each member. This allows us to think about how we want to handle each member stage by stage instead of worrying about loops and getting to each member. We can do this through the Stream interface introduced in Java 8.

class DoSomething {
    public static void main(String[] args) {
        String[] names = new String[]{"John", "Mary", "Todd", "Stephen", "Cardi B"};
        Arrays.stream(names)
          .map(str -> "Hello, " + str + "!")
          .forEach(System.out::println); // equivalent to `str -> System.out.println(str)`
                                         // we can use this cool syntax because there's only one parameter to System.out.println and it can be inferred that we want to pass in the Stream's string into it.
    }
}

The output from this is:

Hello, John!
Hello, Mary!
Hello, Todd!
Hello, Stephen!
Hello, Cardi B!

By using a stream, we only have to worry about how to handle each individual element. The coordination work of handling everything as a collection is passed onto the Stream interface.

Cooler things we can do with a stream include:

  • sorting by comparison using .sort() (anything that's a Comparable or sorted by a Comparator)
  • filtering by criteria using .filter() (providing a predicate)
  • reducing with .reduce() and .collect() (using a start and accumulator model)

Let's say only people whose names have "o" in it can get a shoutout. Then we could use filter in our stream.

class DoSomething {
    public static void main(String[] args) {
        String[] names = new String[]{"John", "Mary", "Todd", "Stephen", "Cardi B"};
        Arrays.stream(names)
          .filter(name -> name.contains("o"))
          .map(str -> "Hello, " + str + "!")
          .forEach(System.out::println);
    }
}

This results in:

Hello, John!
Hello, Todd!

There are many more cool methods available in the Stream interface that I recommend you check out and see if it will be appropriate for your needs. But more importantly, thinking in this paradigm is really interesting, helps improve code concision (especially in verbose languages like Java), and gives you another way to approach problems that might even be easier than tackling it in a procedural manner (especially with asynchronous functions).

Back to blog

Back to top