• Demystifying Java Lambda Expressions
  • Original author: Randal Kamradt Sr
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: samyu2000
  • Proofread by: Kimhooo, 1autodidact

Decrypt Java Lambda expressions

I seem to have spent a lot of time explaining functional programming in Java. There is nothing esoteric about it. In order to use the functionality of some functions, you need to define functions nested within them. Why do you do that? When you develop in an object-oriented way, you are already using functional programming, but in a controlled way. Polymorphism in Java is achieved by holding several functions that can be overridden in subclasses. In this way, other functions of the class can call the overwritten function, even if the outer function is not overwritten and its behavior changes.

Let’s take a polymorphic example and convert it to a function that uses lambda expressions. The code is as follows:

@Slf4j
public class Application {
    static abstract class Pet {
        public abstract String vocalize(a);
        public void disturb(a) { log.info(vocalize()); }}static class Dog extends Pet {
        public String vocalize(a) { return "bark"; }}static class Cat extends Pet {
        public String vocalize(a) { return "meow"; }}public static void main(String [] args)  {
        Pet cat = new Cat();
        Pet dog = newDog(); cat.disturb(); dog.disturb(); }}Copy the code

This is a classic object-oriented example of the Dog and Cat classes inheriting from Pet classes. Sound familiar? In this no-fuss example, how does the program work if you do the disturb() method? The result: cats and dogs each make their own barks.

But what if you need a snake? You need to create a new class. What if you need 1,000 classes? Template files are required for each class. If the Pet interface had only one simple method, Java would treat it as a function. I moved Pet out of the Disturb interface (maybe it wasn’t here initially, it wasn’t a Pet property). As follows:

@Slf4j
public class Application {
    interface Pet {
        String vocalize(a);
    }
    static void disturbPet(Pet p) {
        log.info(p.vocalize());
    }
    public static void main(String [] args)  {
        Pet cat = () -> "meow";
        Pet dog = () -> "bark";
        Pet snake = () -> "hiss"; disturbPet(cat); disturbPet(dog); disturbPet(snake); }}Copy the code

This weird grammar () -> something is amazing. However, it simply defines a function that has no input arguments and returns objects. Since the Pet interface has only one method, developers can invoke methods in this way. From a technical point of view, it implements the Pet interface and overwrites the VOCalize function. But for the purposes of our discussion, it is a function that can be embedded with other functions.

This code can be further simplified because the Supplier interface can replace the Pet interface. As follows:

@Slf4j
public class Application {
    static void disturbPet(Supplier<String> petVocalization) {
        log.info(petVocalization.get());
    }
    public static void main(String [] args)  {
        disturbPet(() -> "meow");
        disturbPet(() -> "bark");
        disturbPet(() -> "hiss"); }}Copy the code

Because Supplier is a public interface, it is in the java.util.function package.

These lambda functions look like we made them up. But behind the scenes, we are using a single function to implement the interface and provide a concrete implementation of a single function.

Let’s talk about another public function, Consumer. It takes a value as an input parameter, returns no value, and essentially consumes the value. If you’re already using the forEach method for a list or stream object, you can use Consumer here. We collect all the pet sounds, put them in a list, and call them one by one. As follows:

@Slf4j
public class Application {
    static void disturbPet(Supplier<String> petVocalization) {
        log.info(petVocalization.get());
    }
    public static void main(String [] args)  {
        List<Supplier<String>> yourPetVocalizations = List.of(
                () -> "bark", () - >"meow", () - >"hiss"); yourPetVocalizations.forEach(v -> disturbPet(v)); }}Copy the code

Now, if you add a bird, just add () -> “chirp” to the list. Note that the first v in the v -> disturbPet(v) expression is not parentheses. For lambda expressions that contain a single argument, the parentheses may be left out.

OK, the example I showed you is not intuitive. My goal is to start with polymorphic functions and introduce lambda expressions. When can lambda expressions actually be used? There are some examples that are universal and should be studied over and over again. These examples are also included in the Stream library.

This is an intuitive example. I’ll get a list of files, delete those that don’t start with a dot, and get the filename and size of the file. The first step is to get the array of files from the current directory and convert it to Stream. We can implement this using the File class:

File dir = new File(".");
Stream s = Arrays.stream(dir.listFiles());
Copy the code

Since a directory is also a file object, we can perform certain file-object operations on it. At the same time, “.” To represent a directory, we can also call the listFiles method. But it returns an array, so it should be a stream, and we need to use the arrays.stream method to turn the array into a stream object.

Now, we delete the File that starts with the dot, turn the File object into a string of its name and size, alphabetically, and write to the log.

public class Application {
    public static void main(String [] args)  {
        File dir = new File("."); Arrays.stream(dir.listFiles()) .filter(f -> f.isFile()) .filter(f -> ! f.getName().startsWith("."))
                .map(f -> f.getName() + ""+ f.length()) .sorted() .forEach(s -> log.info(s)); }}Copy the code

Two new ways to handle lambda expressions are filter and map. The filter method equals the Predicate type and the map method equals the Function type, both of which belong to the java.util. Function package. They both provide common methods for manipulating objects, Predicate for testing certain characteristics of an object, and Function for converting between objects.

Note: I also consider the case of a file. How do I deal with directories? What happens if you use recursion into a directory? How do you handle it with stream objects? There is a special map that adds an internal flow object to an external flow object. What does that mean? Let’s look at the following example:

public static Stream<File> getFiles(File file) {
     returnArrays.stream(file.listFiles()) .filter(f -> ! f.getName().startsWith("."))
             .flatMap(f -> {
                if(f.isDirectory()) {
                    return getFiles(f);
                } else {
                    returnStream.of(f); }}); }Copy the code

As you can see, the lambda expression used in the flatMap method performs recursion if the file object is a directory, and returns the file object itself if it is not. The return value of the lambda expression of the flatMap method must be some stream object. So in the case of a single file, we need to use stream.of () to match the return value type. You should also note that lambda expressions are enclosed in curly braces, so if you need to return an object, you should add a return statement.

To use the getFiles method, add it to the main method.

public static void main(String [] args)  {
        File dir = new File(".");
        getFiles(dir)
                .map(f -> f.getAbsolutePath()
                     .substring(dir.getAbsolutePath().length())
                     + "" + f.length())
                .sorted()
                .forEach(s -> log.info(s));
    }
Copy the code

In the absence of a full path, we must have some mechanism to get the relative path in the file name. But now, it doesn’t have to be that complicated.

Functions that take other functions as arguments are generally called higher-order functions. We have seen several higher-order functions: forEach, filter, map, and flatMap. Each of them represents a way to manipulate objects in an abstract way other than parameters and return values. When we use lambda, we want to do something explicit. In this way, we can also concatenate multiple operations on a series of objects to get the desired result.

I hope this article will shed some light on the mystery of lambda functions. I think when it was first introduced, it was a little scary in itself. It was borrowed, of course, from Alonzo Church’s lambda calculus, but that’s another story. By now, you should know that functions can be created out of thin air using this simple syntax.

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.