在之前的开发中,遇到了这样的需求:记录某个更新操作之前的数据作为日志内容,之后可以供管理员在页面上查看该日志。
思路:
经过短暂对比后,我选择方案2,理由如下:
要点:
具体情况可以分为:1、是否需要得到一个新的流?2、是否只需要一个简单 boolean 结果?
我开发需求是要得到具体哪些数据不一样,所以选择返回一个新的流,只是得到一个 boolean 来判断是否相同是不够的。
如果是得到一个新的流,那么推荐使用.filter() + .collect()
@Test
public void testFilter(){
//第一个数组
List<ListData> list1 = new ArrayList<>();
list1.add(new ListData("测测名字11",11,"email@11"));
list1.add(new ListData("测测名字22",22,"email@22"));
list1.add(new ListData("测测名字33",33,"email@33"));
log.info("第一个数组为:{}", list1);
//第二个数组
List<ListData> list2 = new ArrayList<>();
list2.add(new ListData("测测名字111",111,"email@11"));
list2.add(new ListData("测测名字22",22,"email@22"));
list2.add(new ListData("测测名字33",33,"email@33"));
log.info("第二个数组为:{}", list2);
//返回一个新的结果数组
List<ListData> resultList = list1.stream()
//最外层的filter里是条件,这个条件需要返回一个boolean:符合条件返回true,不符合条件返回false
.filter(p1 -> list2.stream()
//这个filter也是条件:判断两个数组里名字和年龄是否都相等,符合条件返回true,不符合条件返回false
.filter(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge()))
//如有内容则返回流中的第一条记录,其它情况都返回空
.findFirst().orElse(null)
//这个是最外层的filter的断言
== null)
//将上一步流处理的的结果,收集成一个新的集合
.collect(Collectors.toList());
log.info("经过 Stream 流处理后输出的结果数组为: {}", resultList);
}
结合.filter() + noneMatch() 其实也与上面的语句效果相同:
List<ListData> resultList = list1.stream()
.filter(p1 -> list2.stream()
//这个 noneMatch 也是条件:判断两个数组里名字和年龄是否都相等,符合条件返回true,不符合条件返回false
.noneMatch(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge())))
.collect(Collectors.toList());
log.info("经过 Stream 流处理后输出的结果数组为: {}", resultList);
结合 filter() + contains() 方法( 其中 contains() 方法的使用详见 1.2 小节的注意事项),与以上的效果也一样:
List<ListData> resultList = list1.stream().filter(p1 -> !list2.contains(p1)).collect(Collectors.toList());
log.info("经过 Stream 流处理后输出的结果数组为: {}", resultList);
下面是以上代码的运行结果如图 1 所示:
如果只需要一个简单的 boolean 结果,那么推荐使用.anyMatch() 或者 allMatch()
//返回一个boolean结果
boolean flag = list1.stream()
//只要流中任意一个元素符合条件则返回true,否则返回false
.anyMatch(p1 -> list2.stream()
//如果流中全部元素都符合条件,就返回true,否则返回false;当流为空时总是返回true
.allMatch(p2 -> p2.getName().equals(p1.getName()) && p2.getAge().equals(p1.getAge())));
log.info("经过 Stream 流对比是否相等: {}", flag);
下面是以上代码的运行结果如图 2 所示:
除了 Stream 流之外,还可以使用 JDK 自带的.contains() 相关方法来判断
//List 集合接口自带的方法
boolean isEqual = list1.containsAll(list2) && list2.containsAll(list1);
//与上述方法效果一致
boolean isEqual = list1.stream().anyMatch(p1 -> list2.contains(p1));
//下面的是上述语句的 lambda 表达式写法
//boolean isEqual = list1.stream().anyMatch(list2::contains);
注意事项:.contains() 相关方法底层是迭代器 Iterator 以及 .equals() 方法,需要为 List 集合包含的泛型
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ListData {
private String name;
private Integer age;
private String email;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ListData listData = (ListData) o;
return Objects.equals(name, listData.name) && Objects.equals(age, listData.age) && Objects.equals(email, listData.email);
}
}
下面是以上代码的运行结果如图 3 所示:
理论上可以用 for 循环或者迭代器来做,效果与使用 .containsAll() 方法差不多,但是自己手写的话可能会比较复杂,数据量稍大些的话效率较低,一般不考虑采用,这里我就不演示了。
上述的集合都是泛型为自定义引用类型的集合,下面分享一些简单集合,如整形、字符串类型集合的 Stream 流对比操作。
List<Integer> list1 = Arrays.asList(1, 6,);
List<Integer> list2 = Arrays.asList(3, 2, 1);
//Java 本身提供的 Integer 类已经实现了 Comparable 接口,可以直接.sort() 比较
boolean isEqual = list1.stream().sorted().collect(Collectors.toList())
.equals(list2.stream().sorted().collect(Collectors.toList()));
log.info("是否相等:{}", isEqual);
// 先排序然后转成 String 逗号分隔,joining()拼接
List<String> list3 = Arrays.asList("语文","数学","英语");
List<String> list4 = Arrays.asList("数学","英语","语文");
//Java 本身提供的 String 类也已经实现了 Comparable 接口
boolean flag = list3.stream().sorted().collect(Collectors.toList())
.equals(list4.stream().sorted().collect(Collectors.toList()));
log.info("是否相等:{}", flag);
下面是简单集合比较的运行结果,如图 4 所示:
不知道大家有没有发现,上述简单类型的类可以直接比较,而自己写的类就不能,会报”cannot be cast to java.lang.Comparable“。
举个例子,对于自定义的引用类型 ListData , Java 不知道应该怎样为 ListData 的对象排序,是应该按名字排序? 还是按年龄来排序?
注意:.sort() 方法底层实现需要依赖 Comparator 接口,那么这个引用类型 ListData 类要自己手动去实现 Comparator() 接口并重写 compare() 方法才能这样做比较。
List<ListData> list1 = new ArrayList<>();
list1.add(new ListData("泛型为引用类型", 666, "abc"));
List<ListData> list2 = new ArrayList<>();
list2.add(new ListData("泛型为引用类型", 888, "def"));
//这里想要收集成为集合进行比较,需要先根据特定的元素排序(年龄),然后再按顺序比较
boolean flag = list1.stream().sorted(Comparator.comparing(ListData::getAge)).collect(Collectors.toList())
.equals(list2.stream().sorted(Comparator.comparing(ListData::getAge)).collect(Collectors.toList()));
log.info("是否相等: {}", flag);
Stream API 是 Java 8 中最为重要的更新之一,是处理集合的关键抽象概念,也是每个 Java 后端开发人员都必须无条件掌握的内容。
Stream 和 Collection 集合的主要区别:Collection 是内存数据结构,重在数据的存储;而 Stream 是集合的操作计算,重在一系列的流式操作。
一个数据源(如:集合、数组)来获取一个流,具体有 3 种方式来创建:
通过集合直接创建(最常用)
//Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
//返回一个顺序流
default Stream<E> stream(){}
//返回一个并行流
default Stream<E> parallelStream{}
Arrays 也可以获取数组流
//返回一个流
public static <T> Stream<T> stream(T[] array){}
调用 Stream 类静态方法 of() 来创建流
public static<T> Stream<T> of(T... values){}
每次处理都会返回一个持有结果的新 Stream,即中间操作的方法返回值仍然是 Stream 类型的对象。因此中间操作可以是链式的,可对数据源的数据进行 n 次处理,但是在终止操作前,并不会真正执行;
中间操作可谓是最重要也最常使用的操作,具体分为3种:筛选与切片、映射、排序,如以下表格所示:
筛选与切片
方法 | 描 述 |
---|---|
Stream |
筛选,接收 Predicate 的条件,从流中排除某些元素,返回一个符合该条件的流 |
Stream |
截断,使其元素的数量不超过给定数量 |
Stream |
跳过,返回一个扔掉了前 n 个元素的流,若流中元素不足 n 个则返回一个空流,可与 limit() 形成互补 |
Stream |
去重,利用流所生成元素的 hashCode() 和 equals() 去除流中的重复元素 |
映射
这里只介绍常见的映射方法,flatMap() 的系列方法并不常用。
方法 | 描述 |
---|---|
接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。 | |
LongStream mapToLong(ToLongFunction<? super T> mapper); | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 Long 类型的Stream 流。 |
排序
方法 | 描述 |
---|---|
Stream |
产生一个新流,其中按自然顺序(如Integer)排序 |
Stream |
产生一个新流,其中按比较器指定的顺序排序 |
终止操作的方法返回值类型不再是 Stream,而可以是任何不为流的值,如List
方法 | 描述 |
---|---|
boolean anyMatch(Predicate<? super T> predicate); | 检查是否所有元素都符合条件,符合就返回 true,不符合则返回 false |
boolean allMatch(Predicate<? super T> predicate); | 检查是否至少有一个元素符合条件,有则返回 true,无则返回false |
boolean noneMatch(Predicate<? super T> predicate); | 检查是否所有元素都不匹配条件,都不符合则返回 true,其它情况返回false |
Optional |
返回流中第一个元素并放置到 Optional 容器中 |
Optional |
返回流中任意一个元素并放置到 Optional 容器中 |
long count(); | 返回流中元素的总个数 |
Optional |
经比较器按顺序比较后,返回流中最大值 |
Optional |
经比较器按顺序比较后,返回流中最小值 |
void forEach(Consumer<? super T> action); | 内部迭代,如果要对集合迭代可以直接使用.foreach(),不必经过 Stream |
<R, A> R collect(Collector<? super T, A, R> collector); | 将流转换为其他形式,如:将 Stream 中元素收集成.toList()、.toSet() 等 |
这里有个特殊的方法,.groupingBy() 不属于 Stream 而是属于 Collectors:
方法 | 返回类型 | 描述 |
---|---|---|
.stream().collect(Collectors.groupingBy()); | public static <T, K> Collector<T, ?, Map<K, List |
根据流中的某属性值对流进行分组,属性为 K,结果为指定的泛型,如 List |
文章到这里就结束了,关于 Stream 流 API 是日常开发中经常会遇到的,熟练运用可以提高我们的开发效率,让我们写出简洁易懂的代码,我们作为后端开发必须重视起来。总有人说它的调试 debug 是个缺点,不妨试试”Trace Current Stream Chain“按钮,可以追踪当前流中的链式变化。
那么今天的分享到这里就结束了,如有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区交流!
参考文档:
笔者在最近的项目开发中,遇到了两个父子关系紧密相关的场景:评论树结构、部门树结构。具体的需求如:找出某条评论下的所有子评论id集合,找出某个部门下所有的子部门id集合。
这一篇文章拖了有点久,虽然在项目中使用分布式锁的频率比较高,但整理成文章发布出来还是花了一点时间。在一些移动端、用户量大的互联网项目中,经常会使用到 Redis 分布式锁作为控制访问高并发的工具。