Detailed explanation of Java functional programming (Lambda expressions, method references)

1. Comparison between new and old ways

Often, methods produce different results depending on the data passed. What if you want a method to behave differently each time it is called? If you pass code to a method, you can control its behavior.

The previous approach was to create an object, have one of its methods contain the desired behavior, and then pass this object to the method we want to control. The following example demonstrates this and then adds the Java 8 implementation: method references and lambda expressions:

package funcprog;

interface Strategy {
    
    
    String approach(String msg);
}

class DefaultStrategy implements Strategy {
    
    
    @Override
    public String approach(String msg) {
    
    
        return msg.toLowerCase() + "?";
    }
}

class Unrelated {
    
    
    static String twice(String msg) {
    
    
        return msg + " " + msg;
    }
}

public class Strategize {
    
    
    Strategy strategy;
    String msg;

    Strategize(String msg) {
    
    
        strategy = new DefaultStrategy();
        this.msg = msg;
    }

    void f() {
    
    
        System.out.println(strategy.approach(msg));
    }

    void changeStrategy(Strategy strategy) {
    
    
        this.strategy = strategy;
    }

    public static void main(String[] args) {
    
    
        Strategize s = new Strategize("Hello world");
        s.f();  // hello world?

        Strategy[] strategies = {
    
    
            new Strategy() {
    
    
                @Override
                public String approach(String msg) {
    
    
                    return msg.toUpperCase() + "!";
                }
            },  // 匿名内部类
            msg -> msg.substring(0, 5),  // Lambda表达式
            Unrelated::twice  // 方法引用
        };

        for (Strategy newStrategy: strategies) {
    
    
            s.changeStrategy(newStrategy);
            s.f();
        }

        /*
         * HELLO WORLD!
         * Hello
         * Hello world Hello world
         */
    }
}

Strategy provides an interface, and the function is carried through the only approach() method. By creating different Strategy objects, we can create different behaviors .

Traditionally, we accomplish this behavior by defining a class that implements the Strategy interface, such as DefaultStrategy. A more concise and natural way is to create an anonymous inner class, but there will still be a certain amount of duplicate code, and it always takes us some effort to understand that an anonymous inner class is being used.

The outstanding feature of Java 8's Lambda expression is the use of arrows-> to separate parameters and function bodies. The right side of the arrow is the expression returned from Lambda, which is similar to the class definition. and anonymous inner classes achieve the same effect, but with much less code.

The method reference in Java 8 is ::, with the class name or object name on the left and the method name on the right, but there is no parameter list.

2. Lambda expression

Lambda expressions are function definitions written with as little syntax as possible. Lambda expressions produce functions, not classes. On the Java Virtual Machine (JVM), everything is a class, so there are various operations behind the scenes to make Lambda look like a function.

The basic syntax of any lambda expression is as follows:

  1. parameter;
  2. is followed by ->, which can be read as "produce";
  3. ->Followed by a method body.

The following aspects need to be noted:

  • If there is only one parameter, you can write just that parameter without the parentheses, or you can use parentheses, although this is less common.
  • If there are multiple parameters, place them in a parameter list enclosed in parentheses.
  • If there are no parameters, parentheses must be used to indicate an empty parameter list.
  • If the method body has only one line, the result of the expression in the method body will automatically become the return value of the Lambda expression, and return cannot be used.
  • If the Lambda expression requires multiple lines of code, these lines must be placed in {}, in which case you need to use return from Lambda expressions generate a value.
package funcprog;

interface Description {
    
    
    String f();
}

interface Body {
    
    
    String f(String str);
}

interface Multi {
    
    
    String f(String str, int x);
}

public class LambdaExpressions {
    
    
    static Description d = () -> "Hello World!";
    static Body b1 = s -> "Hello " + s;
    static Body b2 = (s) -> "Hello " + s;
    static Multi m = (s, x) -> {
    
    
        System.out.println("Multi");
        return "Hello " + s + " " + x;
    };

    public static void main(String[] args) {
    
    
        System.out.println(d.f());
        System.out.println(b1.f("AsanoSaki"));
        System.out.println(b2.f("AsanoSaki"));
        System.out.println(m.f("AsanoSaki", 666));

        /*
         * Hello World!
         * Hello AsanoSaki
         * Hello AsanoSaki
         * Multi
         * Hello AsanoSaki 666
         */
    }
}

