Java stream common operations on streams

Introduction

As we all know, starting from Java8, jdk has added a new Stream class to complement the collection class.

Stream provides a higher-order abstraction of Java collection operations and expressions using an intuitive way similar to querying data from a database with SQL statements.

This style treats the collection of elements to be processed as a stream, which travels through the pipeline and can be processed at the nodes of the pipeline, such as filtering, sorting, aggregating, etc.

The flow of operations is as follows.

+--------------------+ +------+ +------+ +--+ +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+ +------+ +------+ +--+ +-------+

Adopting the Stream API can greatly improve the productivity of Java programmers, allowing them to write efficient, clean and concise code.

Here, we will do a comparison with the actual daily development programming style.

Iteration operations

Iterating over collections

In everyday development, we often need to iterate through the elements of the collection object, for example, we will use the following way to iterate through the elements, and then filter out the set of a field, jdk7 operation.

/**
 * jdk7 Get a collection of user IDs from a collection object
 * @param userList
 * @return
 */
public List<Long> getUserIds(List<User> userList){
    List<Long> userIds = new ArrayList<>();
    for (User user : userList) {
        userIds.add(user.getUserId());
    }
    return userIds;
}

When Stream programming is used, with a single line of code, you can achieve.

/**
 * jdk8 Get a collection of user IDs from a collection object
 * @param userList
 * @return
 */
public List<Long> getUserIds(List<User> userList){
    List<Long> userIds = userList.stream().map(User::getUserId).collect(Collectors.toList());
    return userIds;
}

filter elements

Filtering elements is something you often encounter in everyday development, for example, in jdk7, we would do something like this.

/**
 * jdk7 filter out data from collection objects whose user ID is not null
 * @param userList
 * @return
 */
public List<Long> getUserIds7(List<User> userList){
    List<Long> userIds = new ArrayList<>();
    for (User user : userList) {
        if(user.getUserId() ! = null){
            userIds.add(user.getUserId());
        }
    }
    return userIds;
}

With Stream api, we only need to filter out the required data by the filter method to filter out the data whose user ID is not empty.

/**
 * jdk8 Filter data from a collection object whose user ID is not empty
 * @param userList
 * @return
 */
public List<Long> getUserIds8(List<User> userList){
    List<Long> userIds = userList.stream().filter(item -> item.getUserId() ! = null).map(User::getUserId).collect(Collectors.toList());
    return userIds;
}

Remove duplicates

If you want to exclude duplicate data for the returned collection content, the operation is also very simple, use Collectors.toSet() when merging.

/**
 * jdk8 Filter the data from the collection object with user ID not null and de-duplicate it
 * @param userList
 * @return
 */
public Set<Long> getUserIds(List<User> userList){
    Set<Long> userIds = userList.stream().filter(item -> item.getUserId() ! = null).map(User::getUserId).collect(Collectors.toSet());
    return userIds;
}

Data type conversion

In the actual development process, there are often problems with inconsistent data type definitions, for example, some systems, using String to receive, and some with Long, for this scenario, we need to convert them, and the operation is very simple.

/**
 * jdk8 Convert Long type data to String type
 * @param userIds
 * @return
 */
public List<String> getUserIds10(List<Long> userIds){
    List<String> userIdStrs = userIds.stream().map(x -> x.toString()).collect(Collectors.toList());
    return userIdStrs;
}

Array to collection

Sometimes we encounter the scenario of converting arrays to collections, and it is easy to use the stream api operation.

public static void main(String[] args) {
        // Create an array of strings
        String[] strArray = new String[]{"a", "b", "c"};
        //The converted List belongs to java.util.ArrayList and can be used for normal add/remove operations.
        List<String> strList = Stream.of(strArray).collect(Collectors.toList());
}

The collection to Map operation

In the actual development process, there is also one of the most frequently used operation is that a primary key field in the collection elements as key, elements as value, to achieve the needs of the collection to map, this demand in the data assembly is used very much, * ** especially in the company prohibit even table sql query operations, view data assembly can only be achieved at the code level ** .

For example, in the following code, the role table is associated with the role group ID information, when querying the role information, the role group name also needs to be displayed and processed, using the map method to match, which will be very efficient.

Code examples:

