2

【Java技术指南】「实战盲区」深入透析Java8的Stream的原理及实战指南

 1 year ago
source link: https://blog.51cto.com/alex4dream/5895275
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【Java技术指南】「实战盲区」深入透析Java8的Stream的原理及实战指南

推荐 原创

洛神灬殇 2022-11-29 15:30:33 博主文章分类:【Java技术专题】 ©著作权

文章标签 操作符 java 数据 文章分类 其它 编程语言 yyds干货盘点 阅读数432

Java最有影响力的功能

要说到Java8的技术体系中,最让人难以忘怀的功能,那非Lambda和Stream莫属了。两者结合操作,达成天作之合,有点势不可挡。

它主要用于补充集合类,它的强大,相信用过它的朋友,能明显的感受到,不用使用for循环就能对集合作出很好的操作。

Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。

这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。

元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。

Stream流

Stream流处理的添加是Java 8中的主要新功能之一,要理解这些需要对Java8(lambda表达式、方法引用)有基本的工作知识。

【Java技术指南】「实战盲区」深入透析Java8的Stream的原理及实战指南_数据
Stream功能介绍
  • 首先,应将Java 8的Stream流与Java原本的 I / O流(例如:FileInputStream等)区分开,两者在本质角度而言是两个东西
  • 简而言之,流是数据源周围的包装器,使我们能够使用该数据源进行操作,并使批量处理方便快捷。
  • 流不存储数据,从这个意义上说,它不是数据结构,它也永远不会修改基础数据源。
  • 此新功能java.util.stream-支持对元素流进行功能样式的操作,例如对集合进行map-reduce转换。
Stream功能定义

Stream将要处理的元素集合看作一种流,在流的过程中,借助Stream API对流中的元素进行操作,比如:筛选、排序、聚合等。

【Java技术指南】「实战盲区」深入透析Java8的Stream的原理及实战指南_数据_02

采用Stream API可以极大提高 Java 程序员的生产力,让程序员写出高效率、干净、简洁的代码。

Stream的功能分析
  • Stream的操作符从流程操作是否完结大体上分为两种:中间操作符和终止操作符
流程中操作符

对于数据流来说,流程中操作符在执行指定处理程序后,数据流依然可以传递给下一级的操作符。

  • map(mapToInt,mapToLong,mapToDouble) :转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
  • flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,默认提供了拍平成int,long,double的操作符。
  • limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。
  • distint 去重操作,对重复元素去重,底层使用了equals方法。
  • filter 过滤操作,把不想要的数据过滤。
  • peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
  • skip 跳过操作,跳过某些元素。
  • sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。
流程终止操作符

数据经过中间加工操作,就轮到终止操作符上场了,终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

  • collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
  • count 统计操作,统计最终的数据个数。
  • findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。
  • noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
  • min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。
  • reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
  • forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。
  • toArray 数组操作,将数据流的元素转换成数组。

注意:中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作)

Stream流的API分析

Stream的创建
  1. 通过java.util.Collection.stream()方法用集合创建流
List<String> list = Arrays.asList("a", "b", "c");
// 创建一个顺序流
Stream<String> stream = list.stream();
// 创建一个并行流
Stream<String> parallelStream = list.parallelStream();
  1. 使用java.util.Arrays.stream(T[] array)方法用数组创建流
int[] array={1,3,5,6,8};
IntStream stream = Arrays.stream(array);
  1. 使用Stream的静态方法:of()、iterate()、generate()
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
Stream<Integer> stream2 = Stream.iterate(0, (x) -> x + 3).limit(4);
stream2.forEach(System.out::println); // 0 3 6 9
Stream<Double> stream3 = Stream.generate(Math::random).limit(3);
stream3.forEach(System.out::println);
stream和parallelStream的简单区分:
针对于筛选集合中的奇数,两者的处理不同之处:
  • stream是顺序流,由主线程按顺序对流执行操作;
