JavaRush /Java Blog /Random EN /Popular about lambda expressions in Java. With examples a...
Стас Пасинков
Level 26
Киев

Popular about lambda expressions in Java. With examples and tasks. Part 1

Published in the Random EN group
Who is this article for?
  • For those who think they already know Java Core well, but have no idea about lambda expressions in Java. Or, perhaps, you’ve already heard something about lambdas, but without details.
  • for those who have some understanding of lambda expressions, but are still afraid and unusual to use them.
If you do not fall into one of these categories, you may well find this article boring, incorrect, and generally “not cool.” In this case, either feel free to pass by, or, if you are well versed in the topic, suggest in the comments how I could improve or supplement the article. The material does not claim any academic value, much less novelty. Rather, on the contrary: in it I will try to describe complex (for some) things as simply as possible. I was inspired to write by a request to explain the stream api. I thought about it and decided that without understanding lambda expressions, some of my examples about “streams” would be incomprehensible. So let's start with lambdas. Popular about lambda expressions in Java.  With examples and tasks.  Part 1 - 1What knowledge is required to understand this article:
  1. Understanding of object-oriented programming (hereinafter referred to as OOP), namely:
    • knowledge of what classes and objects are, what is the difference between them;
    • knowledge of what interfaces are, how they differ from classes, what is the connection between them (interfaces and classes);
    • knowledge of what a method is, how to call it, what an abstract method is (or a method without an implementation), what the parameters/arguments of a method are, how to pass them there;
    • access modifiers, static methods/variables, final methods/variables;
    • inheritance (classes, interfaces, multiple inheritance of interfaces).
  2. Knowledge of Java Core: generics, collections (lists), threads.
Well, let's get started.

A little history

Lambda expressions came to Java from functional programming, and there from mathematics. In the middle of the 20th century in America, a certain Alonzo Church worked at Princeton University, who was very fond of mathematics and all kinds of abstractions. It was Alonzo Church who came up with lambda calculus, which at first was a set of some abstract ideas and had nothing to do with programming. At the same time, mathematicians such as Alan Turing and John von Neumann worked at the same Princeton University. Everything came together: Church came up with the lambda calculus system, Turing developed his abstract computing machine, now known as the “Turing machine.” Well, von Neumann proposed a diagram of the architecture of computers, which formed the basis of modern computers (and is now called “von Neumann architecture”). At that time, Alonzo Church's ideas did not gain as much fame as the work of his colleagues (with the exception of the field of “pure” mathematics). However, a little later, a certain John McCarthy (also a graduate of Princeton University, at the time of the story - an employee of the Massachusetts Institute of Technology) became interested in Church's ideas. Based on them, in 1958 he created the first functional programming language, Lisp. And 58 years later, the ideas of functional programming leaked into Java as number 8. Not even 70 years have passed... In fact, this is not the longest period of time for applying a mathematical idea in practice.

The essence

A lambda expression is such a function. You can think of this as a regular method in Java, the only difference is that it can be passed to other methods as an argument. Yes, it has become possible to pass not only numbers, strings and cats to methods, but also other methods! When might we need this? For example, if we want to pass some callback. We need the method we call to be able to call some other method that we pass to it. That is, so that we have the opportunity to transmit one callback in some cases, and another in others. And our method, which would accept our callbacks, would call them. A simple example is sorting. Let's say we write some kind of tricky sorting that looks something like this:
public void mySuperSort() {
    // ... do something here
    if(compare(obj1, obj2) > 0)
    // ... and here we do something
}
Where, ifwe call the method compare(), pass there two objects that we compare, and we want to find out which of these objects is “greater”. We will put the one that is “more” before the one that is “smaller”. I wrote “more” in quotes because we are writing a universal method that will be able to sort not only in ascending but also in descending order (in this case, “more” will be the object that is essentially smaller, and vice versa). To set the rule for exactly how we want to sort, we need to somehow pass it to our mySuperSort(). In this case, we will be able to somehow “control” our method while it is being called. Of course, you can write two separate methods mySuperSortAsc()for mySuperSortDesc()sorting in ascending and descending order. Or pass some parameter inside the method (for example, booleanif true, sort in ascending order, and if falsein descending order). But what if we want to sort not some simple structure, but, for example, a list of string arrays? How will our method mySuperSort()know how to sort these string arrays? To size? By total length of words? Perhaps alphabetically, depending on the first row in the array? But what if, in some cases, we need to sort a list of arrays by the size of the array, and in another case, by the total length of words in the array? I think you've already heard about comparators and that in such cases we simply pass a comparator object to our sorting method, in which we describe the rules by which we want to sort. Since the standard method sort()is implemented on the same principle as , mySuperSort()in the examples I will use the standard one sort().
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

Comparator<String[]> sortByLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
};

Comparator<String[]> sortByWordsLength = new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        int length1 = 0;
        int length2 = 0;
        for (String s : o1) {
            length1 += s.length();
        }
        for (String s : o2) {
            length2 += s.length();
        }
        return length1 - length2;
    }
};

