Understand Java 8 lambda expressions

The lambda expression is like this.

Runnable runner = () -> { System.out.println("Hello Lambda!"); };

You may be scared if you are not familiar with it, but it is actually configured using existing Java features. Here, let's break down the lambda expression and understand it from the beginning.

Click here for abbreviation of lambda expression. [Java] Omission summary of lambda expression

1. Try disassembling the lambda expression

Lambda expressions use a mechanism called local classes and anonymous classes.

1-1. Local class

A local class is a mechanism that can be used by declaring a class during method processing.

public static void main(String[] args) {

  class Local {
    public void sayHello() {
      System.out.println("Hello!");
    }
  }

  Local local = new Local();
  local.sayHello(); // Hello!
}

You can also define a local class that implements the interface.

public static void main(String[] args) {

  class Local implements Runnable {
    @Override
    public void run() {
      System.out.println("Hello Lambda!");
    }
  }

  Runnable runner = new Local();
  runner.run(); // Hello Lambda!
}

Next, let's take a look at the anonymous class.

1-2. Anonymous class

Anonymous class is a mechanism that omits the declaration of the local class that implements the interface. Here is an example of an anonymous class that implements the Runnable interface.

public static void main(String[] args) {

  Runnable runner = new Runnable() {
    @Override
    public void run() {
      System.out.println("Hello Lambda!");
    }
  };

  runner.run(); //Hello Lambda!
}

It looks as if you are instantiating the Rannable interface, but you are actually creating an instance of an anonymous class that implements the Rannable interface. Finally, let's take a look at the lambda expression.

1-3. Lambda type

It becomes a lambda expression by omitting "new Runnable () {}" and "public void run" from the anonymous class.

public static void main(String[] args) {

  Runnable runner = () -> { System.out.println("Hello Lambda!"); };
  runner.run(); //Hello Lambda!
}

The first () represents the argument of the run method, and the contents of-> {} are the implementation contents of the run method. An instance of an anonymous class that implements Runnable is assigned to the runner variable. In other words, a lambda expression is an expression that creates an instance that implements an interface </ b>.

By the way, if you omit "new Runnable () {}", you don't know what type of instance to create. In Java, it is a mechanism to infer automatically according to the type of the variable to be assigned. This mechanism is called type inference </ b>.

You can specify an interface as an argument to define a method that can accept a lambda expression.

public static void main(String[] args) {
    method(()->{System.out.println("Hello Lambda!");});
}
public static void method(Runnable r) {
    r.run();
}
// Hello Lambda!

This is also type inferred from the argument type, and an instance of Runnable type is automatically generated.

Also, if you omit "public void run", you won't know which method to override for an interface that has multiple methods defined. Therefore, lambda expressions can only use interfaces with one abstract method </ b>.

Only the Rannable interface can create lambda expressions with no arguments and no return value. If you want to create it in another form, a functional interface has been added, so use that.

2. Functional interface

A lot of new interfaces have been added from SE8 under the java.util.function package. These are called functional interfaces. https://docs.oracle.com/javase/jp/8/docs/api/java/util/function/package-summary.html

Functional interfaces are a group of interfaces that have a single method and are very convenient to use in lambda expressions. Among these, we will introduce the frequently used Functions, Consumers, and Predicates.

2-1. Function<T, R> In Function \ <T, R>, T specifies the method argument type, and R specifies the return type. The method is R apply (T).

Function<Integer, String> asterisker = (i) -> { return "*"+ i; };
String result = asterisker.apply(10);
System.out.println(result); // *10

There is also an interface called BiFunction that accepts two arguments.

BiFunction<Integer, Integer, Integer> adder = (a, b) -> { return a + b; };
int result = adder.apply(1, 2);
System.out.println(result); // 3

2-2. Consumer<T> T in Consumer \ specifies the type of method argument. The method is void accept (T).

Consumer<String> buyer = (goods) -> { System.out.println(goods + "I bought"); };
buyer.accept("rice ball"); // rice ballを購入しました。

I won't introduce it, but there is also a BiConsumer interface with two arguments and no return value.