【Java技术指南】「实战盲区」深入透析Java8的Stream的原理及实战指南_数据_03
parallelStream是并行流,内部以多线程并行执行的方式对流进行操作,但前提是流中的数据处理没有顺序要求。
【Java技术指南】「实战盲区」深入透析Java8的Stream的原理及实战指南_数据_04
遍历/匹配(foreach/find/match)

Stream也是支持类似集合的遍历和匹配元素的,只是Stream中的元素是以Optional类型存在的。Stream的遍历、匹配非常简单。

List<Integer> list = Arrays.asList(7, 6, 9, 3, 8, 2, 1);
// 遍历输出符合条件的元素
list.stream().filter(x -> x > 6).forEach(System.out::println);
// 匹配第一个
Optional<Integer> findFirst = list.stream().filter(x -> x > 6).findFirst();
// 匹配任意(适用于并行流)
Optional<Integer> findAny = list.parallelStream().filter(x -> x > 6).findAny();
// 是否包含符合特定条件的元素
boolean anyMatch = list.stream().anyMatch(x -> x < 6);
System.out.println("匹配第一个值:" + findFirst.get());
System.out.println("匹配任意一个值:" + findAny.get());
System.out.println("是否存在大于6的值:" + anyMatch);

筛选(filter)

筛选,是按照一定的规则校验流中的元素,将符合条件的元素提取到新的流中的操作。筛选出Integer集合中大于7的元素,并打印出来

public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(6, 7, 3, 8, 1, 2, 9);
Stream<Integer> stream = list.stream();
stream.filter(x -> x > 7).forEach(System.out::println);
}
}

聚合(max/min/count)

max、min、count这些字眼你一定不陌生,没错,在mysql中我们常用它们进行数据统计。Java stream中也引入了这些概念和用法,极大地方便了我们对集合、数组的数据统计工作。

获取String集合中最长的元素。
public class StreamTest {

public static void main(String[] args) {
List<String> list = Arrays.asList("adnm", "admmt", "pot", "xbangd", "weoujgsd");
Optional<String> max = list.stream().max(Comparator.comparing(String::length));
System.out.println("最长的字符串:" + max.get());
}
}
输出结果:
最长的字符串:weoujgsd
获取Integer集合中的最大值。
public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 9, 4, 11, 6);
// 自然排序
Optional<Integer> max = list.stream().max(Integer::compareTo);
// 自定义排序
Optional<Integer> max2 = list.stream().max(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
System.out.println("自然排序的最大值:" + max.get());
System.out.println("自定义排序的最大值:" + max2.get());
}
}

计算Integer集合中大于5的元素的个数。

public class StreamTest {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(7, 6, 4, 8, 2, 11, 9);
long count = list.stream().filter(x -> x > 5).count();
System.out.println("list中大于6的元素个数:" + count);
}
}

映射(map/flatMap)

映射可以将一个流的元素按照一定的映射规则映射到另一个流中。分为map和flatMap:

  • map:接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。
  • flatMap:接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流。

英文字符串数组的元素全部改为大写。

String[] strArr = { "abcd", "bcdd", "defde", "fTr" };
List<String> strList = Arrays.stream(strArr).map(String::toUpperCase).collect(Collectors.toList());
System.out.println("每个元素大写:" + strList);

整数数组每个元素+3

List<Integer> intList = Arrays.asList(1, 3, 5, 7, 9, 11);
List<Integer> intListNew = intList.stream().map(x -> x + 3).collect(Collectors.toList());
System.out.println("每个元素+3:" + intListNew);

将两个字符数组合并成一个新的字符数组。

List<String> list = Arrays.asList("m,k,l,a", "1,3,5,7");
List<String> listNew = list.stream().flatMap(s -> {
// 将每个元素转换成一个stream
String[] split = s.split(",");
Stream<String> s2 = Arrays.stream(split);
return s2;
}).collect(Collectors.toList());
归并操作(reduce)

归并,也称缩减,顾名思义,是把一个流缩减成一个值,能实现对集合求和、求乘积和求最值操作。

