Children, learn lambda expressions

Posted by jariizumi on Fri, 31 Dec 2021 04:10:29 +0100

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();
Copy code

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());
Copy code

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.");
      }
  });
}
Copy code

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();
Copy code

Comparison array (Lambda version)

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

Add a click event to the button (Lambda version)

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

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

Syntax of Lambda expressions

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. It's still 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.")
Copy code

Multiple parameters

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

0 parameters

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

Expression block

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

Functional interface

A new annotation has been added in Java 8: @FunctionalInterface , 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.
  • @FunctionalInterface Annotations are not required, but it is recommended to add them, 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. Net for Java 8 util. A large number of functional interfaces have been built in the function package, as shown below:

Functional interfaceParameter typeReturn typeMethod namedescribe
SuppliernothingTgetGenerate a data of type T
ConsumerTvoidacceptConsume a data of type T
BiConsumer<T,U>T,UvoidacceptConsumption type T and type U data
Function<T,R>TRapplyThe data of parameter type T is converted into data of type R through function processing
BiFunction<T,U,R>T,URapplyThe data with parameter types T and U are transformed into data of type R through function processing
UnaryOperatorTTapplyA unary operation on type t still returns type T
BinaryOperatorT,TTapplyBinary operation on type t still returns type T
PredicateTvoidtestPerforms function processing on type T and returns a Boolean value
BiPredicate<T,U>T,UvoidtestFunction 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);
    }
}
Copy code

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

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)
Copy code

Can be replaced by:

System.out::println
 Copy code

Another example:

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

Can be replaced by:

String::compareToIgnoreCase
 Copy code

Note: the method name cannot be followed by parameters! It can be written as system Out:: println, but cannot be written 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();
    }
}
Copy code

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());
Copy code

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);
Copy code

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

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();
        }
    };
}
Copy code

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);
        }
    };
}
Copy code

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
 Copy code

Default method in interface

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());
Copy code

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";
    }
}
Copy code

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.

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);
  }
}
Copy code

So the Paths class is meaningless~

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?


 

Topics: Java Back-end