2-3. Predicate<T> T in Predicate \ specifies the type of method argument. The method is boolean test (T).

Predicate<String> checker = (s)-> { return s.equals("Java"); };
boolean result = checker.test("Java");
System.out.println(result); //true

3. How to use lambda expression

Why was the lambda expression born in the first place? That's because I wanted to change the process dynamically by passing the process as an argument instead of the value. Typical examples are Collections.sort and Stream API methods.

3-1. Collections.sort(List<T>, Comparator<? super T>) The sorting method depends on the type and situation of the object. For example, when sorting numbers, they may be in simple ascending order or in absolute ascending order. In order to realize both sorts, it is necessary to dynamically switch the numerical comparison process. Therefore, Collections.sort is designed to receive the comparison process itself.

int[] numbers = {-1, 2, 0, -3, 8};

List<Integer> numbersList = new ArrayList<>();

for(int n : numbers) {
  numbersList.add(n);
}

Collections.sort(numbersList,[Sort method]);

For [Sort method], specify the instance that implements the compare (s1, s2) method of the Comparator interface. The compare method returns an int type and determines the magnitude as follows.

Return value Large and small
Greater than 0 s1 > s2
0 s1 = s2
Less than 0 s1 < s2

Let's actually sort using a lambda expression.

Collections.sort(numbersList, (a, b) -> { return a - b; });

for(Integer n : numbersList) {
  System.out.print(n + " ");
}
// -3 -1 0 2 8

You can change the sorting method by changing the contents of the lambda expression. Let's do it in descending order.

Collections.sort(numbersList, (a, b) -> { return b - a; });

for(Integer n : numbersList) {
  System.out.print(n + " ");
}
// 8 2 0 -1 -3 

Let's try in the order of absolute values.

Collections.sort(numbersList, (a, b) -> { return a*a - b*b; });

for(Integer n : numbersList) {
  System.out.print(n + " ");
}
// 0 -1 2 -3 8 

Of course, if you pass an instance of a regular class that implements Comparator, it will work fine. However, by using a lambda expression, it seems that the process itself is being passed, so you can write more concisely and intuitively.

3-2.StreamAPI A method called stream has been added to the Collection interface. The stream method returns its own Stream instance. Stream defines a lot of useful methods that take a functional interface as an argument.

void forEach(Consumer<T>) The forEach method takes a Consumer as an argument and repeats the process for the number of elements.

int[] numbers = {-1, 2, 0, -3, 8};

List<Integer> numbersList = new ArrayList<>();

for(int n : numbers) {
  numbersList.add(n);
}

numbersList.stream().forEach((i) -> { System.out.print(i + " "); });
// -1 2 0 -3 8 

Stream filter(Predicate<T>) The filter method takes Predicate as an argument and returns a Stream excluding those that do not meet the conditions. Since it is also a Stream that is returned, you can call the ForEach method as it is.

numbersList.stream().filter((i) -> { return i > 0; })
                    .forEach((i) -> { System.out.print(i + " "); });
                    // 2 8 

Since the return value of the filter method is Stream type, you can continue to call forEach. That's why methods like filter are called intermediate operations. On the other hand, a method like forEach is called a termination operation.

Stream map(Function<T, R>) The map method takes a Function as an argument and returns the processed result as a Stream. It is an intermediate operation like filter.

numbersList.stream().filter((i) -> { return i >= 0; })
                    .map((i) -> { return "*" + i + "*"; })
                    .forEach((s) -> { System.out.print(s + " "); });
                    // *2* *0* *8* 

Stream sorted(Comparator<T>) Although it is a java.util package, it also provides a sorted method that takes a Comparator as an argument. It is an intermediate operation like filter.

numbersList.stream().filter((i) -> { return i >= 0; })
                    .sorted((i1, i2) -> { return i1 - i2; })
                    .map((i) -> { return "*" + i + "*"; })
                    .forEach((s) -> { System.out.print(s + " "); });
                    // *0* *2* *8* 

that's all. If it's different from the lambda expression you saw, try searching for abbreviated patterns.

Recommended Posts