[Java 8] Lambda expressions optimize recursion (I) and lambda expressions
Recursive Optimization
Many algorithms rely on recursion, such as Divide-and-Conquer ). However, StackOverflowError often occurs when a common recursive algorithm is used to deal with large-scale problems. To solve this problem, we can use a technology called Tail-Call Optimization to optimize recursion. At the same time, you can save the sub-problem results to avoid repeated solutions to the sub-problem. This optimization method is called mmoization ).
This article first introduces tail recursion, and the memorandum mode will be introduced in the next article.
Use end call Optimization
When recursive algorithms are applied to large-scale problems, StackOverflowError may occur. This is because there are too many subproblems to be solved and the hierarchy of recursive Nesting is too deep. In this case, tail call optimization can be used to avoid this problem. This technique is called a tail call because the last statement in a recursive method is a recursive call. This is different from conventional recursive methods. Conventional recursion usually occurs in the middle of a method. After recursion ends, the result is often processed.
Java does not support tail recursion at the compiler level. But we can use Lambda expressions to implement it. Next we will apply this technology in the factorial algorithm to achieve recursive optimization. The following code uses a factorial recursive algorithm that has not been optimized:
public class Factorial { public static int factorialRec(final int number) { if(number == 1) return number; else return number * factorialRec(number - 1); }}
The above recursive algorithms can be solved normally when processing small-scale input, but it is very likely to throw StackOverflowError after large-scale input:
try { System.out.println(factorialRec(20000));} catch(StackOverflowError ex) { System.out.println(ex);}// java.lang.StackOverflowError
The reason for this problem is not recursion, but a number variable must be saved while the recursive call ends. Because the last operation of a recursive method is a multiplication operation, when solving a subproblem (factorialRec(number - 1)
), You need to save the current number value. Therefore, as the number of problems increases, the number of subproblems increases. Each subproblem corresponds to a layer of the Call Stack. When the size of the Call Stack exceeds the threshold set by JVM, stackOverflowError occurs.
Convert to tail recursion
The key to conversion to tail recursion is to ensure that recursive calls to itself are the last operation. Unlike the recursive method above: the last operation is a multiplication operation. To avoid this, we can first perform a multiplication operation and pass the result into the recursive method as a parameter. However, this is not enough because a Stack Frame will be created in the call Stack every time a recursive call occurs ). As the recursive call depth increases, the number of stack frames increases, leading to StackOverflowError. You can avoid stack frame creation by making recursive call latencies. The following code is a prototype implementation:
public static TailCall<Integer> factorialTailRec( final int factorial, final int number) { if (number == 1) return TailCalls.done(factorial); else return TailCalls.call(() -> factorialTailRec(factorial * number, number - 1));}
The factorial parameter to be accepted is the initial value, while the number parameter is the factorial value. We can find that recursive calls are reflected in Lambda expressions accepted by the call method. The TailCall interface and the tailcalltool class in the above Code are not implemented yet.
Create TailCall function interface
The purpose of TailCall is to replace the stack frames in the traditional recursion and use Lambda expressions to represent multiple consecutive recursive calls. So we need to get the next recursive operation through the current recursive operation, which is similar to the apply method of the UnaryOperator function interface. At the same time, we also need methods to complete these tasks:
- Determine whether recursion is over
- Get the final result
- Trigger Recursion
Therefore, we can design the TailCall function interface as follows:
@FunctionalInterfacepublic interface TailCall<T> { TailCall<T> apply(); default boolean isComplete() { return false; } default T result() { throw new Error("not implemented"); } default T invoke() { return Stream.iterate(this, TailCall::apply) .filter(TailCall::isComplete) .findFirst() .get() .result(); }}
The isComplete, result, and invoke Methods complete the three tasks mentioned above. However, the specific isComplete and result must be overwritten according to the nature of recursive operations. For example, for Recursive intermediate steps, the isComplete method can return false, however, true must be returned for the last recursive step. For the result method, the intermediate recursive step can throw an exception, while the final recursive step requires the result.
The invoke method is the most important method. It concatenates all recursive operations through the apply method and gets the final result through the end call without stack frames. The concatenation method utilizes the iterate method provided by the Stream type. It is essentially an infinite list, which meets the characteristics of recursive calls to some extent, this is because the number of recursive calls is limited, but the number can be unknown. The terminate operation for this infinite list is the filter and findFirst methods. In all recursive calls, only the last recursive call will return true in isComplete. When called, it means the end of the entire recursive call chain. Finally, this value is returned through findFirst.
If you are not familiar with Stream's iterate method, refer to the previous article to introduce the use of this method.
Create tailcils tool class
In the prototype design, call and done methods of the tailcils tool class are called:
- The call method is used to obtain the next recursion of the current recursion.
- The done method is used to end a series of recursive operations and obtain the final result.
public class TailCalls { public static <T> TailCall<T> call(final TailCall<T> nextCall) { return nextCall; } public static <T> TailCall<T> done(final T value) { return new TailCall<T>() { @Override public boolean isComplete() { return true; } @Override public T result() { return value; } @Override public TailCall<T> apply() { throw new Error("end of recursion"); } }; }}
In the done method, we return a special TailCall instance to represent the final result. Note that its apply method is implemented as called and throws an exception, because there is no subsequent recursive operation for the final recursive result.
Although the above TailCall and tailcils are designed to solve the simple recursive algorithm of factorial, they are undoubtedly useful in any algorithm that requires tail recursion.
Use tail recursive functions
The code to use them to solve the factorial problem is simple:
System.out.println(factorialTailRec(1, 5).invoke()); // 120System.out.println(factorialTailRec(1, 20000).invoke()); // 0
The first parameter represents the initial value, and the second parameter represents the value of the factorial to be calculated.
However, an error was returned when calculating the 20000 factorial, because the integer data could not accommodate such a large result and an overflow occurred. In this case, you can use BigInteger to replace the Integer type.
In fact, the first parameter of factorialTailRec is unnecessary. Generally, the initial value is 1. Therefore, we can simplify the process accordingly:
Public static int factorial (final int number) {return factorialTailRec (1, number ). invoke ();} // call method System. out. println (factorial (5); System. out. println (factorial (20000 ));
Use BigInteger instead of Integer
The decrement and multiple methods need to be defined to help complete the factorial operation of big integer data:
public class BigFactorial { public static BigInteger decrement(final BigInteger number) { return number.subtract(BigInteger.ONE); } public static BigInteger multiply( final BigInteger first, final BigInteger second) { return first.multiply(second); } final static BigInteger ONE = BigInteger.ONE; final static BigInteger FIVE = new BigInteger("5"); final static BigInteger TWENTYK = new BigInteger("20000"); //... private static TailCall<BigInteger> factorialTailRec( final BigInteger factorial, final BigInteger number) { if(number.equals(BigInteger.ONE)) return done(factorial); else return call(() -> factorialTailRec(multiply(factorial, number), decrement(number))); } public static BigInteger factorial(final BigInteger number) { return factorialTailRec(BigInteger.ONE, number).invoke(); }}
Write a simple addition operation using the lambda expression in java 8
/*
An interface. If there is only one explicitly declared abstract method,
Then it is a function interface.
Usually marked with @ FunctionalInterface (or not marked)
*/
Public interface Inteface1 {
// Abstract modification is not required.
Public abstract void test (int x, int y );
// Public void test1 (); // an error is returned. There cannot be two methods, although abstract modification is not used.
Public boolean equals (Object o); // equals belongs to the Object method, so no error is reported.
}
Public class Test {
Public static void main (String args []) {
Inteface1 f1 = (int x, int y)-> {System. out. println (x + y );};
F1.test (3, 4 );
Inteface1 f2 = (int x, int y)-> {System. out. println ("Hello Lambda! \ T the result is "+ (x + y ));};
F2.test (3, 4 );
}
}
Java 8 lambda expressions
The whole process is probably okay.
Return userResDao. findEqualByProperty ("user. token ", token ). parallelStream (). map (UserRes: getRes ). collect (Collectors. toList (); but there is no debugging environment, details are not guaranteed