Recursion means that a function calls itself. You can also write recursive Lambda expressions in Java, but be aware that this Lambda expression must be assigned to a static variable or an instance variable, otherwise a compilation error will occur:

package funcprog;

interface Factorial {
    
    
    int f(int n);
}

public class RecursiveFactorial {
    
    
    static Factorial fact;  // 需要将Lambda表达式赋值给一个静态变量

    public static void main(String[] args) {
    
    
        fact = n -> n == 0 ? 1 : n * fact.f(n - 1);

        for (int i = 0; i < 5; i++)
            System.out.print(fact.f(i) + " ");  // 1 1 2 6 24
    }
}

Please note that you cannot initialize it like this when defining fact:

static Factorial fact = n -> n == 0 ? 1 : n * fact.f(n - 1);

Although this is a reasonable expectation, it is too complex for the Java compiler to handle, so compilation errors occur.

Now we use recursive lambda expressions to implement the Fibonacci sequence, this time using instance variables and initializing them with a constructor:

package funcprog;

interface Fibonacci {
    
    
    int f(int n);
}

public class RecursiveFibonacci {
    
    
    Fibonacci fib;

    RecursiveFibonacci() {
    
      // 构造器内初始化Fibonacci
        fib = n -> n == 0 ? 0 : n == 1 ? 1 : fib.f(n - 2) + fib.f(n - 1);
    }

    public static void main(String[] args) {
    
    
        RecursiveFibonacci rf = new RecursiveFibonacci();
        for (int i = 0; i < 10; i++)
            System.out.print(rf.fib.f(i) + " ");  // 0 1 1 2 3 5 8 13 21 34
    }
}

3. Method reference

Java 8 method references point to methods without the historical baggage of previous Java versions. Method references use class names or object names, followed by ::, and then the method name: < /span>

package funcprog;

interface Callable {
    
    
    void call(String s);
}

class Print {
    
      // 非静态类
    void print(String s) {
    
      // 签名和call()一致
        System.out.println(s);
    }
}

public class MethodReferences {
    
    
    static void hello(String s) {
    
      // 静态方法引用
        System.out.println("Hello " + s);
    }

    static class GoodMorning {
    
    
        void goodMorning(String s) {
    
      // 静态内部类的非静态方法
            System.out.println("Good morning " + s);
        }
    }

    static class GoodEvening {
    
    
        static void goodEvening(String s) {
    
      // 静态内部类的静态方法
            System.out.println("Good evening " + s);
        }
    }

    public static void main(String[] args) {
    
    
        Print p = new Print();
        Callable c = p::print;
        c.call("AsanoSaki");

        c = MethodReferences::hello;
        c.call("AsanoSaki");

        c = new GoodMorning()::goodMorning;
        c.call("AsanoSaki");

        c = GoodEvening::goodEvening;
        c.call("AsanoSaki");

        /*
         * AsanoSaki
         * Hello AsanoSaki
         * Good morning AsanoSaki
         * Good evening AsanoSaki
         */
    }
}

3.1 Runnable

java.langRunnableThe interface in the :run() method has no parameters and no return value, so we can Lambda expression or method reference used as package also follows the special single-method interface format. Its Runnable

package funcprog;

class RunnableReference {
    
    
    static void f() {
    
    
        System.out.println("RunnableReference::f()");
    }
}

public class RunnableMethodReference {
    
    
    public static void main(String[] args) {
    
    
        // Thread对象接受一个Runnable作为其构造器参数
        new Thread(new Runnable() {
    
      // 匿名内部类
            @Override
            public void run() {
    
    
                System.out.println("Anonymous");
            }
        }).start();  // start()方法会调用run()

        new Thread(  // Lambda表达式
            () -> System.out.println("Lambda")
        ).start();

        new Thread(RunnableReference::f).start();  // 方法引用

        /*
         * Anonymous
         * Lambda
         * RunnableReference::f()
         */
    }
}

3.2 Unbound method reference

