Children, learn lambda expressions!

1 Why use Lambda expressions

Let's first look at a few pieces of code that Java 8 used to encounter:

Create thread and start

// Create thread
public class Worker implements Runnable {
@Override
public void run() {
    for (int i = 0; i < 100; i++) {
        doWork();
    }
}
}
// Start thread
Worker w = new Worker();
new Thread(w).start();

Compare arrays

// Define a comparator
public class LengthComparator implements Comparator<String> {
 @Override
 public int compare(String first, String second) {
     return Integer.compare(first.length(), second.length());
 }
}
//Compare character arrays
Arrays.sort(words, new LengthComparator());

Add a click event to the button

public void onClick(Button button) {
  button.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
          System.out.println("button clicked.");
      }
  });
}

We are used to these three pieces of code.

But their problem is also very prominent: there is too much * * "noise" *! To realize the comparison function of an array, we need to write at least 5 lines of code, but only one line of code is what we really care about!

Java's complex and redundant code implementation has been criticized by programmers. Fortunately, with the rise of JVM platform language Scala and the popularity of functional programming style, Oracle has made revolutionary changes in the eighth series of Java versions, and introduced a series of syntax features of functional programming style, such as Lambda expression and Stream.

If you use Lambda expressions, the implementation of the above three pieces of code will become extremely concise.

Create thread and start (Lambda version)

new Thread(() -> {
for (int i = 0; i < 100; i++) {
    doWork();
}
}).start();

Comparison array (Lambda version)

