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
- Original link: Demystifying Java Lambda Expressions
- Original author: Randal Kamradt Sr
- Translation from: Juejin Translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: samyu2000
- Proofreader: Kimhooo, 1autodidact
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 Android、iOS、Frontend、Backend、Blockchain、Product、Design、Artificial Intelligence and more. To see more high-quality translations, please continue to follow Juejin Translation Project, Official Weibo, and Zhihu Column.