List<Integer> list = Arrays.asList(1, 3, 2, 8, 11, 4);
// 求和方式1
Optional<Integer> sum = list.stream().reduce(Integer::sum);
// 求和方式2
Optional<Integer> sum2 = list.stream().reduce(Integer::sum);
// 求和方式3
Integer sum3 = list.stream().reduce(0, Integer::sum);
// 求乘积
Optional<Integer> product = list.stream().reduce((x, y) -> x * y);
// 求最大值方式1
Optional<Integer> max = list.stream().reduce((x, y) -> x > y ? x : y);
// 求最大值写法2
Integer max2 = list.stream().reduce(1, Integer::max);
统计(count/averaging)

Collectors提供了一系列用于数据统计的静态方法:

  • 计数:count
  • 平均值:averagingInt、averagingLong、averagingDouble
  • 最值:maxBy、minBy
  • 求和:summingInt、summingLong、summingDouble
  • 统计以上所有:summarizingInt、summarizingLong、summarizingDouble

案例:统计员工人数、平均工资、工资总额、最高工资。

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
// 求总数
long count = personList.size();
// 求平均工资
Double average = personList.stream().collect(Collectors.averagingDouble(Person::getSalary));
// 求最高工资
Optional<Integer> max = personList.stream().map(Person::getSalary).max(Integer::compare);
// 求工资之和
int sum = personList.stream().mapToInt(Person::getSalary).sum();
// 一次性统计所有信息
DoubleSummaryStatistics collect = personList.stream().collect(Collectors.summarizingDouble(Person::getSalary));
System.out.println("员工总数:" + count);
System.out.println("员工平均工资:" + average);
System.out.println("员工最高工资:" + max.get());
System.out.println("员工工资总和:" + sum);
System.out.println("员工工资所有统计:" + collect);
分组(partitioningBy/groupingBy)

分区:将stream按条件分为两个Map,比如员工按薪资是否高于8000分为两部分。 分组:将集合分为多个Map,比如员工按性别分组。有单级分组和多级分组。 案例:将员工按薪资是否高于8000分为两部分;将员工按性别和地区分组

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "Washington"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "New York"));
personList.add(new Person("Anni", 8200, 24, "female", "New York"));
// 将员工按薪资是否高于8000分组
Map<Boolean, List<Person>> part = personList.stream().collect(Collectors.partitioningBy(x -> x.getSalary() > 8000));
// 将员工按性别分组
Map<String, List<Person>> group = personList.stream().collect(Collectors.groupingBy(Person::getSex));
// 将员工先按性别分组,再按地区分组
Map<String, Map<String, List<Person>>> group2 = personList.stream().collect(Collectors.groupingBy(Person::getSex, Collectors.groupingBy(Person::getArea)));
接合(joining)

joining可以将stream中的元素用特定的连接符(没有的话,则直接连接)连接成一个字符串。

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Tom", 8900, 23, "male", "New York"));
personList.add(new Person("Jack", 7000, 25, "male", "Washington"));
personList.add(new Person("Lily", 7800, 21, "female", "Washington"));
String names = personList.stream().map(Person::getName).collect(Collectors.joining(","));
System.out.println("所有员工的姓名:" + names);
List<String> list = Arrays.asList("A", "B", "C");
String string = list.stream().collect(Collectors.joining("-"));
System.out.println("拼接后的字符串:" + string);
排序(sorted)

sorted,中间操作。有两种排序:

  • sorted():自然排序,流中元素需实现Comparable接口
  • sorted(Comparator com):Comparator排序器自定义排序

案例:将员工按工资由高到低(工资一样则按年龄由大到小)排序

