4. JDK8 Collection Flow-4

Keywords: Java Attribute less github

Previously, I wrote the introduction and screening of JDK8 collection flow, which is the open and intermediate operations used by the collection flow. This brings different ways of collecting data.

The code GitHub address in this section: https://github.com/WeidanLi/Java-jdk8-collect

I. Prepare:

It's still the old rule to use menus for examples. (Copy this part of the code is recommended)

II. Introduction of Collectors

Collectors are containers for collecting things. They are used for terminal operations when using collective streams. That is, we filter or filter streams in our daily business logic. Then we should always have a container to store these filtered elements. Then the collector comes in handy. An example of the simplest use of the collector as shown in the code (of course, I don't think people should be so bored at ordinary times)

 /**
 * A collector is a container for collecting data in a stream
 * cn.liweidan.collect.Demo01#demo01
 */
@Test
public void demo01(){
    List<Dish> collect = dishList.stream().collect(Collectors.toList());
    System.out.println(collect);
}

Predefined collectors provided by JDK8

Officials have provided us with some of the commonly needed functions of predefined collectors, namely:

  • Element Specification and Summary
  • Element grouping
  • Elemental partitioning

1. counting()

counting() is used to summarize the number of elements in the stream, and there are two ways to write them, as shown in the code. counting() is still relatively simple to use.

/**
 * cn.liweidan.collect.Demo01#demo02()
 * counting()Use
 */
@Test
public void demo02(){
    /* Query the number of menus with more than 400 calories */
    Long count = dishList.stream().filter(dish -> dish.getColories() > 400).collect(Collectors.counting());
    System.out.println("Number of menus with more than 400 calories:" + count);

    /* The second way of writing */
    count = dishList.stream().filter(dish -> dish.getColories() > 400).count();
    System.out.println("Number of menus with more than 400 calories:" + count);
}

2. Find the maximum or minimum value of an attribute of an element in a stream

We usually need to get the maximum and minimum value of an attribute in an object set for query. According to JDK8, we need to first traverse all the objects in the set, read the value of an attribute, then record the maximum or minimum value, and return the value after all traversal. This kind of writing is troublesome to describe and to write.

JDK8 streams can easily get the above requirements. You just need to define what you need in the stream, and when the stream is complete, you can get the value you need. But first we need to define a comparator to tell the JVM what value we need.

/**
 * cn.liweidan.collect.Demo01#demo03()
 * Take out the maximum and minimum
 */
@Test
public void demo03(){
    /* Define a calorie comparator */
    Comparator<Dish> comparator = Comparator.comparingInt(Dish::getColories);
    /* Collectors.maxBy(comparator)That is to say, the maximum in the outflow */
    Optional<Dish> collect = dishList.stream().collect(Collectors.maxBy(comparator));
    System.out.println(collect.get());
    /* Collectors.minBy(comparator)That is to say, the minimum value in the outflow */
    Optional<Dish> collect1 = dishList.stream().collect(Collectors.minBy(comparator));
    System.out.println(collect1.get());
}

But sometimes we need the maximum and minimum values to be taken out of a stream, and then we can use the latter grouping for implementation.

4. Summary

Summarization is the summary of all calories in a menu.

/**
 * cn.liweidan.collect.Demo01#demo04()
 * Summary: Calorie counts for all menus in the collection
 */
@Test
public void demo04(){
    int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
    System.out.println(collect);
}

In the example, we used summingInt method, and Collectors also provided Long and Double methods for statistics of Long and Double.

The summary includes not only sum, but also average, maximum and minimum, etc. Collectors also define averaging Int and Int Summary Statistics to extract the average values of element attributes and all statistical data (including maximum, minimum, mean, etc.)

/**
 * Query the average calories in the menu set and all the statistics
 */
@Test
public void demo05(){
    /* Query the average calories of all menus */
    Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
    System.out.println("Calorie mean:" + collect);
    /* All summary data in the query menu */
    IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories));
    System.out.println(collect1);// IntSummaryStatistics{count=9, sum=4200, min=120, average=466.666667, max=800}
}

5. joining connection string

The joining method can automatically call the toString method of the object, and then connect the strings together. If you need to use a separator, just pass the separator to the method.

/**
 * joining Connection string
 */
@Test
public void demo06(){
    String collect = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
    System.out.println(collect);
    // pork, beef, chicken, french fries, rice, season fruit, pizza, prawns, salmon
}

6. Implementing Custom Reduction--reduce

In fact, the average and maximum operations mentioned before mosquito nets are the official encapsulation of commonly used methods of reduce. If these methods provided by the government can not meet the requirements, then we need to customize the implementation of reduce by ourselves.