// Role group ID set
Set<Long> roleGroupIds = new HashSet<>();
//Query all role information
List<RoleInfo> dbList = roleInfoMapper.findByPage(request);
for (RoleInfo source : dbList) {
    roleGroupIds.add(source.getRoleGroupId());
    RoleInfoDto result = new RoleInfoDto();
    BeanUtils.copyProperties(source, result);
    resultList.add(result);
}
// Query the role group information
if (CollectionUtils.isNotEmpty(roleGroupIds)) {
    List<RoleGroupInfo> roleGroupInfoList = roleGroupInfoMapper.selectByIds(new ArrayList<>(roleGroupIds));
    if (CollectionUtils.isNotEmpty(roleGroupInfoList)) { if (CollectionUtils.isNotEmpty(roleGroupInfoList)) {
        // Convert List to Map, where id primary key is the key and object is the value
        Map<Long, RoleGroupInfo> sourceMap = new HashMap<>();
        for (RoleGroupInfo roleGroupInfo : roleGroupInfoList) {
            sourceMap.put(roleGroupInfo.getId(), roleGroupInfo);
        }
        // Wrap the role group name
        for (RoleInfoDto result : resultList) {
            if (sourceMap.containsKey(result.getRoleGroupId())) {
                result.setRoleGroupName(sourceMap.get(result.getRoleGroupId()).getName());
            }
        }
    }
}

Collection to map (no grouping)

In jdk7, to turn the elements in a collection to map, we usually use the following way.

/**
 * jdk7 converts a collection into a Map, where the user ID is used as the primary key key
 * @param userList
 * @return
 */
public Map<Long, User> getMap(List<User> userList){
    Map<Long, User> userMap = new HashMap<>();
    for (User user : userList) {
        userMap.put(user.getUserId(), user);
    }
    return userMap;
}

In jdk8, with the stream api approach, we only need one line of code to implement.

/**
 * jdk8 converts a collection into a Map, where the user ID is the primary key, and if there are duplicate keys in the collection, the first one matched is the primary key.
 * @param userList
 * @return
 */
public Map<Long, User> getMap(List<User> userList){
    Map<Long, User> userMap = userList.stream().collect(Collectors.toMap(User::getUserId, v -> v, (k1,k2) -> k1));
    return userMap;
}

Set to map (grouping)

In practice, there are scenarios that require us to add the same key to a collection, rather than overwrite it, which change how to do it?

If we use jdk7, we will probably do so.

/**
 * jdk7 converts a collection into a Map, adds the same key to a collection, and implements grouping
 * @param userList
 * @return
 */
public Map<Long, List<User>> getMapGroup(List<User> userList){
    Map<Long, List<User>> userListMap = new HashMap<>();
    for (User user : userList) {
        if(userListMap.containsKey(user.getUserId())){
            userListMap.get(user.getUserId()).add(user);
        } else {
            List<User> users = new ArrayList<>();
            users.add(user);
            userListMap.put(user.getUserId(), users);
        }
    }
    return userListMap;
}

And in jdk8, with the stream api approach, we only need one line of code to implement.

/**
 * jdk8 converts a collection into a Map, and adds the same key, to a collection, to achieve grouping
 * @param userList
 * @return
 */
public Map<Long, List<User>> getMapGroup(List<User> userList){
    Map<Long, List<User>> userMap = userList.stream().collect(Collectors.groupingBy(User::getUserId));
    return userMap;
}

Paging operations

The power of stream api is not only to perform various combinations of operations on collections, but also to support paging operations.

For example, the following array is sorted from smallest to largest, and after the sorting is done, 10 data are queried from the first row, as follows.

// The data to be queried
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5,10, 6, 20, 30, 40, 50, 60, 100);
List<Integer> dataList= numbers.stream().sort((x, y) -> x.compareTo(y)).skip(0).limit(10).collect(Collectors.toList());
System.out.println(dataList.toString());

where skip parameter indicates the first few rows, limit indicates the number of queries, similar to page capacity!

Find and match operations

stream api also supports lookups on collections, and also supports regular match mode.

  • allMatch (check if all elements match)
List<Integer> list = Arrays.asList(10, 5, 7, 3);
boolean allMatch = list.stream()//
       .allMatch(x -> x > 2);// whether all elements are greater than 2
System.out.println(allMatch);
  • findFirst (returns the first element)
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Optional<Integer> first = list.stream()//
        .findFirst();
Integer val = first.get();
System.out.println(val);// output 10
  • reduce (you can combine elements of a stream repeatedly to get a value)
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Integer result = list.stream()//
    . reduce(2, Integer::sum);
System.out.println(result);// output 27, actually equivalent to 2 + 10 + 5 + 7 + 3, is a cumulative

stream api supports a very large number of operations, only a few types are listed here, specifically when using, you can refer to the official website interface documentation.

Parallel operation

By parallelism, we mean that multiple tasks occur at the same point in time and are processed by different cpu’s without grabbing resources from each other; and by concurrency, we mean that multiple tasks occur at the same point in time but are processed by the same cpu and grab resources from each other.

For example, we use parallelStream to output the number of empty strings as follows.

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd", "", "jkl");
// Use parallel calculation method to get the number of empty strings
long count = strings.parallelStream().filter(string -> string.isEmpty()).count();

In practice, parallel operations are not necessarily faster than serial operations! For simple operations, with very large numbers and a multi-core server, Stream parallelism is recommended! Instead, serial operations are more reliable!