arrays.sort(sortByLength);
Result:
  1. mom washed the frame
  2. peace Labor may
  3. I really love java
Here the arrays are sorted by the number of words in each array. An array with fewer words is considered “smaller”. That's why it comes at the beginning. The one where there are more words is considered “more” and ends up at the end. If sort()we pass another comparator to the method (sortByWordsLength), then the result will be different:
  1. peace Labor may
  2. mom washed the frame
  3. I really love java
Now the arrays are sorted by the total number of letters in the words of such an array. In the first case there are 10 letters, in the second 12, and in the third 15. If we use only one comparator, then we can not create a separate variable for it, but simply create an object of an anonymous class right at the time of calling the method sort(). Like that:
String[] array1 = {"Mother", "soap", "frame"};
String[] array2 = {"I", "Very", "I love", "java"};
String[] array3 = {"world", "work", "May"};

List<String[]> arrays = new ArrayList<>();
arrays.add(array1);
arrays.add(array2);
arrays.add(array3);

arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
The result will be the same as in the first case. Task 1 . Rewrite this example so that it sorts the arrays not in ascending order of the number of words in the array, but in descending order. We already know all this. We know how to pass objects to methods, we can pass this or that object to a method depending on what we need at the moment, and inside the method where we pass such an object, the method for which we wrote the implementation will be called. The question arises: what do lambda expressions have to do with it? Given that a lambda is an object that contains exactly one method. It's like a method object. A method wrapped in an object. They just have a slightly unusual syntax (but more on that later). Let's take another look at this entry
arrays.sort(new Comparator<String[]>() {
    @Override
    public int compare(String[] o1, String[] o2) {
        return o1.length - o2.length;
    }
});
Here we take our list arraysand call its method sort(), where we pass a comparator object with one single method compare()(it doesn’t matter to us what it’s called, because it’s the only one in this object, we won’t miss it). This method takes two parameters, which we work with next. If you work in IntelliJ IDEA , you've probably seen how it offers you this code to significantly shorten:
arrays.sort((o1, o2) -> o1.length - o2.length);
That's how six lines turned into one short one. 6 lines were rewritten into one short one. Something has disappeared, but I guarantee that nothing important has disappeared, and this code will work exactly the same as with an anonymous class. Task 2 . Figure out how to rewrite the solution to problem 1 using lambdas (as a last resort, ask IntelliJ IDEA to turn your anonymous class into a lambda).

Let's talk about interfaces

Basically, an interface is just a list of abstract methods. When we create a class and say that it will implement some kind of interface, we must write in our class an implementation of the methods that are listed in the interface (or, as a last resort, not write it, but make the class abstract). There are interfaces with many different methods (for example List), there are interfaces with only one method (for example, the same Comparator or Runnable). There are interfaces without a single method at all (so-called marker interfaces, for example Serializable). Those interfaces that have only one method are also called functional interfaces . In Java 8 they are even marked with a special @FunctionalInterface annotation . It is interfaces with one single method that are suitable for use by lambda expressions. As I said above, a lambda expression is a method wrapped in an object. And when we pass such an object somewhere, we, in fact, pass this one single method. It turns out that it doesn’t matter to us what this method is called. All that is important to us is the parameters that this method takes, and, in fact, the method code itself. A lambda expression is, essentially. implementation of a functional interface. Where we see an interface with one method, it means that we can rewrite such an anonymous class using a lambda. If the interface has more/less than one method, then the lambda expression will not suit us, and we will use an anonymous class, or even a regular one. It's time to dig into the lambdas. :)

Syntax

The general syntax is something like this:
(параметры) -> {тело метода}
That is, parentheses, inside them are the method parameters, an “arrow” (these are two characters in a row: minus and greater), after which the body of the method is in curly braces, as always. The parameters correspond to those specified in the interface when describing the method. If the type of variables can be clearly defined by the compiler (in our case, it is known for sure that we are working with arrays of strings, because it Listis typed precisely by arrays of strings), then the type of the variables String[]need not be written.
If you are not sure, specify the type, and IDEA will highlight it in gray if it is not needed.
You can read more in the Oracle tutorial , for example. This is called "target typing" . You can give any names to the variables, not necessarily those specified in the interface. If there are no parameters, then just parentheses. If there is only one parameter, just the variable name without parentheses. We've sorted out the parameters, now about the body of the lambda expression itself. Inside the curly braces, write the code as for a regular method. If your entire code consists of only one line, you don’t have to write curly braces at all (as with ifs and loops). If your lambda returns something, but its body consists of one line, it returnis not at all necessary to write. But if you have curly braces, then, as in the usual method, you need to explicitly write return.

Examples

Example 1.
() -> {}
The simplest option. And the most meaningless one :). Because it does nothing. Example 2.
() -> ""
Also an interesting option. It accepts nothing and returns an empty string ( returnomitted as unnecessary). The same, but with return:
() -> {
    return "";
}
Example 3. Hello world using lambdas
() -> System.out.println("Hello world!")
Receives nothing, returns nothing (we cannot put returnbefore the call System.out.println(), since the return type in the method println() — void), simply displays an inscription on the screen. Ideal for implementing an interface Runnable. The same example is more complete:
public class Main {
    public static void main(String[] args) {
        new Thread(() -> System.out.println("Hello world!")).start();
    }
}
Or like this:
public class Main {
    public static void main(String[] args) {
        Thread t = new Thread(() -> System.out.println("Hello world!"));
        t.start();
    }
}
Or we can even save the lambda expression as an object of type Runnable, and then pass it to the constructor thread’а:
public class Main {
    public static void main(String[] args) {
        Runnable runnable = () -> System.out.println("Hello world!");
        Thread t = new Thread(runnable);
        t.start();
    }
}
Let's take a closer look at the moment of saving a lambda expression into a variable. The interface Runnabletells us that its objects must have a method public void run(). According to the interface, the run method does not accept anything as parameters. And it doesn't return anything (void). Therefore, when writing this way, an object will be created with some method that does not accept or return anything. Which is quite consistent with the method run()in the interface Runnable. That's why we were able to put this lambda expression into a variable like Runnable. Example 4
() -> 42
Again, it does not accept anything, but returns the number 42. This lambda expression can be placed in a variable of type Callable, because this interface only defines one method, which looks something like this:
V call(),
where Vis the type of the return value (in our case int). Accordingly, we can store such a lambda expression as follows:
Callable<Integer> c = () -> 42;
Example 5. Lambda in several lines
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Again, this is a lambda expression without parameters and its return type void(since there is no return). Example 6
x -> x
Here we take something into a variable хand return it. Please note that if only one parameter is accepted, then the parentheses around it do not need to be written. The same, but with brackets:
(x) -> x
And here is the option with an explicit one return:
x -> {
    return x;
}
Or like this, with brackets and return:
(x) -> {
    return x;
}
Or with an explicit indication of the type (and, accordingly, with parentheses):
(int x) -> x
Example 7
x -> ++x
We accept it хand return it, but for 1more. You can also rewrite it like this:
x -> x + 1
In both cases, we do not indicate parentheses around the parameter, method body, and word return, since this is not necessary. Options with brackets and return are described in example 6. Example 8
(x, y) -> x % y
We accept some хand у, return the remainder of the division xby y. Parentheses around parameters are already required here. They are optional only when there is only one parameter. Like this with explicit indication of types:
(double x, int y) -> x % y
Example 9
(Cat cat, String name, int age) -> {
    cat.setName(name);
    cat.setAge(age);
}
We accept a Cat object, a string with a name and an integer age. In the method itself, we set the passed name and age to the Cat. catSince our variable is a reference type, the Cat object outside the lambda expression will change (it will receive the name and age passed inside). A slightly more complicated version that uses a similar lambda:
public class Main {
    public static void main(String[] args) {
        // create a cat and print to the screen to make sure it's "blank"
        Cat myCat = new Cat();
        System.out.println(myCat);

        // create lambda
        Settable<Cat> s = (obj, name, age) -> {
            obj.setName(name);
            obj.setAge(age);
        };

        // call the method, to which we pass the cat and the lambda
        changeEntity(myCat, s);
        // display on the screen and see that the state of the cat has changed (has a name and age)
        System.out.println(myCat);
    }

    private static <T extends WithNameAndAge>  void changeEntity(T entity, Settable<T> s) {
        s.set(entity, "Murzik", 3);
    }
}

interface WithNameAndAge {
    void setName(String name);
    void setAge(int age);
}

interface Settable<C extends WithNameAndAge> {
    void set(C entity, String name, int age);
}

class Cat implements WithNameAndAge {
    private String name;
    private int age;

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Cat{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
Result: Cat{name='null', age=0} Cat{name='Murzik', age=3} As you can see, at first the Cat object had one state, but after using the lambda expression, the state changed. Lambda expressions work well with generics. And if we need to create a class Dog, for example, that will also implement WithNameAndAge, then in the method main()we can do the same operations with Dog, without changing the lambda expression itself at all. Task 3 . Write a functional interface with a method that takes a number and returns a Boolean value. Write an implementation of such an interface in the form of a lambda expression that returns trueif the passed number is divisible by 13 without a remainder . Task 4 . Write a functional interface with a method that takes two strings and returns the same string. Write an implementation of such an interface in the form of a lambda that returns the longest string. Task 5 . Write a functional interface with a method that accepts three fractional numbers: a, b, cand returns the same fractional number. Write an implementation of such an interface in the form of a lambda expression that returns a discriminant. Who forgot, D = b^2 - 4ac . Task 6 . Using the functional interface from task 5, write a lambda expression that returns the result of the operation a * b^c. Popular about lambda expressions in Java. With examples and tasks. Part 2.
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION