Java turned 25 years old this year, older than some of the teenagers in this room, but unfortunately not older than me. Java is too young. (Would I say it’s because I’m old?)

Last month, a pilot version of Java 15 was quietly released, but one of the mysteries in the Java world has always been, “Send it, send it, my favorite Java 8.”

According to Snyk and The Java Magazine’s 2020 JVM Ecology Survey, 64 percent of all Java releases are still using Java 8. Others are probably already using Java 9, Java 11, Java 13, and some fairy developers are still using JDK 1.6 and 1.7.

Although Java 8 has been released for many years and is widely used, it is surprising that many students have never used Java 8’s new features, such as Lambda expressions, method references, and today’s Stream. Stream is an easy-to-use, functional-style API wrapped around Lambda and method references.

Java 8 was released in 2014. To be honest, I didn’t use Stream until a long time after Java 8 was released, since I was still struggling in the world of C# when Java 8 was released, and Lambda expressions were used much earlier. Because Lambda is easy to use in Python, yes, I’ve been writing Python for longer than I’ve been writing Java.

The Stream API you use is essentially a functional programming style, where “functions” are method references and “expressions” are Lambda expressions.

Lambda expressions

A Lambda expression is an anonymous function. A Lambda expression is named after the Lambda calculus in mathematics and directly corresponds to the Lambda abstraction. It is an anonymous function, that is, a function without a function name. Lambda expressions can represent closures.

In Java, a Lambda expression is formatted like this

// No argument, no return value () -> log.info()"Lambda"(int a, int b) -> {a+b}Copy the code

It is equivalent to

log.info("Lambda");

private int plus(int a, int b){
  	return a+b;
}
Copy the code

A new Thread needs an instance of an object that implements from the Runnable type. A better way to create a new Thread is to create a new class that implements from the Runnable type. The class implements Runnable, and then passes an instance of the new class as a parameter to Thread. Anonymous inner classes, on the other hand, don’t need to find an object to accept, just as a parameter.

new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Quickly create and start a thread");
    }
}).run();
Copy the code

But does it feel messy and uncouth to write this way, and Lambda expressions are a different matter.

new Thread(()->{
    System.out.println("Quickly create and start a thread");
}).run();
Copy the code

How, such change, instantaneous feeling pure and fresh free from vulgarity many, concise grace many.

Lambda expressions can achieve the same effect by simplifying the form of anonymous inner classes, but Lambda is much more elegant. Although the ultimate goal is the same, the internal implementation principle is different.

An anonymous inner class creates a new anonymous inner class after compilation, whereas Lambda is implemented by calling the JVM Invokedynamic directive and does not generate a new class.

Method references

Method references make it possible to assign a method to a variable or pass it as an argument to another method. The :: double colon is used as a symbol for method references, such as the following two lines that refer to the parseInt method of the Integer class.

Function<String, Integer> s = Integer::parseInt;
Integer i = s.apply("10");
Copy the code

Or the following two lines, referring to the compare method of the Integer class.

Comparator<Integer> comparator = Integer::compare; Int result = comparator.com pare said (100, 10);Copy the code

For example, the following two lines of code refer to the compare method of the Integer class with a different return type, but both execute correctly and return correctly.

IntBinaryOperator intBinaryOperator = Integer::compare; Int result = intBinaryOperator. ApplyAsInt (10100);Copy the code

Believe some students to see here I am afraid is the following state, completely unreasonable, also too casual, return to anyone can receive disk.

Don’t get too excited. Come on. Now let’s get rid of the mask.

Q: What methods can be cited?

A: Let’s just say that any method you have access to can be referenced.

Q: What type is the return value?

A: Function, Comparator, IntBinaryOperator. It looks like there are no rules.

The type returned is a Java 8-specific FunctionalInterface annotated with @functionalinterface.

For example, the Function interface is defined as follows:

@FunctionalInterface
public interface Function<T, R> {
    R apply(T t);
}
Copy the code

It is also important that the number of arguments, types, and return types of your reference methods correspond to the method declarations in the functional interface.

For example, the integer. parseInt method is defined as follows:

public static int parseInt(String s) throws NumberFormatException {
    return parseInt(s,10);
}
Copy the code

ParseInt = Function (); Function (); Function (); Function (); Function ();

This will correctly receive a reference to an Integer::parseInt method and call Funciton’s apply method, in which case the corresponding integer.parseint method will be called.

By applying this standard to the Integer::compare method, it is easy to understand why the Integer::compare method can be received using either the Comparator<Integer> or IntBinaryOperator, and calls to each method return the correct result.

The Integer.com pARE method is defined as follows:

public static int compare(int x, int y) {
    return(x < y) ? -1 : ((x == y) ? 0:1); }Copy the code

The return value is of type int, two arguments, and both arguments are of type int.

Then look at the functional interface definitions for the Comparator and IntBinaryOperator and their corresponding methods:

@FunctionalInterface
public interface Comparator<T> {
    int compare(T o1, T o2);
}

@FunctionalInterface
public interface IntBinaryOperator {
    int applyAsInt(int left, int right);
}
Copy the code

It will match correctly, so both functional interfaces used in the previous example will work. In fact, there are more than two, as long as in some functional interface declare such a method: two arguments, the argument type is int or generic, and return value is int or generic, can be perfectly accepted.

A number of functional interfaces are defined in the JDK, mainly under the java.util.function package, and java.util.Comparator is specifically used as a custom comparator. Runnable is also a functional interface.

Implement an example yourself

1. Define a functional interface and add a method

Defines a FunctionalInterface named KiteFunction, uses the @functionalinterface annotation, and then declares a method run that takes two parameters, both of generic type, and returns a generic result.

It is also important to note that only one implementable method can be declared in a functional interface. You cannot declare a run method and a start method, and the compiler will not know which one to use. However, the method decorated with the default keyword has no effect.

@functionalInterface Public interface KiteFunction<T, R, S> {/** * defines a two-parameter method * @param T * @param S * @return
     */
    R run(T t,S s);
}
Copy the code

2. Define a method corresponding to the run method in KiteFunction

In the FunctionTest class we define the method DateFormat, a method that formats the LocalDateTime type to a string type.

public class FunctionTest {
    public static String DateFormat(LocalDateTime dateTime, String partten) {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
        returndateTime.format(dateTimeFormatter); }}Copy the code

3. Call by method reference

Normally we would just use functiontest.dateformat ().

And in a functional way, it looks like this.

KiteFunction<LocalDateTime,String,String> functionDateFormat = FunctionTest::DateFormat;
String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");
Copy the code

Instead of defining the DateFormat method outside, I could use an anonymous inner class as follows.

public static void main(String[] args) throws Exception {
  
    String dateString = new KiteFunction<LocalDateTime, String, String>() {
        @Override
        public String run(LocalDateTime localDateTime, String s) {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s);
            return localDateTime.format(dateTimeFormatter);
        }
    }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
    System.out.println(dateString);
}
Copy the code

As mentioned in the first Runnable example, such an anonymous inner class can be abbreviated as a Lambda expression, which looks like this:

public static void main(String[] args) throws Exception {

        KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> {
            DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten);
            return dateTime.format(dateTimeFormatter);
        };
        String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss");
        System.out.println(dateString);
}
Copy the code

Use Lambda expressions such as (LocalDateTime dateTime, String partten) -> {} to return method references directly.

Stream API

It takes a lot of work to explain the use of the Stream API. Know why, but also know why.

Stream is a powerful tool in Java 8 for handling collection data. A lot of complicated methods, such as filtering and grouping, require a lot of code. It is often possible to use a Stream in one line of code, and since streams are chained operations, one line of code may call several methods.

The Collection interface provides the stream() method, which makes it easy to use the Stream API for various operations within a Collection. It is important to note that nothing we do will affect the source collection, and you can extract multiple streams on a collection at the same time.

The Stream interface, which inherits from BaseStream, accepts arguments of the type referenced by the Stream method. For example, the filter method accepts arguments of the type Predicate, which is a functional interface used for conditional comparison, filtering, and filtering. This functional interface is also used in JPA for query concatenation.

public interface Stream<T> extends BaseStream<T, Stream<T>> { Stream<T> filter(Predicate<? super T> predicate); // Other interfaces}Copy the code

Here’s a look at the common Stream apis.

of

You can receive a generic object or become a generic collection to construct a Stream object.

private static void createStream(){
    Stream<String> stringStream = Stream.of("a"."b"."c");
}
Copy the code

empty

Create an empty Stream object.

concat

Connect two Streams, return a new Stream without changing either Steam object.

private static void concatStream(){
    Stream<String> a = Stream.of("a"."b"."c");
    Stream<String> b = Stream.of("d"."e");
    Stream<String> c = Stream.concat(a,b);
}
Copy the code

max

It is generally used to find the maximum value in a set of numbers, or the entity that has the maximum value according to the attributes of the number type in the entity. It receives a Comparator<T>, which is a functional interface type used to define a comparison between two objects. For example, the following method uses the Integer::compareTo method reference.

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Integer max = integerStream.max(Integer::compareTo).get();
    System.out.println(max);
}
Copy the code

Of course, we can also customize a Comparator of our own, and review method references in the form of Lambda expressions.

private static void max(){
    Stream<Integer> integerStream = Stream.of(2, 2, 100, 5);
    Comparator<Integer> comparator =  (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1);
    Integer max = integerStream.max(comparator).get();
    System.out.println(max);
}
Copy the code

min

This is the same as Max, but minimizes.

findFirst

Gets the first element in the Stream.

findAny

Fetch an element from a Stream. In the serial case, the first element is usually returned, but not in the parallel case.

count

Returns the number of elements.

Stream<String> a = Stream.of("a"."b"."c");
long x = a.count();
Copy the code

peek

Create a channel that performs an operation on each element of the Stream, corresponding to the functional interface of Consumer<T>, which, as its name implies, consumes Stream elements, such as the following method, which converts each element to the corresponding uppercase and outputs it.

private static void peek() {
    Stream<String> a = Stream.of("a"."b"."c");
    List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList());
}
Copy the code

forEach

Similar to peek, forEach accepts a consumer functional interface that allows operations to be performed on each element. Unlike Peek, however, after forEach is executed, the Stream is actually consumed and no further operations can be performed on it. Peek, on the other hand, is still an operable Stream object.

By the way, when we use the Stream API, we do a chain operation. This is because many methods, such as the filter method, return the Stream object that was processed by the current method. So the Stream API is still available.

private static void forEach() {
    Stream<String> a = Stream.of("a"."b"."c");
    a.forEach(e->System.out.println(e.toUpperCase()));
}
Copy the code

forEachOrdered

The function is the same as forEach, except that forEachOrdered consumes the elements in the Stream in the order in which they were inserted. ForEach and forEachOrdered behave differently when parallelism is enabled.

Stream<String> a = Stream.of("a"."b"."c");
a.parallel().forEach(e->System.out.println(e.toUpperCase()));
Copy the code

When using the code above, the output might be B, A, C or A, C, B or A, B, C, while using the code below, it would be A, B, C every time

Stream<String> a = Stream.of("a"."b"."c");
a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));
Copy the code

limit

Get the first n items of data, similar to MySQL limit, except that only one parameter is accepted, namely the number of items of data.

private static void limit() {
    Stream<String> a = Stream.of("a"."b"."c");
    a.limit(2).forEach(e->System.out.println(e));
}
Copy the code

The above code prints a, B.

skip

Skip the first n pieces of data, such as the following code, and return c.

private static void skip() {
    Stream<String> a = Stream.of("a"."b"."c");
    a.skip(2).forEach(e->System.out.println(e));
}
Copy the code

distinct

The following method returns elements A, b, and c, and keeps only one of the duplicate elements b.

private static void distinct() {
    Stream<String> a = Stream.of("a"."b"."c"."b");
    a.distinct().forEach(e->System.out.println(e));
}
Copy the code

sorted

There are two overloads, one with no arguments and the other with a Comparator parameter.

The non-parameter type is sorted in natural order. It is only suitable for simple elements, such as numbers and letters.

private static void sorted() {
    Stream<String> a = Stream.of("a"."c"."b");
    a.sorted().forEach(e->System.out.println(e));
}
Copy the code

You need to customize the sorting rules for parameters. For example, the following method sorts the order by the size of the second letter. The output is A1, b3, and c6.

private static void sortedWithComparator() {
    Stream<String> a = Stream.of("a1"."c6"."b3"); a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))? 1:-1).forEach(e->System.out.println(e)); }Copy the code

To better illustrate the next few apis, I simulated a few similar pieces of data that are often used in projects, 10 pieces of user information.

private static List<User> getUserData() {
    Random random = new Random();
    List<User> users = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
        User user = new User();
        user.setUserId(i);
        user.setUserName(String.format("Ancient kite % S", i));
        user.setAge(random.nextInt(100));
        user.setGender(i % 2);
        user.setPhone("18812021111");
        user.setAddress("No");
        users.add(user);
    }
    return users;
}
Copy the code

filter

Filter the data that meets the conditions. For example, in the following method, records with gender 0 and age greater than 50 are screened out.

private static void filter(){ List<User> users = getUserData(); Stream<User> stream = users.stream(); stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e)); // / stream. Filter (new Predicate<User>() {// @override // public Booleantest(User user) {
//            return user.getGender().equals(0) && user.getAge()>50;
//        }
//    }).forEach(e->System.out.println(e));
}
Copy the code

map

The map method interface is declared as follows. It takes a Function interface, best translated as a map, which maps new types from raw data elements.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);
Copy the code

Function is declared by observing the apply method, which takes a T parameter and returns an R parameter. It’s good for converting one type to another, which is what map was originally designed for, and for changing the type of the current element, such as converting an Integer to a String or a DAO entity to a DTO instance.

Of course, the T and R types can be the same, so that it is no different from the peek method.

@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);
}
Copy the code

For example, the following method, which should be a common requirement of business systems, converts User to the data format output by the API.

private static void map(){ List<User> users = getUserData(); Stream<User> stream = users.stream(); List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList()); } private static UserDto dao2Dto(User user){ UserDto dto = new UserDto(); BeanUtils.copyProperties(user, dto); // Other extra processingreturn dto;
}
Copy the code

mapToInt

Converts an element to an int and encapsulates it based on the map method.

mapToLong

Convert the element to type Long and encapsulate it based on the map method.

mapToDouble

Converts the element to a Double and encapsulates it based on the map method.

flatMap

This is used in some special scenarios, when your Stream is one of the following structures, you need to use the flatMap method, which is used to flatten the original TWO-DIMENSIONAL structure.

  1. Stream<String[]>
  2. Stream<Set<String>>
  3. Stream<List<String>>

The above three structures can be converted into Stream<String> through the flatMap method, which is convenient for other operations later.

For example, the following method flattens List<List<User>> and then uses map or some other method.

private static void flatMap(){
    List<User> users = getUserData();
    List<User> users1 = getUserData();
    List<List<User>> userList = new ArrayList<>();
    userList.add(users);
    userList.add(users1);
    Stream<List<User>> stream = userList.stream();
    List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList());
}
Copy the code

flatMapToInt

The usage references flatMap, which flattens elements into int types and encapsulates them based on the flatMap method.

flatMapToLong

The usage references flatMap, which flatters elements to type Long and encapsulates them based on the flatMap method.

flatMapToDouble

The usage references flatMap, which flatters elements into Double types and encapsulates them based on the flatMap method.

collection

After a series of operations, most of the time our end result is not to fetch Stream data, but to translate the result into common data structures such as lists and maps, and collection is for this purpose.

Take the example of the map method. After converting the object type, the result set we need is a List<UserDto > type. Use the COLLECT method to convert the Stream to the type we need.

Here is the definition of the collect interface method:

<R, A> R collect(Collector<? super T, A, R> collector);
Copy the code

The following example demonstrates filtering a simple Integer Stream out of values greater than 7 and then converting it to a List<Integer> collection, using the Collectors Collectors Collectors. ToList ().

private static void collect(){
    Stream<Integer> integerStream = Stream. Of,2,5,7,8,12,33 (1); List<Integer> list =integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList());
}
Copy the code

Collect; collect; collect; collect; collect; collect; collect; collect; collect; collect So this is an ArrayList from creation to calling addAll.

private static void collect(){
    Stream<Integer> integerStream = Stream. Of,2,5,7,8,12,33 (1); List<Integer> list =integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add,
            ArrayList::addAll);
}
Copy the code

This is the same logic we used to customize Collectors, but we don’t need to customize at all. Collectors already have a lot of ready-to-use Collectors. For example, Collectors. ToList (), Collectors. ToSet (), and Collectors. GroupingBy (), for example, can be grouped by the userId field. A Map with userId as the key and List as the value is returned, or the number of keys is returned.

// userId:List<User> Map<String,List<User>> Map = user.stream().collect(Collectors. GroupingBy (User::getUserId)); // Return userId: number of groups Map<String,Long> Map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));Copy the code

toArray

Collection returns a list, map, etc. ToArray returns an array, with two overloads and an empty argument, and returns Object[].

The other accepts an IntFunction<R> type parameter.

@FunctionalInterface
public interface IntFunction<R> {

    /**
     * Applies this function to the given argument.
     *
     * @param value the function argument
     * @return the function result
     */
    R apply(int value);
}
Copy the code

For example, the argument is User[]::new (new) an array of users with the length of the last Stream.

private static void toArray() {
    List<User> users = getUserData();
    Stream<User> stream = users.stream();
    User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new);
}
Copy the code

reduce

Its function is to use the calculation result of the last time in each calculation. For example, in the sum operation, the sum of the first two numbers is added to the sum of the third number, and then the fourth number is added to the position of the last number. Finally, the result is returned, which is the working process of reduce.

private static void reduce(){
    Stream<Integer> integerStream = Stream. Of,2,5,7,8,12,33 (1); Integer sum =integerStream.reduce(0,(x,y)->x+y);
    System.out.println(sum);
}
Copy the code

In addition, reduce is used by many Collectors methods, such as groupingBy, minBy, and maxBy.

Parallel Stream

Streams are essentially used for data processing. To speed up processing, the Stream API provides a way to process streams in parallel. ParallelStream objects can be created using either users.parallelstream () or users.stream().parallel(). The supported apis are almost identical to regular streams.

By default, a ForkJoinPool thread pool is used for parallel streams. Customization is also supported, though this is not usually necessary. ForkJoin’s divide-and-conquer strategy fits nicely with parallel stream processing.

While parallelism sounds like a nice word, using parallel streams is not always the right thing to do, and many times it’s not necessary at all.

When should parallel flow operations be used or not used?

  1. Using parallel streams on a multi-core CPU sounds like nonsense.
  2. Normal serial streams are fine for small data volumes, and parallel streams have little impact on performance.
  3. CPU intensive computations are good for parallel streams, while IO intensive computations are slower for parallel streams.
  4. Although the computation is parallel, it may be fast, but in the end, most of the time we still use collect merge. If the merge cost is too high, it is not suitable to use parallel Stream.
  5. Some operations, such as limit, findFirst, and forEachOrdered operations that depend on the order of elements, are not suitable for parallel streams.

The last

Java is 25 years old, how many students like me are using Java 8, how many students are using earlier versions, please tell your story.