An unbound method reference refers to an ordinary (non-static) method that has not yet been associated with an object. For an unbound reference, the object must be provided before it can be used:

package funcprog;

class A {
    
    
    String f() {
    
     return "A::f()"; }
}

interface GetStringUnbound {
    
    
    String get();
}

interface GetStringBoundA {
    
    
    String get(A a);
}

public class UnboundMethodReference {
    
    
    public static void main(String[] args) {
    
    
        GetStringBoundA g = A::f;
        A a = new A();

        System.out.println(a.f());  // A::f()
        System.out.println(g.get(a));  // A::f()
    }
}

If we assign A::f to GetStringUnbound as follows, the compiler will report an error, even though the signature of get() is the same as < /span> represents an unbound method reference because it is not bound to an object. object to attach to. Therefore, cannot be called without a . f() Same. The problem is that there's actually another (hidden) parameter involved: our old friend thisAf()A::f

To solve this problem, we need a A object, so our interface actually needs an additional parameter, as shown in GetStringBoundA Indicates that if you assign A::f to a GetStringBoundA, Java will happily accept it. In the case of unbound references, the signature of a functional method (a single method in an interface) no longer exactly matches the signature of the method reference. There is a good reason for this, which is that we need an object for the method to called on it.

Ing.get(a) we accept the unbound reference and then call A >, and ended up calling somehow. Java knows that it must accept the first parameter, which is actually , and calls the method on it. get()a.f()this

If the method has more parameters, just follow the pattern of taking the first parameter asthis, that is, for this example the first parameter isA is enough.

3.3 Constructor method reference

We can also capture a reference to a constructor and later call that constructor with that reference:

package funcprog;

class Cat {
    
    
    String name;

    Cat() {
    
     name = "Kitty"; }  // 无参构造器

    Cat(String name) {
    
     this.name = name; }  // 有参构造器
}

interface MakeCatNoArgs {
    
    
    Cat makeCat();
}

interface MakeCatWithArgs {
    
    
    Cat makeCat(String name);
}

public class ConstructorReference {
    
    
    public static void main(String[] args) {
    
    
        MakeCatNoArgs m1 = Cat::new;  // 所有构造器名字都是new,编译器可以从接口来推断使用哪个构造器
        MakeCatWithArgs m2 = Cat::new;

        Cat c1 = m1.makeCat();  // 调用此处的函数式接口方法makeCat()意味着调用构造器Cat::new
        Cat c2 = m2.makeCat("Lucy");
    }
}

4. Functional interface

Both method references and Lambda expressions must be assigned values, and these assignments require type information to allow the compiler to ensure the correctness of the type. Especially Lambda expressions introduce new requirements. Consider the following code:

x -> x.toString()

We see that the return type must be String, but what type is x? Because lambda expressions contain some form of type inference (the compiler infers some information about the type without the programmer explicitly specifying it) , so the compiler must be able to infer the type of somehow. x

Here's a second example:

(x, y) -> x + y

Nowx and y can be any type that supports the + operator, including two different numeric types , or a String and some other type that can be automatically converted to String.

Java 8 introduced java.util.function which contains a set of interfaces, which are the target types of Lambda expressions and method references. Each interface contains only one abstract method, called /span> annotation:. When writing interfaces, this functional method pattern can be enforced using the Functional approach@FunctionalInterface

package funcprog;

@FunctionalInterface
interface FunctionalPrint {
    
    
    void print(String s);
}

@FunctionalInterface
interface NotFunctional {
    
    
    void f1();
    void f2();
}  // 编译器会报错

public class FunctionalAnnotation {
    
    
    public void hello(String s) {
    
    
        System.out.println("Hello " + s);
    }

    public static void main(String[] args) {
    
    
        FunctionalAnnotation f = new FunctionalAnnotation();
        FunctionalPrint fp = f::hello;
        fp.print("AsanoSaki");  // Hello AsanoSaki

        fp = s -> System.out.println("Hi " + s);
        fp.print("AsanoSaki");  // Hi AsanoSaki
    }
}

@FunctionalInterfaceThe annotation is optional, Java will treat in main() as a functional interface. In the definition of the interface we can see the role of : if there is more than one method in the interface, a compilation error message will be generated. FunctionalPrintNotFunctional@FunctionalInterface

Now let's take a closer look at what happens in the definition of fp. FunctionalPrint defines interfaces, but only methods are assigned to themhello(), not a class, it's not even a method in a class that implements one of the interfaces defined here. This is a little magic added by Java 8: if we assign a method reference or Lambda expression to a functional interface (and the type can match), then Java will adjust the assignment to match the target interface. Under the hood, the Java compiler will create an instance of a class that implements the target interface and wrap our method reference or Lambda expression in it .

The interface using@FunctionalInterface annotation is also calledSingle Abstract Method (Single Abstract Method, SAM) type.

4.1 Default target interface

java.util.functionIt aims to create a set of target interfaces that are complete enough so that generally we do not need to define our own interfaces. The naming of this set of interfaces follows certain rules. Generally speaking, you can understand what a specific interface does through its name. Some interface examples are as follows:

package funcprog;

import java.util.function.BiConsumer;
import java.util.function.DoubleFunction;
import java.util.function.Function;
import java.util.function.IntToDoubleFunction;

public class FunctionVariants {
    
    
    static Function<Integer, String> f = x -> "Function " + x;
    static DoubleFunction<String> df = x -> "DoubleFunction " + x;
    static IntToDoubleFunction itdf = x -> x / 2.0;
    static BiConsumer<Integer, String> bc = (x, s) -> System.out.println("BiConsumer " + x + " " + s);

    public static void main(String[] args) {
    
    
        System.out.println(f.apply(6));  // Function 6
        System.out.println(df.apply(6));  // DoubleFunction 6.0
        System.out.println(itdf.applyAsDouble(6));  // 3.0
        bc.accept(6, "AsanoSaki");  // BiConsumer 6 AsanoSaki
    }
}

4.2 Functional interfaces with more parameters

java.util.functionAfter all, the interface in is limited. What if we need a function interface with 3 parameters? Because those interfaces are quite intuitive, it's easy to take a look at the Java library's source code and write our own:

package funcprog;

@FunctionalInterface
interface TriFunction<T, U, V, R> {
    
      // R为泛型返回类型
    R apply(T t, U u, V v);
}

public class TriFunctionTest {
    
    
    public static void main(String[] args) {
    
    
        TriFunction<Integer, Long, Double, Double> tf = (i, j, k) -> i + j + k;
        System.out.println(tf.apply(1, 2L, 3D));  // 6.0
    }
}

5. Higher-order functions

A higher-order function is a function that can accept a function as a parameter or a function as a return value. With Lambda expressions, it is easy to create and return a function in a method. To accept and use a function, the method must be in its parameters. Function types are correctly described in lists:

package funcprog;

import java.util.function.Function;

public class ProduceFunction {
    
    
    static Function<String, String> produce() {
    
      // 函数作为返回值
        return String::toLowerCase;  // 或者用Lambda表达式s -> s.toLowerCase()
    }

    static String consume(Function<Integer, String> toStr, int x) {
    
      // 函数作为参数
        return toStr.apply(x);
    }

    public static void main(String[] args) {
    
    
        Function<String, String> toLower = produce();
        System.out.println(toLower.apply("Hello World"));  // hello world

        System.out.println(consume(x -> "To String " + x, 6));  // To String 6
    }
}

6. Function combination

Function composition refers to combining multiple functions to create new functions, which is generally considered part of functional programming. Some interfaces in java.util.function also include methods that support function composition. Let’s take the andThen() and compose() methods as examples:

  • andThen(): Perform the original operation first, and then perform the operations in the method parameters.
  • compose(): Execute the operation in the method parameters first, and then perform the original operation.
package funcprog;

import java.util.function.Function;

public class FunctionComposition {
    
      // hello world
    static Function<String, String> f1 = s -> s.substring(0, 5),
                                    f2 = s -> new StringBuilder(s).reverse().toString();

    public static void main(String[] args) {
    
    
        System.out.println(f1.andThen(f2).apply("Hello World"));  // olleH
        System.out.println(f1.compose(f2).apply("Hello World"));  // dlroW
    }
}

Guess you like

Origin blog.csdn.net/m0_51755720/article/details/134057388