本文收集了网络上Stream流的用法和我在项目中的实际使用案例,不对其原理进行描述(俺也不会,哈哈)。
以下使用有错误、疑问或者更好的写法欢迎评论区讨论学习。
同时也欢迎各位大佬评论区留下本文中未提及的好用的api方法讨论学习。
创建业务逻辑测试实体类。
加入测试数据,以代码示例都以此测试数据为依据。
public static List<User> getUserList() {
List<User> list = new ArrayList<>();
list.add(User.builder().id(1L).userName("剑帝").age(18).height(BigDecimal.valueOf(170.7)).createTime(DateUtil.parseDateTime("2023-05-21 17:17:17")).build());
list.add(User.builder().id(2L).userName("街霸").age(20).height(BigDecimal.valueOf(165.6)).createTime(DateUtil.parseDateTime("2023-05-20 18:17:17")).build());
list.add(User.builder().id(3L).userName("红眼").age(25).height(BigDecimal.valueOf(185.7)).createTime(DateUtil.parseDateTime("2023-06-21 18:17:17")).build());
list.add(User.builder().id(4L).userName("瞎子").age(27).height(BigDecimal.valueOf(180.5)).createTime(DateUtil.parseDateTime("2023-05-30 17:17:17")).build());
list.add(User.builder().id(5L).userName("枪炮师").age(23).height(BigDecimal.valueOf(175.7)).createTime(DateUtil.parseDateTime("2022-05-21 17:17:17")).build());
return list;
}
1、去重方法
需要注意的是 distinct() 在不修改对象重写的equals和hashCode方法情况下,对象中的每个属性值必须完全相等才会被去重。
如果需要对某个属性进行去重的话可以重写equals和hashCode方法,或者使用如下写法。(这也是网上给出的普遍方法,我个人认为这个方法有一点瑕疵,就是新的数据和旧数据顺序不一致了,欢迎评论区留下更好的方法!)
【示例】在测试数据基础上新增去重的测试数据
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
list.add(User.builder().id(7L).userName("枪炮师").age(23).height(BigDecimal.valueOf(175.7)).createTime(DateUtil.parseDateTime("2022-05-21 17:17:17")).build());
// List<User> userList = list.stream().distinct().collect(Collectors.toList());
List<User> userList = list.stream().collect(Collectors.collectingAndThen(Collectors.toCollection(() ->
new TreeSet<>(Comparator.comparing(User::getUserName))), ArrayList::new));
userList.forEach(System.err::println);
}
运行结果:
2、内存分页方法
在一些复杂的业务场景下,数据库分页往往达不到需求,这时候就需要手动分页,此api还是很好的,比之前手动计算好太多了。
【示例】limit(long n) 方法用于返回前n条数据,skip(long n) 方法用于跳过前n条数据。
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// current为页码(当前页),pageSize为每页显示条数,此示例是展示第二页的数据
long current = 2; // 根据实际情况代入参数,此处仅为展示效果
long pageSize = 3; // 根据实际情况代入参数,此处仅为展示效果
List<User> userList = list.stream().skip((current - 1) * pageSize).limit(pageSize).collect(Collectors.toList());
userList.forEach(System.err::println);
}
执行结果:
3、过滤方法
【示例】过滤出来年龄大于20并且身高大于180的数据。(其他字段按照需求写对应的判断即可)
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
List<User> userList = list.stream().filter(e -> e.getAge() > 20 && e.getHeight().compareTo(new BigDecimal("180")) > 0).collect(Collectors.toList());
userList.forEach(System.err::println);
}
执行结果:
4、数据处理方法
使用场景很灵活,下面仅列举了一些我使用过的场景。
4.1 map()
常用于把对象中的某个元素转换为一个新的集合。
【示例】把对象中的主键id转换为一个新的集合
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
List<Long> idList = list.stream().map(User::getId).collect(Collectors.toList());
idList.forEach(System.err::println);
}
执行结果:
4.2 flatMap
将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流。
常用于中间数据处理。
【示例】收集对象中的属性值成为一个新的集合
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
Set<String> resultSet = list.stream().flatMap(e -> {
HashSet<String> set = new HashSet<>();
set.add(e.getId().toString());
set.add(e.getAge().toString());
return set.stream();
}).collect(Collectors.toSet());
resultSet.forEach(System.err::println);
}
执行结果:
【示例】将map中的value处理为一个新的集合
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 造测试map,仅为测试使用,无特殊意义。
Map<String, List<User>> map = new HashMap<>();
map.put(list.get(0).getUserName(), list.subList(0, 2));
map.put(list.get(1).getUserName(), list.subList(2, 4));
map.forEach((k, v) -> System.err.println(k + "--------" + v));
System.err.println("-----------------------------------");
List<Integer> ageList = map.values().stream().flatMap(Collection::stream).map(User::getAge).collect(Collectors.toList());
ageList.forEach(System.err::println);
}
执行结果:
5、判断方法
【示例】
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 判断集合中是否存在年龄大于18的元素
boolean result1 = list.stream().anyMatch(e -> e.getAge() > 18);
// 判断集合中否存在用户名称是都包含 "剑" 的元素
boolean result2 = list.stream().allMatch(e -> e.getUserName().contains("剑"));
// 判断集合中是否不存在身高大于185的元素
boolean result3 = list.stream().noneMatch(e -> e.getHeight().compareTo(new BigDecimal("185")) > 0);
System.err.println(result1);
System.err.println(result2);
System.err.println(result3);
}
执行结果:
6、统计方法
6.1 reduce()
【示例】使用 reduce() 求用户列表中年龄的最大值、最小值、总和。
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
Integer maxAge = list.stream().map(User::getAge).reduce(Integer::max).get();
Integer minAge = list.stream().map(User::getAge).reduce(Integer::min).get();
Integer sumAge = list.stream().map(User::getAge).reduce(Integer::sum).get();
System.err.println("最大年龄:" + maxAge);
System.err.println("最小年龄:" + minAge);
System.err.println("年龄总和:" + sumAge);
}
执行结果:
6.2 mapToInt(T -> int) 、mapToDouble(T -> double) 、mapToLong(T -> long)
int sumVal = userList.stream().map(User::getAge).reduce(0,Integer::sum);计算元素总和的方法其中暗含了装箱成本,map(User::getAge) 方法过后流变成了 Stream 类型,而每个 Integer 都要拆箱成一个原始类型再进行 sum 方法求和,这样大大影响了效率。针对这个问题 Java 8 有良心地引入了数值流 IntStream, DoubleStream, LongStream,这种流中的元素都是原始数据类型,分别是 int,double,long。
流转换为数值流:
- mapToInt(T -> int) : return IntStream
- mapToDouble(T -> double) : return DoubleStream
- mapToLong(T -> long) : return LongStream
【示例】使用 mapToInt() 求用户列表中年龄的最大值、最小值、总和、平均值。
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
int maxAge = list.stream().mapToInt(User::getAge).max().getAsInt();
int minAge = list.stream().mapToInt(User::getAge).min().getAsInt();
int sumAge = list.stream().mapToInt(User::getAge).sum();
double aveAge = list.stream().mapToInt(User::getAge).average().getAsDouble();
System.err.println("最大年龄:" + maxAge);
System.err.println("最小年龄:" + minAge);
System.err.println("年龄总和:" + sumAge);
System.err.println("平均年龄:" + aveAge);
}
执行结果:
3.3 counting() 和 count()
【示例】
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 统计用户名中包含 "剑" 的人数,使用 counting() 方法进行统计
Long nameCount = list.stream().filter(user -> user.getUserName().contains("剑")).collect(Collectors.counting());
// 统计25岁以上的人数,使用 count() 方法进行统计(推荐)
Long ageCount = list.stream().filter(user -> user.getAge() >= 25).count();
System.err.println("用户名中包含 '剑' 的人数:" + nameCount + "人");
System.err.println("25岁以上的人数:" + ageCount + "人");
}
执行结果:
3.4 summingInt()、summingLong()、summingDouble()
计算总和。
【示例】计算年龄总和
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 计算年龄总和(不推荐使用,idea都提示了)
int sumAge = list.stream().collect(Collectors.summingInt(User::getAge));
System.err.println("年龄总和:" + sumAge);
}
执行结果:
3.5 averagingInt()、averagingLong()、averagingDouble()
用于计算平均值。使用方法同上,不再赘述。
3.6 summarizingInt()、summarizingLong()、summarizingDouble()
这三个方法比较特殊,比如 summarizingInt 会返回 IntSummaryStatistics 类型。
IntSummaryStatistics类提供了用于计算的平均值、总数、最大值、最小值、总和等方法,方法如下图:
【示例】使用 IntSummaryStatistics 统计:最大值、最小值、总和、平均值、总数。
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 获取IntSummaryStatistics对象
IntSummaryStatistics statistics = list.stream().collect(Collectors.summarizingInt(User::getAge));
// 统计:最大值、最小值、总和、平均值、总数
System.err.println("最大年龄:" + statistics.getMax());
System.err.println("最小年龄:" + statistics.getMin());
System.err.println("年龄总和:" + statistics.getSum());
System.err.println("平均年龄:" + statistics.getAverage());
System.err.println("用户总数:" + statistics.getCount());
}
执行结果:
3.7 BigDecimal 类型的统计
【示例】用户身高信息统计
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 最高身高
BigDecimal maxHeight = list.stream().map(User::getHeight).reduce(BigDecimal::max).orElse(BigDecimal.ZERO);
// 最低身高
BigDecimal minHeight = list.stream().map(User::getHeight).reduce(BigDecimal::min).orElse(BigDecimal.ZERO);
// 身高总和
BigDecimal sumHeight = list.stream().map(User::getHeight).reduce(BigDecimal.ZERO, BigDecimal::add);
// 平均身高
BigDecimal avgHeight = list.stream().map(User::getHeight).reduce(BigDecimal.ZERO, BigDecimal::add).divide(new BigDecimal(list.size()));
// 打印统计结果
System.err.println("最高身高:" + maxHeight + "cm");
System.err.println("最低身高:" + minHeight + "cm");
System.err.println("身高总和:" + sumHeight + "cm");
System.err.println("平均身高:" + avgHeight + "cm");
}
执行结果:
7、排序方法
sorted() / sorted((T, T) -> int)
如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Stream。反之, 需要调用 sorted((T, T) -> int) 实现 Comparator 接口。
4.1 普通排序
- comparingInt
- comparingLong
- comparingDouble
【示例】使用comparingInt()对年龄进行排序
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据年龄排序(正序)
// List<User> resultList = list.stream().sorted(Comparator.comparingInt(User::getAge)).collect(Collectors.toList());
// 根据年龄排序(倒序)
List<User> resultList = list.stream().sorted(Comparator.comparingInt(User::getAge).reversed()).collect(Collectors.toList());
resultList.forEach(System.err::println);
}
执行结果:
4.2 对时间进行排序
【示例】
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据创建时间排序(正序)
// List<User> resultList = list.stream().sorted(Comparator.comparing(User::getCreateTime)).collect(Collectors.toList());
// 根据创建时间排序(倒序)
List<User> resultList = list.stream().sorted(Comparator.comparing(User::getCreateTime).reversed()).collect(Collectors.toList());
resultList.forEach(System.err::println);
}
执行结果:
4.3 对中文进行排序
此处用了 hutool拼音工具 获取中文拼音,具体使用方法可点击查看官网介绍。
【示例】
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据用户名称排序(正序)
// List<User> resultList = list.stream().sorted(Comparator.comparing(User::getUserName, (x, y) -> {
// x = PinyinUtil.getPinyin(x);
// y = PinyinUtil.getPinyin(y);
// Collator instance = Collator.getInstance();
// return instance.compare(x, y);
// })).collect(Collectors.toList());
// 根据用户名称排序(倒序)
List<User> resultList = list.stream().sorted(Comparator.comparing(User::getUserName, (x, y) -> {
x = PinyinUtil.getPinyin(x);
y = PinyinUtil.getPinyin(y);
Collator instance = Collator.getInstance();
return instance.compare(x, y);
}).reversed()).collect(Collectors.toList());
resultList.forEach(System.err::println);
}
执行结果:
8、分组方法
分组这里的测试数据不太好,重新造测试数据如下:
5.1 普通分组
【示例】根据年龄分组
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据年龄分组
Map<Integer, List<User>> map = list.stream().collect(Collectors.groupingBy(User::getAge));
map.forEach((key, value) -> {
System.err.println("年龄为:"+ key + " 的数据集合:");
value.forEach(System.err::println);
System.err.println("--------------------------------------------------------------------------");
});
}
执行结果;
5.2 多级分组
【示例】先根据姓名分组,然后根据年龄分组
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据姓名分组,然后根据年龄分组
Map<String, Map<Integer, List<User>>> map = list.stream().collect(Collectors.groupingBy(User::getUserName,
Collectors.groupingBy(User::getAge)));
map.forEach((key1, heightMap) -> {
System.err.println(key1 + ":");
heightMap.forEach((key2,user)-> {
System.err.println(key2 + ":");
user.forEach(System.err::println);
});
System.err.println("--------------------------------------------------------------------------");
});
}
执行结果:
5.3 分组后处理数据
下面的示例有些是我在写一些报表需求时候写的,暂时不理解没关系,遇到相应的需求时就会理解了。
【示例】根据名称分组,然后计算平均年龄
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据名称分组,然后计算平均年龄
Map<String, Double> map = list.stream().collect(Collectors.groupingBy(User::getUserName,
Collectors.averagingInt(User::getAge)));
map.forEach((key, value) -> {
System.err.println(key + "的平均年龄:" + value);
});
}
执行结果:
【示例】根据姓名分组,然后去年龄最大的
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 根据姓名分组,然后去年龄最大的
Map<String, Integer> map = list.stream().collect(Collectors.groupingBy(User::getUserName,
Collectors.collectingAndThen(Collectors.toList(),
l -> l.stream().map(User::getAge).reduce(Integer::max).get())));
map.forEach((k, v) -> {
System.err.println("key: " + k + "-----" + "value: 最大年龄为:" + v);
});
}
执行结果:
【示例】先根据姓名分组,然后根据年龄分组并取第一条的身高
public static void main(String[] args) {
// 获取测试数据
List<User> list = getUserList();
// 先根据姓名分组,然后根据年龄分组并取第一条的身高
Map<String, Map<Integer, BigDecimal>> map = list.stream().collect(
Collectors.groupingBy(User::getUserName,
Collectors.groupingBy(User::getAge,
Collectors.mapping(User::getHeight,
Collectors.collectingAndThen(Collectors.toList(), l -> l.get(0))))));
map.forEach((key1, ageMap) -> {
System.err.println(key1 + ":");
ageMap.forEach((key2, height)-> {
System.err.println(key2 + ":");
System.err.println("身高:" + height);
});
System.err.println("--------------------------------------------------------------------------");
});
}
执行结果:
9、对map的简单操作
下列示例在处理字典数据的时候会用到。
【示例】转换map集合的数据类型
public static void main(String[] args) {
// 获取测试数据
Map<Object, Object> map = new HashMap<>();
map.put(1, 200.3);
map.put(2, 500);
Map<String, String> stringMap = map.entrySet().stream().collect(Collectors.toMap(k -> k.getKey().toString(), v -> v.getValue().toString()));
stringMap.forEach((k, v) -> {
System.err.println("key:" + k + "---------" + "value:" + v);
});
}
执行结果:
【示例】反转map的key、value
public static void main(String[] args) {
// 获取测试数据
Map<Object, Object> map = new HashMap<>();
map.put(1, "格斗家");
map.put(2, "鬼剑士");
Map<String, String> reverseMap = map.entrySet().stream().collect(Collectors.toMap(k -> k.getValue().toString(), v -> v.getKey().toString()));
reverseMap.forEach((k, v) -> {
System.err.println("key:" + k + "---------" + "value:" + v);
});
}
执行结果: