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 quite well, but have no idea about lambda expressions in Java. Or, perhaps, you have already heard something about lambdas, but without details.
  • for those who have some understanding of lambda expressions, but using them is still scary and unusual.
If you do not fall into one of these categories, this article may well seem boring, incorrect, and generally “not ice” to you. 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 someone) 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 for starters, 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, objects are, what is the difference between them;
    • knowledge of what interfaces are, how they differ from classes, what is the relationship between them (interfaces and classes);
    • knowledge of what a method is, how to call it, what an abstract method (or a method without implementation) is, what are method parameters / arguments, 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: generic types (generics), collections (lists), threads (threads).
Well, let's get started.

A bit of 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 the 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 a system of lambda calculus, Turing developed his abstract computer, now known as the “Turing machine”. Well, von Neumann proposed a scheme for the architecture of computers, which formed the basis of modern computers (and is now called "von Neumann architecture"). At that time, the ideas of Alonzo Church did not gain such high-profile fame as the work of his colleagues (with the exception of the field of "pure" mathematics). However, a little later, someone 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 at number 8. Not even 70 years have passed ... In fact, not the longest period of application of a mathematical idea in practice. 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 at number 8. Not even 70 years have passed ... In fact, not the longest period of application of a mathematical idea in practice. 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 at number 8. Not even 70 years have passed ... In fact, not the longest period of application of a mathematical idea in practice.

essence

A lambda expression is such a function. You can think of it as a normal method in Java, only its peculiarity is that it can be passed to other methods as an argument. Yes, it became possible to pass not only numbers, strings and cats to methods, but also other methods! When might we need it? For example, if we want to send some callback. We need the method we are calling to be able to call some other method that we pass to it. That is, so that we have the opportunity in some cases to send one callback, and in others - another. 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(), we pass two objects there, which we compare, and we want to know which of these objects is "greater". The one that is “more” we will put before the one that is “less”. I wrote “greater than” in quotes because we are writing a universal method that will be able to sort not only in ascending order, but also in descending order (in this case, “greater” will be the object that is essentially less, 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 can somehow "manage" our method during its call. Of course, you can write two separate methods mySuperSortAsc()for mySuperSortDesc()sorting in ascending and descending order. Or pass some parameter inside the method (let's saybooleanand if true, sort in ascending order, and if false- in descending order). But what if we want to sort not some simple structure, but, for example, a list of arrays of strings? How will our method mySuperSort()know how to sort these arrays of strings? To size? By the total length of words? Perhaps alphabetically, depending on the first row in the array? But what if in some cases we need to sort the list of arrays by the size of the array, and in another case, by the total length of the words in the array? I think you have already heard about comparators and that in such cases we simply pass a comparator object to our sort method, in which we describe the rules by which we want to sort. Since the standard method sort()is implemented on the same principle asmySuperSort(), in the examples I will use the standard 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 love java
Here the arrays are sorted by the number of words in each array. An array where there are fewer words is considered “smaller”. That's why it's at the beginning. The one with more words counts as "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 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 moment the method is called 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. This is all we already know. We can pass objects to methods, we can pass this or that object to a method, depending on what we currently need, 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? Moreover, a lambda is such an object that contains exactly one method. Such an object-method. A method wrapped in an object. They just have a slightly unusual syntax (but more on that later). Let's take a look at this post again.
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 a single method compare()(we don’t care what it’s called, because it’s the only one in this object, we won’t miss here). This method takes two parameters with which we work further. If you are working in IntelliJ IDEA , then you have probably seen how it suggests that you significantly reduce this code:
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 such code will work exactly the same as with an anonymous class. Task 2 . Figure out how to rewrite the solution to Problem 1 with 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 the implementation of those methods that are listed in the interface (or, in extreme cases, do not write, 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 (the so-called marker interfaces, such as Serializable). Those interfaces that have only one method are also called functional interfaces . In Java 8, they are even marked with a special annotation @FunctionalInterface. It is interfaces with a 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 we do not care 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, in essence. implementation of the functional interface. Where we see an interface with one method, it means that we can rewrite such an anonymous class through a lambda. If there is more / less than one method in the interface, then the lambda expression will not work for us, and we will use an anonymous class, or even a regular one. It's time to tinker with lambdas. :)

Syntax

The general syntax is something like this:
(параметры) -> {тело метода}
That is, parentheses, inside their method parameters, an “arrow” (these are two characters in a row: minus and more), after which the method body 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 is Listtyped with arrays of strings), then the type of variables String[]can not be written.
If you are not sure, specify the type, and IDEA will gray it out if it is not needed.
You can read more in the oracle tutorial , for example. This is called target typing . Variable names can be given whatever you like, not necessarily exactly 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 figured out the parameters, now about the body of the lambda expression itself. Inside the curly braces, write the code as you would for a normal method. If your entire code consists of only one line, you can not 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 necessary to write at all. But if you have curly braces, then, as in the usual method, you need to explicitly write return.

Examples

Example 1
() -> {}
The easiest option. And the most meaningless :). Since it does nothing. Example 2
() -> ""
Also an interesting option. Accepts nothing and returns an empty string ( returnomitted as unnecessary). Same but with return:
() -> {
    return "";
}
Example 3: Hello world with lambdas
() -> System.out.println("Hello world!")
Takes nothing, returns nothing (we can't prefix returnthe call System.out.println()because the return type in the method println() — void)just displays the label. Perfect for implementing an interface Runnable. This 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 to a variable. The interface Runnabletells us that its objects must have a public void run(). According to the interface, the run method takes nothing as parameters. And returns nothing (void). Therefore, with such a record, 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 Runnable. That's why we were able to put this lambda expression in a variable of type Runnable. Example 4
() -> 42
Again, it takes nothing, but returns the number 42. Such a lambda expression can be placed in a variable of type Callable, because only one method is defined in this interface, which looks something like this:
V call(),
where Vis the return type (in our case int). Accordingly, we can save such a lambda expression as follows:
Callable<Integer> c = () -> 42;
Example 5. Lambda on multiple lines
() -> {
    String[] helloWorld = {"Hello", "world!"};
    System.out.println(helloWorld[0]);
    System.out.println(helloWorld[1]);
}
Again, this is a lambda expression with no parameters, and it has a return type void(since there is no return). Example 6
x -> x
Here we take something into a variable х, and return it as well. Please note that if only one parameter is accepted, then the brackets around it can be omitted. The same, but with parentheses:
(x) -> x
And here's the explicit version return:
x -> {
    return x;
}
Or like this, with brackets and return:
(x) -> {
    return x;
}
Or with an explicit type specification (and, accordingly, with brackets):
(int x) -> x
Example 7
x -> ++x
We accept х, we return it, but for 1more. It can also be rewritten like this:
x -> x + 1
In both cases, the brackets around the parameter, the method body and the word returnare not specified, since this is not necessary. Variants with brackets and with return are described in example 6. Example 8
(x, y) -> x % y
We accept some хand у, return the remainder of the division xby y. The parentheses around the parameters are already required here. They are optional only when there is only one parameter. Like this with explicit 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. Since catwe have a reference type variable, the Cat object outside the lambda expression will also change (it will receive the name and age passed inside). A slightly more complicated version, where a similar lambda is used:
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, and after using the lambda expression, the state changed. Lambda expressions go great with generics. And if we need to create a class Dog, for example, which 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 string that is longer. 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 as a lambda expression that returns the 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