Thinking logic of computer programs (91) and thinking 91
In the previous sections, we have discussed Java 7. From this section, we will discuss some features of Java 8, including:
- Passing behavior code-Lambda expressions
- Function-based data processing-stream
- Combined asynchronous programming-CompletableFuture
- New Date and Time API
In this section, we will first discuss Lambda expressions. What is it? What is the purpose?
Lambda expressions are a new syntax introduced by Java 8 and a compact way of passing code. Their names are derived from the Lambda Algorithm in academic circles. We will not discuss the specifics.
To understand Lambda expressions, let's first review interfaces, anonymous internal classes, and code passing.
Pass code through the interface
We have introduced interfaces and interface-oriented programming in section 19. Programming for interfaces rather than specific types can reduce program coupling, improve flexibility, and improve reusability. The interface is often used to pass code. For example, in section 59, we have introduced the following method of File:
public String[] list(FilenameFilter filter)public File[] listFiles(FilenameFilter filter)
List and listFiles require not the FilenameFilter object, but include the following methods:
boolean accept(File dir, String name);
In other words, list and listFiles want to accept a piece of method code as a parameter, but there is no way to directly pass the method code itself, and only one interface can be passed.
For example, we have introduced some Collections algorithms in section 53. Many Methods accept a parameter Comparator, for example:
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c)public static <T> T max(Collection<? extends T> coll, Comparator<? super T> comp)public static <T> void sort(List<T> list, Comparator<? super T> c)
They do not need a Comparator object, but contain the following methods:
int compare(T o1, T o2);
However, there is no way to directly pass the method. Only one interface can be passed.
We have introduced the ExecutorService for asynchronous task execution in section 77. The methods for submitting tasks include:
<T> Future<T> submit(Callable<T> task);<T> Future<T> submit(Runnable task, T result);Future<?> submit(Runnable task);
Callable and Runnable interfaces are also used to pass the task code.
To pass behavior code through an interface, you must pass an instance object that implements this interface. in the previous chapter, the simplest way is to use an anonymous internal class, for example:
// List all files suffixed with. txt in the current directory. File f = new File (". "); File [] files = f. listFiles (new FilenameFilter () {@ Override public boolean accept (File dir, String name) {if (name. endsWith (". txt ") {return true;} return false ;}});
Sort files by file name. The code is:
Arrays.sort(files, new Comparator<File>() { @Override public int compare(File f1, File f2) { return f1.getName().compareTo(f2.getName()); }});
Submit a simple task with the code:
ExecutorService executor = Executors.newFixedThreadPool(100);executor.submit(new Runnable() { @Override public void run() { System.out.println("hello world"); }});
Lambda expressions
Syntax
Java 8 provides a new compact code transfer syntax-Lambda expressions. For the example of the file listed above, the code can be changed:
File f = new File(".");File[] files = f.listFiles((File dir, String name) -> { if (name.endsWith(".txt")) { return true; } return false;});
It can be seen that, compared with the anonymous internal class, code passing becomes more intuitive, there is no template Code Implementing the interface, no declaration method, no name, but the implementation code of the method is provided directly. Lambda expressions are separated by "->". The preceding part is the method parameter, and the code of the method is in.
The code above can be simplified:
File[] files = f.listFiles((File dir, String name) -> { return name.endsWith(".txt");});
When the subject code has only one statement, the brackets and return statements can also be omitted. The above code can be changed:
File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));
Note: When there are no parentheses, the subject code is an expression, and the value of this expression is the return value of the function. No extra points are allowed at the end of the expression, or a return statement cannot be added.
The parameter type declaration of the method can also be omitted. The above code can be further simplified:
File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));
The parameter type of the method can be omitted because Java can automatically infer that the parameter type accepted by listFiles is FilenameFilter. This interface has only one method accept, the two parameter types of this method are File and String.
Is the code much simpler and clearer?
The sorted code can be written as follows using a Lambda expression:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
The code for submitting a task can be written as follows using a Lambda expression:
executor.submit(()->System.out.println("hello"));
The parameter is null and written ().
When there is only one parameter, the brackets in the parameter section can be omitted. For example, File also has the following method:
public File[] listFiles(FileFilter filter)
FileFilter is defined:
public interface FileFilter { boolean accept(File pathname);}
Use FileFilter to overwrite the example of the listed files. The code can be:
File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));
Variable reference
Similar to anonymous internal classes, Lambda expressions can also access variables defined outside the Subject Code. However, for local variables, Lambda expressions can only access final variables, the difference with an anonymous internal class is that it does not require the variable to be declared as final, but the variable cannot be re-assigned. For example:
String msg = "hello world";executor.submit(()->System.out.println(msg));
You can access the local variable msg, but msg cannot be re-assigned. If it is written as follows:
String msg = "hello world";msg = "good morning";executor.submit(()->System.out.println(msg));
The Java compiler prompts an error.
This is the same as the anonymous internal class. Java will pass the msg value as a parameter to the Lambda expression and create a copy for the Lambda expression. Its code accesses this copy, instead of the msg variable declared externally. If msg is allowed to be modified, the programmer may mistakenly assume that the Lambda expression will read the modified value, causing more confusion.
Why do I have to create a copy and directly access the external msg variable? No, because msg is defined in the stack. When Lambda expressions are executed, msg may have been released. If you want to modify the value, you can define the variable as an instance variable or define the variable as an array, for example:
String[] msg = new String[]{"hello world"};msg[0] = "good morning";executor.submit(()->System.out.println(msg[0]));
Comparison with anonymous internal classes
From the above content, we can see that Lambda expressions are similar to anonymous internal classes, mainly to simplify the syntax. Is it syntactic sugar? Is internal implementation actually an internal class? The answer is no. Java will generate a class for each anonymous internal class, but Lambda expressions will not. Lambda expressions are usually short. A Class generated for each expression will generate a large number of classes, performance will be affected.
Java uses the invokedynamic commands and method handle introduced by Java 7 to support dynamic language. The specific implementation is complicated and we will not discuss it, for more information, see http://cr.openjdk.java.net /~ Briangoetz/lambda/lambda-translation.html, what we need to know is that Java implementation is very efficient and there is no need to worry about generating too many classes.
Lambda expressions are not anonymous internal classes. What is the type of Lambda expressions? Is a functional interface.
Functional Interface
Java 8 introduces the concept of a functional interface. A functional interface is also an interface, but only one abstract method is allowed. All the previously mentioned interfaces have only one abstract method, all of which are functional interfaces. The emphasis is on the "abstract" method because other methods can be defined in Java 8, which will be discussed later. Lambda expressions can be assigned to function interfaces, for example:
FileFilter filter = path -> path.getName().endsWith(".txt");FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt");Comparator<File> comparator = (f1, f2) -> f1.getName().compareTo(f2.getName());Runnable task = () -> System.out.println("hello world");
If you look at the definitions of these interfaces, you will find that they all have an annotation @ FunctionalInterface, for example:
@FunctionalInterfacepublic interface Runnable { public abstract void run();}
@ FunctionalInterface is used to clearly inform the user that this is a functional interface. However, this annotation is not required and is not added. As long as there is only one abstract method, it is also a functional interface. However, if more than one abstract method is defined, the Java compiler will report an error, which is similar to the Override annotation we introduced in section 85.
Predefined functional interfaces
Interface list
Java 8 defines a large number of predefined functional interfaces for common types of code passing. These functions are defined in the java. util. function package, mainly including:
Java 8 provides some special functions for the basic types of boolean, int, long, and double. For example, the main functions related to int are:
What are the functions? They are widely used in Java 8 function-based data processing Stream-related classes. For Stream, we will introduce them in the next section.
Even if Stream is not used, you can directly use these predefined functions in your own code. Let's look at some simple examples.
Predicate example
For example, we first define a simple Student class with two attributes: name and score. The getter/setter method is omitted as follows.
static class Student { String name; double score; public Student(String name, double score) { this.name = name; this.score = score; }}
There is a list of students:
List<Student> students = Arrays.asList(new Student[] { new Student("zhangsan", 89d), new Student("lisi", 89d), new Student("wangwu", 98d) });
In daily development, a common requirement of list processing is filtering. The types of lists are often different, And the filtering conditions change frequently, but the subject logic is similar, you can use Predicate to write a common method, as shown below:
public static <E> List<E> filter(List<E> list, Predicate<E> pred) { List<E> retList = new ArrayList<>(); for (E e : list) { if (pred.test(e)) { retList.add(e); } } return retList;}
This method can be used as follows:
// Filter students with a score of more than 90 = filter (students, t-> t. getScore ()> 90 );
Function example
Another common requirement of list processing is conversion. For example, if a student list is given, a list of names must be returned, or a name can be converted to uppercase for return. You can use Function to write a common method, as follows:
public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) { List<R> retList = new ArrayList<>(list.size()); for (T e : list) { retList.add(mapper.apply(e)); } return retList;}
The code that returns the name list based on the student list can be:
List<String> names = map(students, t -> t.getName());
Code that converts a student name to uppercase can be:
students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()));
Consumer example
In the above example of converting the Student name to uppercase, we create a new object for each student. Another common case is to directly modify the original object, you can use Consumer to write a common method, for example:
public static <E> void foreach(List<E> list, Consumer<E> consumer) { for (E e : list) { consumer.accept(e); }}
The above example can be changed:
foreach(students, t -> t.setName(t.getName().toUpperCase()));
The above examples are mainly used to demonstrate the basic concepts of functional interfaces. In practice, we should use the stream API described in the next section.
Method reference
Basic usage
Lambda expressions are often used to call a method of an object, for example:
List<String> names = map(students, t -> t.getName());
In this case, it can be further simplified as follows:
List<String> names = map(students, Student::getName);
Student: getName is a new syntax introduced by Java 8, called method reference. It is a short method of Lambda expressions, separated, class Name or variable name, followed by method name. The method can be an instance method or a static method, but it has different meanings.
Let's look at some examples and take Student as an example to add a static method:
public static String getCollegeName(){ return "Laoma School";}
Static Method
For static methods, the following statement:
Supplier<String> s = Student::getCollegeName;
It is equivalent:
Supplier<String> s = () -> Student.getCollegeName();
Their parameters are empty and the return type is String.
Instance method
The first parameter of an instance method is an instance of this type, for example, the following statement:
Function<Student, String> f = Student::getName;
It is equivalent:
Function<Student, String> f = (Student t) -> t.getName();
For Student: setName, It is a BiConsumer, that is:
BiConsumer<Student, String> c = Student::setName;
It is equivalent:
BiConsumer<Student, String> c = (t, name) -> t.setName(name);
Variable reference
If the first part of the method reference is the variable name, it is equivalent to calling the method of the object, for example:
Student t = new Student ("James", 89d); Supplier <String> s = t: getName;
It is equivalent:
Supplier<String> s = () -> t.getName();
And:
Consumer<String> consumer = t::setName;
It is equivalent:
Consumer<String> consumer = (name) -> t.setName(name);
Constructor
For the constructor method, the syntax referenced by the method is <class name >:: new, for example, Student: new, the following statement:
BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
It is equivalent:
BiFunction<String, Double, Student> s = Student::new;
Function combination
In the previous example, all functional interfaces are used as method parameters, and other parts pass specific code to them through Lambda expressions. functional interfaces and Lambda expressions can also be used as return values of methods, pass the code back to the caller and combine the two usage methods to construct a composite function, so that the program is simple and easy to read.
Next we will look at some examples. Before introducing the example, we first need to introduce Java 8 interface enhancement.
Static and default Interfaces
Before Java 8, the methods in the interface were all abstract methods without implementation bodies. Java 8 allows two new methods to be defined in the interface: static methods and default methods. They have implementation bodies, for example:
public interface IDemo { void hello(); public static void test() { System.out.println("hello"); } default void hi() { System.out.println("hi"); }}
Test () is a static method that can be called through IDemo. test. Before an interface cannot define static methods, related static methods are often defined in a separate class. For example, the Collection interface has a corresponding separate class Collections, in Java 8, you can write it directly in the interface. For example, the Comparator interface defines multiple static methods.
Hi () is a default method identified by the keyword default. Both the default method and the abstract method are interface methods. The difference is that it has a default implementation, and the Implementation class can change its implementation, it can also be left unchanged. The default method is mainly required for functional data processing to facilitate the addition of functions to interfaces.
Before there is no default method, it is difficult for Java to add functions to interfaces, such as the List interface, because there are too many code not controlled by Java JDK to implement this interface, if you add a method to an interface, the implementation of those interfaces will not be able to run on the new Java version. You must rewrite the code to implement the new method, which is obviously unacceptable. Function-based data processing requires some new methods for some interfaces. Therefore, the concept of the default method is introduced, and a new method is added to the interface, the existing implementation classes of interfaces do not need to be implemented.
For some examples, the List interface adds the sort method, which is defined:
default void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); }}
The stream method is added to the Collection interface, which is defined:
default Stream<E> stream() { return StreamSupport.stream(spliterator(), false);}
It should be noted that even if the method body can be defined, the interface and the abstract class are still different. The interface cannot define instance variables, but the abstract class can.
After learning about static and default methods, let's look at some examples of using them to implement composite functions.
Composite Method in Comparator
The Comparator interface defines the following static methods:
public static <T, U extends Comparable<? super U>> Comparator<T> comparing( Function<? super T, ? extends U> keyExtractor){ Objects.requireNonNull(keyExtractor); return (Comparator<T> & Serializable) (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));}
What does this method mean? It is used to build a Comparator. For example, in the previous example, the code for sorting files by file name is:
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
With the comparing method, the code can be simplified:
Arrays.sort(files, Comparator.comparing(File::getName));
In this way, is the code readability greatly enhanced? Why can the comparing method achieve this effect? It constructs and returns a Lambda expression that complies with the Comparator interface. The Comparator accepts the File parameter type. It uses the passed Function Code keyExtractor to convert the File to a String for comparison. Building and passing code in composite mode like comparing is not easy to read and understand, but it is easy to understand by callers.
Comparator has many default methods. Let's look at two methods:
default Comparator<T> reversed() { return Collections.reverseOrder(this);}default Comparator<T> thenComparing(Comparator<? super T> other) { Objects.requireNonNull(other); return (Comparator<T> & Serializable) (c1, c2) -> { int res = compare(c1, c2); return (res != 0) ? res : other.compare(c1, c2); };}
Reversed returns a new Comparator sorted in reverse order. ThenComparing is also a Comparator that returns a new Comparator. When two elements are sorted in the original order, use the provided other Comparator for comparison.
Let's take a look at an example to sort the student list in descending order of scores (with a high score before). If the scores are the same, they are sorted by name. The Code is as follows:
students.sort(Comparator.comparing(Student::getScore) .reversed() .thenComparing(Student::getName));
In this way, is the code easy to read?
Java. util. function Compound Method
Many functional interfaces in the java. util. function package define some composite methods. Let's look at some examples.
The Function interface is defined as follows:
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t));}
First, convert the T type parameter to the type R, call after to convert the R to V, and finally return the Type V.
It is also defined as follows:
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v));}
For a parameter of the V type, call before to convert V to the T type, and then call the current apply method to convert it to the R type.
Consumer and Predicate all have some composite methods. They are widely used in the function-based data processing API described in the next section. We will not discuss them here.
Summary
This section introduces some new concepts in Java 8, including Lambda expressions, functional interfaces, method references, static methods of interfaces, and default methods.
The most important change is that the transfer of code becomes simple, and the function becomes a first-class citizen in the Code world, which can be easily passed as a parameter and returned as a value, being used together to build new functions seems to be just a few small changes in syntax, but using these small changes can make the code more generic, flexible, and concise and easy to read, this is probably the wonder of functional programming.
In the next section, we will discuss the functional data processing APIs introduced by Java 8, which greatly simplify common set data operations.
(As in other chapters, all the code in this section is located at https://github.com/swiftma/program-logicand under Bao shuo.laoma.java8.c91)
----------------
For more information, see the latest article. Please pay attention to the Public Account "lauma says programming" (scan the QR code below), from entry to advanced, ma and you explore the essence of Java programming and computer technology. Retain All copyrights with original intent.