banner
biuaxia

biuaxia

"万物皆有裂痕,那是光进来的地方。"
github
bilibili
tg_channel

[Reprint] Decoding Java Lambda Expressions

title: 【转载】Demystifying Java Lambda Expressions
date: 2021-08-14 08:35:12
comment: false
toc: true
category:

  • Java
    tags:
  • 转载
  • Java
  • Lambda
  • Demystifying

This article is reprinted from: [Translation] Demystifying Java Lambda Expressions


Demystifying Java Lambda Expressions#

I seem to have spent a lot of time explaining functional programming in Java. In fact, there is nothing profound or difficult to understand. To utilize the functionality of certain functions, you need to define functions nested within functions. Why do that? When you develop in an object-oriented manner, you are already using functional programming methods, just in a controlled way. Polymorphism in Java is achieved by saving several functions that can be overridden in subclasses. This way, other functions in the class can call the overridden function, and even if the external function is not overridden, its behavior changes.

Let's take an example of polymorphism and convert it into a function using lambda expressions. The code is as follows:

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

This is a classic object-oriented example, where the Dog class and Cat class inherit and implement the Pet class. Sound familiar? In this simple example, what happens when you execute the disturb() method? The result is: the cat and dog each make their own sounds.

But what if you need a snake? What should you do? You need to create a new class, and what if you need 1000 classes? Each class requires a template file. If the Pet interface only has a simple method, Java would also consider it a function. I moved Pet out of the disturb interface (perhaps it wasn't here initially, it is not a property of Pet). As shown below:

@Slf4j
public class Application {
    interface Pet {
        String vocalize();
    }
    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);
    }
}

This quirky syntax () -> something is surprising. However, it simply defines a function that has no input parameters and returns an object. Since the Pet interface only has one method, developers can call the method in this way. Technically, it implements the Pet interface and overrides the vocalize function. But for the topic of our discussion, it is a function that can be embedded in other functions.

Since the Supplier interface can replace the Pet interface, this code can be further simplified. As shown below:

@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");
    }
}

Since Supplier is a public interface located in the java.util.function package.

These lambda functions may seem like we are conjuring them out of thin air. But behind them, we are utilizing a single function to implement the interface and provide a specific implementation of that single function.

Let's discuss another common function, Consumer. It takes a value as an input parameter and has no return value, essentially consuming that value. If you have used the forEach method of lists or stream objects, you can use Consumer here. We will collect the vocalizations of all pets into a list and call them one by one. As shown below:

@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));
    }
}

Now, if you add a bird, you just need to add () -> "chirp" to the list. Note: In the expression v -> disturbPet(v), the first v does not need parentheses. For lambda expressions with a single parameter, parentheses can be omitted.

OK, the examples I presented may not be intuitive. My goal is to start with polymorphic functions and introduce the related content of lambda expressions. When can lambda expressions be practically used? There are some examples that are universal and should be studied repeatedly. These examples are also included in the Stream library.

This example is more intuitive; I will retrieve a series of files, delete those that do not start with a dot, and get the file names and sizes. First, we need to get the array of files from the current directory and convert it to Stream type. We can use the File class to implement this:

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

Since directories are also file objects, we can perform certain operations on file objects. At the same time, “.” represents a directory, and we can also call the listFiles method. However, it returns an array, so we should use streams to process it; we need to use the Arrays.stream method to convert the array into a stream object.

Now, we delete files that start with a dot, convert the File objects into strings consisting of their names and sizes, sort them alphabetically, and write them 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));
    }
}

The two new methods for handling lambda expressions are filter and map. The filter method corresponds to the Predicate type, and the map method corresponds to the Function type, both of which belong to the java.util.function package. They both provide methods for generic operations on objects, with Predicate used to test certain characteristics of objects and Function used for transformations between objects.

Note: I also considered the case of a file. How to handle directories? What if we use recursion to go into the directory? How can we use stream objects to handle it? There is a special map that can add an internal stream object to an external stream object. What does this mean? Let's look at the following example:

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

 }

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

To use the getFiles method, we can 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));
    }

Without the full path, we must obtain the relative path from the file name through some mechanism. But now, it doesn't need to be that complicated.

Functions that take other functions as parameters are generally called higher-order functions. We have learned about several higher-order functions: forEach, filter, map, and flatMap. Each of them represents a method of operating objects in a way that is different from parameters and return values. We use lambda to perform explicit operations. Using this method, we can also chain multiple operations on a series of objects to obtain the desired result.

I hope this article can unveil the mystery of lambda functions to readers. I think that when this topic is first introduced, it can be somewhat intimidating. Of course, it is borrowed from Alonzo Church's lambda calculus, but that is another story. Now, you should understand: using this simple syntax, functions can also be conjured up out of thin air.

If you find any errors or areas for improvement in the translation, feel free to modify the translation and PR at Juejin Translation Project to earn corresponding reward points. The Permanent link to this article at the beginning is the Markdown link to this article on GitHub.


Juejin Translation Project is a community for translating high-quality internet technology articles, with sources from English sharing articles on Juejin. The content covers fields such as AndroidiOSFrontendBackendBlockchainProductDesignArtificial Intelligence and more. To see more high-quality translations, please continue to follow Juejin Translation Project, Official Weibo, and Zhihu Column.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.