Arrays.sort(words, (first, second) -> Integer.compare(first.length(), second.length())

Add a click event to the button (Lambda version)

button.addActionListener((event) -> System.out.println("button clicked."));

What about? Through Lambda expressions, the code has become concise enough for you to focus on business code.

2Lambda expression syntax

Format: (parameter) - > expression

Of which:

  1. Parameters can be 0-n. If there are multiple parameters, separate them with commas (,). If there is a parameter, the bracket () can be omitted; If there are no parameters, parentheses () cannot be omitted. [this is not pure enough, which is a little worse than scala!], the type name can be added before the parameter, but it can be omitted due to the automatic type derivation function.
  2. An expression can be a single line expression or multiple statements. If there are multiple statements, they need to be wrapped in braces {}.
  3. The expression does not need to display the result returned by execution, it will be automatically deduced from the context. Here are some examples:

One parameter

event -> System.out.println("button clicked.")

Multiple parameters

(first, second) -> Integer.compare(first.length(), second.length()

0 parameters

() -> System.out.println("what are you nongshalei?")

Expression block

() -> {for (int i = 0; i < 100; i++) {    doWork();}}

3 functional interface

A new annotation has been added in Java 8: @ functional interface, functional interface.

What is a functional interface? It includes the following features:

  • There is only one abstract method in the interface, but default and static methods are allowed.
  • @The functional interface annotation is not required, but it is recommended to add it, so that the compiler can check whether there is only one abstract method in the interface.

The essence of Lambda expression is the anonymous implementation of functional interface. Only the original interface implementation is expressed in a syntax more like functional programming.

java.util.function package of Java 8 has built-in a large number of functional interfaces, as shown below:

Functional interface

Parameter type

Return type

Method name

describe

Supplier

nothing

T

get

Generate a data of type T

Consumer

T

void

accept

Consume a data of type T

BiConsumer<T,U>

T,U

void

accept

Consumption type T and type U data

Function<T,R>

T

R

apply

The data of parameter type T is converted into data of type R through function processing

BiFunction<T,U,R>

T,U

R

apply

The data with parameter types T and U are transformed into data of type R through function processing

UnaryOperator

T

T

apply

A unary operation on type t still returns type T

BinaryOperator

T,T

T

apply

Binary operation on type t still returns type T

Predicate

T

void

test

Performs function processing on type T and returns a Boolean value

BiPredicate<T,U>

T,U

void

test

Function on types T and U and return Boolean values

It can be seen that:

  • The built-in functional interfaces are mainly divided into four categories: Supplier, Consumer, Function and Predicate. Operator is a special case of Function.
  • Except that the Supplier does not provide binary parameters (which is related to the fact that java does not support multiple return values), the other three classes provide binary input parameters.

The following is a comprehensive example:

public class FunctionalCase {
    public static void main(String[] args) {
        String words = "Hello, World";
        String lowerWords = changeWords(words, String::toLowerCase);
        System.out.println(lowerWords);
        
        String upperWords = changeWords(words, String::toUpperCase);
        System.out.println(upperWords);
        
        int count = wordsToInt(words, String::length);
        System.out.println(count);
        
        isSatisfy(words, w -> w.contains("hello"));
        String otherWords = appendWords(words, ()->{
            List<String> allWords = Arrays.asList("+abc", "->efg");
            return allWords.get(new Random().nextInt(2));
        });
        System.out.println(otherWords);
        consumeWords(words, w -> System.out.println(w.split(",")[0]));
    }
    public static String changeWords(String words, UnaryOperator<String> func) {
        return func.apply(words);
    }
    public static int wordsToInt(String words, Function<String, Integer> func) {
        return func.apply(words);
    }
    public static void isSatisfy(String words, Predicate<String> func) {
        if (func.test(words)) {
            System.out.println("test pass");
        } else {
            System.out.println("test failed.");
        }
    }
    public static String appendWords(String words, Supplier<String> func) {
        return words + func.get();
    }
    public static void consumeWords(String words, Consumer<String> func) {
        func.accept(words);
    }
}

If you think these built-in functional interfaces are not enough, you can also customize your own functional interfaces to meet more needs.

4 method reference

If the Lambda expression already has an implemented method, it can be simplified with a method reference. The syntax referenced by the method is as follows:

  • Object:: instance method
  • Class:: static method
  • Class:: instance method

Thus, the Lambda expression mentioned earlier:

event -> System.out.println(event)

Can be replaced by:

System.out::println

Another example:

(x,y)->x.compareToIgnoreCase(y)

Can be replaced by:

String::compareToIgnoreCase

Note: the method name cannot be followed by parameters! It can be written as System.out::println, but not as System.out::println("hello")

If you can get the this parameter of this instance, you can directly access it with the this:: instance method. For the method specified by the parent class, you can access it with the super:: instance method.

Here is an example:

public class Greeter {
    public void greet() {
        String lowcaseStr = changeWords("Hello,World", this::lowercase);
        System.out.println(lowcaseStr);
    }
    public String lowercase(String word) {
        return word.toLowerCase();
    }
    public String changeWords(String words, UnaryOperator<String> func) {
        return func.apply(words);
    }
}
class ConcurrentGreeter extends Greeter {
    public void greet() {
        Thread thread = new Thread(super::greet);
        thread.start();
    }
    public static void main(String[] args) {
        new ConcurrentGreeter().greet();
    }
}

5 constructor reference

Constructor references are similar to method references, except that the function interface returns an instance object or array. The constructor references the following syntax:

  • Class:: new
  • Array:: new

for instance:

List<String> labels = Arrays.asList("button1", "button2");
Stream<Button> stream = labels.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());

Where labels.stream().map(Button::new) is equivalent to labels. Stream(). Map (label - > New button (label))

Take another example of constructor reference of array type:

Button[] buttons = stream.toArray(Button[]::new);

The Stream is directly converted into an array type. Here, the array type is marked with button []: new.

6 variable scope

Let's start with a code:

public void repeatMsg(String text, int count) {
    Runnable r = () -> {
        for (int i = 0; i < count; i++) {
            System.out.println(text);
            Thread.yield();
        }
    };
}

A lambda expression generally consists of the following three parts:

  • parameter
  • expression
  • Free variable

Parameters and expressions are easy to understand. What is the free variable? It is the external variables referenced in the lambda expression, such as the text and count variables in the above example.

Students who are familiar with functional programming will find that Lambda expressions are actually "closures". It's just that Java 8 doesn't have that name. For free variables, if Lambda expressions need to be referenced, modification is not allowed.

In fact, in the anonymous inner class of Java, if you want to reference an external variable, the variable needs to be declared final. Although the free variable of Lambda expression does not have to be declared final, it is also not allowed to be modified.

For example, the following code:

public void repeatMsg(String text, int count) {
    Runnable r = () -> {
        while (count > 0) {
            count--;  // Error, unable to modify the value of external variable
            System.out.println(text);
        }
    };
}

In addition, it is not allowed to declare a parameter or local variable with the same name as a local variable in a Lambda expression. For example, the following code:

Path first = Paths.get("/usr/bin");
Comparator<String> comp = (first, second) -> Integer.compare(first.length(), second.length());
// Error, variable first has been defined

7 default methods in interfaces

Let's talk about why we should add a default method to the Java 8 interface.

For example, the designer of the Collection interface has added a forEach() method to traverse the Collection more concisely. For example:

list.forEach(System.out::println());

However, if a new method is added to the interface, according to the traditional method, the custom implementation classes of the Collection interface must implement the forEach() method, which is unacceptable to the majority of existing implementations.

So the designers of Java 8 came up with this method: add a new method type in the interface, called the default method, which can provide the default method implementation. In this way, if the implementation class does not implement the method, it can use the implementation in the default method by default.

An example:

public interface Person {
    long getId();
    
    default String getName() {
        return "jack";
    }
}

The addition of default methods can replace the previous classic design methods of interfaces and abstract classes, and uniformly define abstract methods and default implementations in one interface. This is probably a skill stolen from Scala's trail.

8 static methods in interfaces

In addition to the default methods, Java 8 also supports the definition of static methods and implementations in interfaces.

For example, before Java 8, for the Path interface, a Path tool class is generally defined to implement the auxiliary methods of the interface through static methods.

It's easy to have static methods in the interface. It's done in one interface! Although this seems to destroy the original design idea of the interface.

public interface Path{
  public static Path get(String first, String... more) {
    return FileSystem.getDefault().getPath(first, more);
  }
}

So the Paths class is meaningless~

9 summary

The use of Lambda expressions can greatly reduce the redundant template code and make you pay more attention to business logic rather than copying a pile of duplicate code, unless you are in a company that measures the workload by the number of code lines. What do you think?

Posted on Fri, 03 Dec 2021 04:40:52 -0500 by danman252