Reduc requires three parameters to be passed in:

  • The first parameter is the starting value of the protocol operation, that is, if the total value is to be counted, the starting value is 0.
  • The second parameter is the method of the object to be invoked, which is the calorie value of the menu.
  • The third parameter is a BinaryOperator operation, where we define how we operate on the values we get. That is to say, add up.

  • The first two parameters can also be removed by directly transferring only the required operations.

/**
 * cn.liweidan.collect.Demo01#demo07()
 * Collectors.reducing Use
 */
@Test
public void demo07(){
    /**
     * Take out the biggest calorie menu
     */
    Optional<Dish> collect = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getColories() > d2.getColories() ? d1 : d2));
    System.out.println(collect.get());

    /**
     * Calculate the total calorie value of the menu
     */
    Integer integer1 = dishList.stream().collect(Collectors.reducing(0,// initial value
            Dish::getColories,// Conversion function
            Integer::sum));// Cumulative function
    System.out.println(integer1);

    Integer integer2 = dishList.stream().map(Dish::getColories).reduce(Integer::sum).get();
    System.out.println(integer2);

    int sum = dishList.stream().mapToInt(Dish::getColories).sum();// Recommend
    System.out.println(sum);

}

mapToInt is recommended when calculating the sum, because it eliminates the performance consumption of automatic packing and unpacking.

IV. Grouping

1. Simple grouping

We often need to group data, especially when we operate a database. When we need to group from a collection, the code becomes very complex, and the grouping function can just solve this problem. We can group the types in the menu, or we can group the menu according to the size of the calories.

/**
 * Simple grouping
 */
@Test
public void test01(){
    /** Grouping by attribute type */
    Map<Dish.Type, List<Dish>> collect = dishList.stream().collect(Collectors.groupingBy(Dish::getType));
    System.out.println(collect);
    // {FISH=[Dish(name=prawns, vegetarain=false, colories=300, type=FISH),
    // Dish(name=salmon, vegetarain=false, colories=450, type=FISH)],
    // OTHER=[Dish(name=french fries, vegetarain=true, colories=530, type=OTHER),
    // Dish(name=rice, vegetarain=true, colories=350, type=OTHER),
    // Dish(name=season fruit, vegetarain=true, colories=120, type=OTHER),
    // Dish(name=pizza, vegetarain=true, colories=550, type=OTHER)],
    // MEAT=[Dish(name=pork, vegetarain=false, colories=800, type=MEAT),
    // Dish(name=beef, vegetarain=false, colories=700, type=MEAT), Dish(name=chicken, vegetarain=false, colories=400, type=MEAT)]}

    /** Customize simple grouping */
    Map<CaloricLevel, List<Dish>> map = dishList.stream().collect(Collectors.groupingBy(d -> {
        /** Take into account all situations when writing if here */
        if(d.getColories() <= 400){
            return CaloricLevel.DIET;
        }else if (d.getColories() <= 700){
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    }));
    System.out.println(map);
    // {FAT=[Dish(name=pork, vegetarain=false, colories=800, type=MEAT)],
    // NORMAL=[Dish(name=beef, vegetarain=false, colories=700, type=MEAT), Dish(name=french fries, vegetarain=true, colories=530, type=OTHER), Dish(name=pizza, vegetarain=true, colories=550, type=OTHER), Dish(name=salmon, vegetarain=false, colories=450, type=FISH)],
    // DIET=[Dish(name=chicken, vegetarain=false, colories=400, type=MEAT), Dish(name=rice, vegetarain=true, colories=350, type=OTHER), Dish(name=season fruit, vegetarain=true, colories=120, type=OTHER), Dish(name=prawns, vegetarain=false, colories=300, type=FISH)]}
}

2. Multilevel grouping

If we need multi-level grouping, such as grouping according to the type of menu and grouping according to calorie size. Then we can pass in a second groupingBy in groupingBy.

/**
 * Multilevel grouping
 */
@Test
public void test02(){
    Map<Dish.Type, Map<CaloricLevel, List<Dish>>> collect = 
            dishList.stream().collect(Collectors.groupingBy(Dish::getType, 
                    Collectors.groupingBy(d -> {
        if (d.getColories() <= 400) {
            return CaloricLevel.DIET;
        } else if (d.getColories() <= 700) {
            return CaloricLevel.NORMAL;
        } else {
            return CaloricLevel.FAT;
        }
    })));
    System.out.println(collect);
}

4. Collect data by subgroup

The second parameter in the previous section passes a groupingBy, but the second parameter of the collector can be passed into other collectors to achieve the purpose of cell phone subgroup data. For example, we can calculate the number of categories for each menu and pass in a counting

/**
 * Multilevel grouping for data collection
 */
@Test
public void test03(){
    /** Calculate the number of menus for each category */
    Map<Dish.Type, Long> typeLongMap = dishList.stream().collect(Collectors.groupingBy(Dish::getType, Collectors.counting()));
    System.out.println(typeLongMap);
}

5. Partition by predicate

Partitioning By: By passing an expression of a result with only true and false conditions, the returned result includes a set of true (satisfying conditions) and a set of false (not satisfying conditions).
For example, if we filter out prime numbers and non-prime numbers, we can pass an expression or a method that returns true and false, true for prime numbers and false for sets of non-prime numbers. The difference between collecting data by subgroup and collecting data by subgroup is that a collection of all elements that do not meet the criteria can also be collected here.

/**
 * Partition of numbers by prime and non-prime numbers
 */
@Test
public void test08(){
    int demoInt = 100;
    Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, demoInt).boxed()
            .collect(Collectors.partitioningBy(candidate -> isPrime(candidate)));
    System.out.println(collect);
}

public boolean isPrime(int candidate){
    /** Through the transmission of the number of square, we only need to transfer the number and the number of square can be compared, the number of calculations will be reduced. */
    int candidateRoot = (int) Math.sqrt((double) candidate);
    /** Generate a data stream of numbers from the beginning to the end of the square, and make redundancy with each element of the data stream. */
    return IntStream.rangeClosed(2, candidateRoot)
            .noneMatch(i -> candidate % i == 0);// Represents that there is no numeric remainder equal to zero between an element and the root of the square.
}

V. Collect Static Factory Method Table

Factory Method Return type purpose Example
toList List<T> Collect all items in the stream into List dishList.collect(Collectors.toList())
toSet Set<T> Collect all items in the stream into Set s dishList.collect(Collectors.toSet())
toCollection Collection<T> Collect all items in the flow into a collection created by a given supply source dishList.collect(Collectors. toCollection(), ArrayList::new)
counting Long Calculate the number of elements in the stream dishList.collect(Collectors.counting)
summingInt Integer Calculate the sum of an attribute of an element in a set int collect = dishList.stream().collect(Collectors.summingInt(Dish::getColories));
averagingInt Double Calculate the mean value of an attribute of an element in a set Double collect = dishList.stream().collect(Collectors.averagingInt(Dish::getColories));
summarizingInt IntSummaryStatistics Calculate the statistical value of an attribute of an element in a set, including the maximum, mean, sum, etc. IntSummaryStatistics collect1 = dishList.stream().collect(Collectors.summarizingInt(Dish::getColories));
joinging String Each element in the connection flow calls toString for stitching and splitting using the passed separator String collect = dishList.stream().map(Dish::getName).collect(Collectors.joining(", "));
maxBy Optional<T> Collect the maximum value of the attribute in the element through the passed comparator, and return Optional.empty() if the flow is empty. Optional<Dish> collect = dishList.stream().collect(Collectors.reducing((d1, d2) -> d1.getColories() > d2.getColories() ? d1 : d2));
minBy Optional<T> The minimum value of the attribute in the element is collected by the passing comparator, and Optional.empty() is returned if the flow is empty. slightly
reducing Types of Reduction Operations Beginning with an initial value as an accumulator, BinaryOperator is used to combine elements in the stream one by one, thereby reducing the flow to a single value. Integer integer1 = dishList.stream().collect(Collectors.reducing(0,Dish::getColories,Integer::sum));
collectingAndThen Types returned by conversion functions Wrap another collector and convert its results
groupingBy Map<K, List<T>> Grouping each value of an element in a convection slightly
partitioningBy Map<boolean, List<T>> Partitioning each value of an element in a convection slightly

6. Developing a Custom Collector

Mode I

If we need to develop our own custom collector, we need to let our own collector implement the Collector interface.
There are five ways to implement the Collector interface. Now let's take developing our own ToList collector as an example and write our own collector.

package cn.liweidan.custom.collector;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;

/**
 * <p>Desciption:Custom ToList Collector</p>
 * CreateTime : 2017/7/10 6:37 p.m.
 * Author : Weidan
 * Version : V1.0
 */
public class MyListCollector<T> implements Collector<T, List<T>, List<T>> {

    /*
        The first generic refers to the generic type of flow to be collected
        The second generic refers to the type of accumulator at collection time
        The third generic type refers to the type returned (which may not be a collection, such as counting()).
     */

    /**
     * Create a new result container
     * @return
     */
    @Override
    public Supplier<List<T>> supplier() {
        return ArrayList::new;
    }

    /**
     * Accumulate elements into containers
     * @return
     */
    @Override
    public BiConsumer<List<T>, T> accumulator() {
        return (list, item) -> list.add(item);
    }

    /**
     * Final conversion of the result container (as reflected in this step if conversion to Long return is required)
     * @return
     */
    @Override
    public Function<List<T>, List<T>> finisher() {
        return Function.identity();// There is no need to convert, just return this function directly.
    }

    /**
     * Specification operations on data in each sub-stream
     * That is to say, in the aggregate stream, the processor will continuously divide the aggregate stream into a certain number of small sub-streams, and then operate.
     * In this step, the elements in each stream are merged together.
     * @return
     */
    @Override
    public BinaryOperator<List<T>> combiner() {
        return (list1, list2) ->{
            list1.addAll(list2);
            return list1;
        };
    }

    /**
     * This method defines the return case of the flow. There are three cases, which are stored in the Characteristics enumeration.
     * UNORDERED: Statutory results are not affected by the traversal and cumulative sequence of projects
     * CONCURRENT: accumulator Functions can be called from multiple threads. If the collector does not label UNORDERED, it can only be specified without a data source
     * INDENTITY_FINISH: Indicates that the finisher method returns an identity function that can be skipped. Marking this case indicates that accumulator A can be converted to accumulator B without checking.
     * @return
     */
    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.CONCURRENT, Characteristics.IDENTITY_FINISH));
    }
}

Now we're testing our own collector. The difference between this and our own collector is that I don't define a factory pattern to get an instance of the toList collector, but I need to manually new it myself.

@Test
public void test(){
    List<Dish> collect = dishList.stream().collect(new MyListCollector<Dish>());
    System.out.println(collect);
}

Mode 2

Mode 2 is relatively simple, but the function is slightly worse. This is to customize the collector by using the overload method of the collector method, without the need to implement the Collector interface.

/**
 * Usage Mode 2 for Custom Collection
 */
@Test
public void test02(){
    ArrayList<Object> collect = dishList.stream().collect(
            ArrayList::new, // The supplier() method, equivalent to mode one, is used to create a container
            List::add,// The accumulator method, equivalent to mode one, is used to iterate through each element to add containers.
            List::addAll// All containers in protocol parallelism
    );
    System.out.println(collect);
}

It is also worth noting that this method does not convey any information about characteristics, that is to say, the default has been set to INDENTITY_FINISH and CONCURRENT.

7. Develop your own prime number collector

We have experimented with a prime collector before, where we use a custom collector to collect a certain range of prime numbers again. In the past, we used numbers less than the square root of the measured number to compare, and here we do further optimization, that is, we only take the prime number less than the square root of the measured number as divisor.

PrimeNumberCollector:

package cn.liweidan.custom.collector2;

import java.util.*;
import java.util.function.*;
import java.util.stream.Collector;

/**
 * <p>Desciption:Prime collector</p>
 * CreateTime : 2017/7/11 10:43 a.m.
 * Author : Weidan
 * Version : V1.0
 */
public class PrimeNumberCollector implements Collector<Integer,
                                            Map<Boolean, List<Integer>>,
                                        Map<Boolean, List<Integer>>> {

    public static <A> List<A> takeWhile(List<A> list, Predicate<A> p){
        int i = 0;
        for (A a : list) {
            if(!p.test(a)){
                return list.subList(0, i);
            }
            i++;
        }
        return list;
    }

    /**
     * Get all the prime numbers and the number under test. Comparing the square root less than the measured number with all the prime numbers, only the prime numbers less than the square root are calculated.
     * @param primes
     * @param candidate
     * @return
     */
    public static boolean isPrime(List<Integer> primes, int candidate) {
        int candidateRoot = (int) Math.sqrt((double) candidate);
        return takeWhile(primes, i -> i <= candidateRoot)
                .stream()
                .noneMatch(p -> candidate % p == 0);
    }

    @Override
    public Supplier<Map<Boolean, List<Integer>>> supplier() {
        return () -> new HashMap<Boolean, List<Integer>>(){{
            put(true, new ArrayList<>());
            put(false, new ArrayList<>());
        }};
    }

    @Override
    public BiConsumer<Map<Boolean, List<Integer>>, Integer> accumulator() {
        return (Map<Boolean, List<Integer>> acc, Integer candidate) -> {
            acc.get(isPrime(acc.get(true), candidate))
                    .add(candidate);
        };
    }

    @Override
    public BinaryOperator<Map<Boolean, List<Integer>>> combiner() {
        return null;
    }

    @Override
    public Function<Map<Boolean, List<Integer>>, Map<Boolean, List<Integer>>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
    }
}

Test:

@Test
public void test01(){
    Map<Boolean, List<Integer>> collect = IntStream.rangeClosed(2, 100).boxed().collect(new PrimeNumberCollector());
    System.out.println(collect);
}

Posted by thepeccavi on Fri, 14 Jun 2019 13:20:35 -0700