List<Person> personList = new ArrayList<Person>();
personList.add(new Person("Sherry", 9000, 24, "female", "New York"));
personList.add(new Person("Tom", 8900, 22, "male", "Washington"));
personList.add(new Person("Jack", 9000, 25, "male", "Washington"));
personList.add(new Person("Lily", 8800, 26, "male", "New York"));
personList.add(new Person("Alisa", 9000, 26, "female", "New York"));
// 按工资升序排序(自然排序)
List<String> newList = personList.stream().sorted(Comparator.comparing(Person::getSalary)).map(Person::getName)
.collect(Collectors.toList());
// 按工资倒序排序
List<String> newList2 = personList.stream().sorted(Comparator.comparing(Person::getSalary).reversed())
.map(Person::getName).collect(Collectors.toList());
// 先按工资再按年龄升序排序
List<String> newList3 = personList.stream()
.sorted(Comparator.comparing(Person::getSalary).thenComparing(Person::getAge)).map(Person::getName)
.collect(Collectors.toList());
// 先按工资再按年龄自定义排序(降序)
List<String> newList4 = personList.stream().sorted((p1, p2) -> {
if (p1.getSalary() == p2.getSalary()) {
return p2.getAge() - p1.getAge();
} else {
return p2.getSalary() - p1.getSalary();
}
}).map(Person::getName).collect(Collectors.toList());
提取/组合

流也可以进行合并、去重、限制、跳过等操作。

String[] arr1 = { "a", "b", "c", "d" };
String[] arr2 = { "d", "e", "f", "g" };
Stream<String> stream1 = Stream.of(arr1);
Stream<String> stream2 = Stream.of(arr2);
// concat:合并两个流 distinct:去重
List<String> newList = Stream.concat(stream1, stream2).distinct().collect(Collectors.toList());
// limit:限制从流中获得前n个数据
List<Integer> collect = Stream.iterate(1, x -> x + 2).limit(10).collect(Collectors.toList());
// skip:跳过前n个数据
List<Integer> collect2 = Stream.iterate(1, x -> x + 2).skip(1).limit(5).collect(Collectors.toList());
System.out.println("流合并:" + newList);
System.out.println("limit:" + collect);
System.out.println("skip:" + collect2);

实际案例操作

日常开发中,我们经常需要需要遍历集合对象中的元素,例如,我们会采用如下方式进行遍历元素,然后过滤出某个字段的集合,当采用 Stream 编程之后,只需要通过一行代码,即可实现:

/**
* jdk8 从集合对象中获取用户ID集合
* @param userList
* @return
*/
public List<Long> getUserIds(List<User> userList){
List<Long> userIds = userList.stream().map(User::getUserId).collect(Collectors.toList());
return userIds;
}

筛选元素,是日常开发中经常会碰到,例如在 jdk8,采用 Stream api,我们只需要通过filter方法来筛选出需要的数据,即可过滤出用户ID不为空的数据。

/**
* jdk8 从集合对象中筛选出用户ID不为空的数据
* @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;
}
删除重复的内容

如果你想对返回的集合内容排除重复的数据,操作也很简单,在合并的时候使用Collectors.toSet()即可!

/**
* jdk8 从集合对象中筛选出用户ID不为空的数据,并进行去重
* @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;
}
数据类型转换

在实际的开发过程中,经常会出现数据类型定义不一致的问题,例如有的系统,使用String接受,有的是用Long,对于这种场景,我们需要将其转换,操作也很简单

/**
* jdk8 将Long类型数据转换成String类型
* @param userIds
* @return
*/
public List<String> getUserIds10(List<Long> userIds){
List<String> userIdStrs = userIds.stream().map(x -> x.toString()).collect(Collectors.toList());
return userIdStrs;
}
数组转集合

我们还会碰到,前端传给我们的是一个数组,但是我们需要转成集合,采用 stream api 操作也很简单!

public static void main(String[] args) {
//创建一个字符串数组
String[] strArray = new String[]{"a","b","c"};
//转换后的List 属于 java.util.ArrayList 能进行正常的增删查操作
List<String> strList = Stream.of(strArray).collect(Collectors.toList());
}
集合转Map操作

在实际的开发过程中,还有一个使用最频繁的操作就是,将集合元素中某个主键字段作为key,元素作为value,来实现集合转map的需求,这种需求在数据组装方面使用的非常多,尤其是在禁止连表 sql 查询操作的公司,视图数据的拼装只能在代码层面来实现。

例如,下面这段代码,角色表里面关联角色组ID信息,当查询角色信息的时候,需要把角色组名称也展示处理,采用map方式来匹配,效率会非常高。

实际代码案例分享
//角色组ID集合
Set<Long> roleGroupIds = new HashSet<>();
//查询所有的角色信息
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);
}
//查询角色组信息
if (CollectionUtils.isNotEmpty(roleGroupIds)) {
List<RoleGroupInfo> roleGroupInfoList = roleGroupInfoMapper.selectByIds(new ArrayList<>(roleGroupIds));
if (CollectionUtils.isNotEmpty(roleGroupInfoList)) {
//将List转换成Map,其中id主键作为key,对象作为value
Map<Long, RoleGroupInfo> sourceMap = new HashMap<>();
for (RoleGroupInfo roleGroupInfo : roleGroupInfoList) {
sourceMap.put(roleGroupInfo.getId(), roleGroupInfo);
}
//封装角色组名称
for (RoleInfoDto result : resultList) {
if (sourceMap.containsKey(result.getRoleGroupId())) {
result.setRoleGroupName(sourceMap.get(result.getRoleGroupId()).getName());
}
}
}
}
集合转 map(不分组)

在 jdk8 中,采用 stream api的方式,我们只需要一行代码即可实现,jdk8 将集合转换成Map,其中用户ID作为主键key,如果集合对象有重复的key,以第一个匹配到的为主

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;
}

打开Collectors.toMap方法源码,一起来看看到底是啥。

public static <T, K, U>
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper,
Function<? super T, ? extends U> valueMapper,
BinaryOperator<U> mergeFunction) {
return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new);
}
参数表可以看出:
  • 第一个参数:表示 key
  • 第二个参数:表示 value
  • 第三个参数:表示某种规则

上文中的Collectors.toMap(User::getUserId, v -> v, (k1,k2) -> k1),表达的意思就是:

  • userId的内容作为key
  • v -> v是表示将元素user作为value。
  • (k1,k2) -> k1表示如果存在相同的key,将第一个匹配的元素作为内容。
集合转map(分组)

实际的操作中,有一些场景需要我们将相同的key,加入到一个集合,而不是覆盖,哪改如何做呢?而在 jdk8 中,采用 stream api的方式,我们只需要一行代码即可实现。

/**
* jdk8 将集合转换成Map,将相同的key,加入到一个集合中,实现分组
* @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;
}

stream api的强大之处还不仅仅是对集合进行各种组合操作,还支持分页操作。

例如,将如下的数组从小到大进行排序,排序完成之后,从第1行开始,查询10条数据出来,操作如下:

//需要查询的数据
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().sorted((x, y) -> x.compareTo(y)).skip(0).limit(10).collect(Collectors.toList());
System.out.println(dataList.toString());

其中skip参数表示第几行,limit表示查询的数量,类似页容量!

查找与匹配操作

stream api 还支持对集合进行查找,同时还支持正则匹配模式。

allMatch(检查是否匹配所有元素)

  • 是否全部元素都大于2
List<Integer> list = Arrays.asList(10, 5, 7, 3);
boolean allMatch = list.stream()
.allMatch(x -> x > 2);
System.out.println(allMatch);
findFirst(返回第一个元素)
  • 获取第一个元素
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Optional<Integer> first = list.stream()
.findFirst();
Integer val = first.get();
System.out.println(val);//输出10
  • (可以将流中元素反复结合起来,得到一个值)
List<Integer> list = Arrays.asList(10, 5, 7, 3);
Integer result = list.stream()
.reduce(2, Integer::sum);
System.out.println(result);//输出27,其实相当于2+10+5+7+3,就是一个累加

stream api 支持的操作方法非常多,这里只列举了几种类型,具体在使用的时候,可以参考官网接口文档说明!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK