stream流
Stream 是 Java 8 引入的函数式编程工具,用于高效处理集合数据。它类似于“数据管道”,将数据从源头(如集合、数组)经过一系列操作(过滤、转换等)后输出结果。stream通常与Lambda表达式相结合,用于简化集合与数组的操作。我们可以将其理解为一条流水线,将数据放到stream流后,我们可以对数据进行各种操作。其中中间方法调用完毕后,还可以调用其他方法,但终结方法一旦调用则不能再调用其
介绍
Stream 是 Java 8 引入的函数式编程工具,用于高效处理集合数据。它类似于“数据管道”,将数据从源头(如集合、数组)经过一系列操作(过滤、转换等)后输出结果。stream通常与Lambda表达式相结合,用于简化集合与数组的操作。
我们可以将其理解为一条流水线,将数据放到stream流后,我们可以对数据进行各种操作。
使用Stream流分为三步:
- 从集合、数组或其他数据源生成一个 Stream 对象。
- 使用中间方法对stream流中的数据进行操作。
- 使用终结方法对stream流中的数据进行操作。
其中中间方法调用完毕后,还可以调用其他方法,但终结方法一旦调用则不能再调用其他方法了。接下来我们依次来看这三个阶段:
获取stream流
在不同的情况下,获取stream流的方式也有所不同:
获取方式 | 使用方法 | 说明 |
---|---|---|
单列集合 | default Stream<E> stream() | Collection中的默认方法 |
双列集合 |
需通过keySet()或entrySet()转为单列集合后再使用 |
无法直接使用stream流 |
数组 | public static <T> Stream<T> stream(T[] array) | Arrays工具类中的静态方法 |
一堆零散数据 | public static <T> Stream<T> of(T… values) | Stream接口中的静态方法 |
单列集合
接下来我们依次来演示,首先是单列集合:
// 单列集合获取Stream流
// 创建一个ArrayList对象,用于存储Object类型的元素
ArrayList<Object> list = new ArrayList<>();
// 使用Collections.addAll()方法向集合中添加元素"a", "b", "c", "d", "e"
Collections.addAll(list, "a", "b", "c", "d", "e");
// 使用集合的stream()方法获取Stream流
Stream<Object> stream1 = list.stream();
// 使用forEach方法遍历Stream流中的元素,并打印每个元素
stream1.forEach(System.out::println);
//输出结果(为节省空间,换行全由空格替代,后文同理):
//a b c d e
但这样写还是过于复杂,我们一般直接使用链式编程进行输出:
ArrayList<Object> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c", "d", "e");
// 使用集合的stream()方法获取Stream流,并直接通过forEach方法遍历打印每个元素
list.stream().forEach(System.out::println);
双列集合
然后来看双列集合。因为双列集合是无法直接使用stream流的,我们只能通过调用keySet()或者entrySet()方法,单独对其键或值进行操作:
// 双列集合
// 创建一个HashMap对象,用于存储String类型的键和Integer类型的值
HashMap<String, Integer> hashMap = new HashMap<>();
// 向HashMap中添加键值对
hashMap.put("第一", 111);
hashMap.put("第二", 222);
hashMap.put("第三", 333);
hashMap.put("第四", 444);
hashMap.put("第五", 555);
// 使用keySet()方法获取所有键的集合,并转换为Stream流,然后遍历打印每个键
hashMap.keySet().stream().forEach(System.out::println);
// 输出结果:第四 第三 第五 第一 第二
// 使用values()方法获取所有值的集合,并转换为Stream流,然后遍历打印每个值
hashMap.values().stream().forEach(System.out::println);
// 输出结果:444 333 555 111 222
// 使用entrySet()方法获取所有键值对的集合,并转换为Stream流,然后遍历打印每个键值对
hashMap.entrySet().stream().forEach(System.out::println);
// 输出结果:第四=444 第三=333 第五=555 第一=111 第二=222
数组
然后是数组,直接调用Arrays工具类中的静态方法即可,无论是基本类型的数组还是引用类型的数组都适用:
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9}; // 定义一个包含1到9的整型数组
Arrays.stream(arr).forEach(System.out::println); // 将数组转换为Stream流,并逐个打印每个元素
零散数据
最后是零散的数据,无论是基础数据还是引用数据都可以,但要求这些数据必须是同一种数据:
// 使用Stream.of()方法创建包含1到5的整数Stream流,并逐个打印每个元素
Stream.of(1, 2, 3, 4, 5).forEach(System.out::println);
// 使用Stream.of()方法创建包含"一"到"五"的字符串Stream流,并逐个打印每个元素
Stream.of(“一”, “二”, “三”, “四”, “五”).forEach(System.out::println);
同时因为Stream.of()的形参是一个可变参数,而可变参数的底层就是数组,所以该方法可以传递一堆零散的数据,也可以传递数组。
但注意,传递的数组必须是引用数据类型,如果传递基本数据类型,系统会将整个数组当作一个元素放到Stream流中。
中间方法
常用的中间方法有以下六个:
名称 | 说明 |
---|---|
Stream<T> filter(Predicate<? super T> predicate) | 过滤 |
Stream<T> limit(long maxSize) | 获取前几个元素 |
Stream<T> skip(long n) | 跳过前几个元素 |
Stream<T> distinct() | 元素去重,依赖hashCode和equals方法 |
static <T> Stream<T> concat(Stream a, Stream b) | 合并a和b两个流为一个流 |
Stream<R> map(Function<T, R> mapper) | 转换流中的数据类型 |
注意:
- 元素去重时,如果流中是自定义对象,需先重写hashCode和equals方法
- 合并流时应保持两流中的数据类型相同,如果不同,大流上的数据类型则为其共同的父类
- 中间方法返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程
- 修改Stream流中的数据,不会影响原来集合或者数组中的数据
过滤filter
先来看过滤,我们需要过滤list中以"张"开头,且长度为三的字符串:
// 创建一个ArrayList集合并添加初始数据
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
// 使用Stream流进行两次过滤操作,先过滤以"张"开头的名字,再过滤长度为3的名字
// 使用匿名内部类实现Predicate接口
list.stream()
.filter(new Predicate<String>() {
@Override
public boolean test(String s) {
// 第一次过滤条件:名字以"张"开头
return s.startsWith("张");
}
})
.filter(new Predicate<String>() {
@Override
public boolean test(String s) {
// 第二次过滤条件:名字长度为3
return s.length() == 3;
}
})
.forEach(s -> System.out.println(s)); // 打印最终过滤结果
上述代码虽然实现了所需功能,但仍旧较复杂,接下来我们使用lambda进行链式编程简化:
// 创建一个ArrayList集合并添加初始数据
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰");
// 使用Lambda表达式简化过滤逻辑,效果与上方代码相同
list.stream()
.filter(name -> name.startsWith("张")) // 第一次过滤:名字以"张"开头
.filter(name -> name.length() == 3) // 第二次过滤:名字长度为3
.forEach(System.out::println); // 使用方法引用打印结果
结合上文注意中的"中间方法返回新的Stream流,原来的Stream流只能使用一次"。
本例子中我们直接使用链式编程进行了两次过滤,那是否可以改为先使用Stream类型的变量来接收上一个filter()方法过滤的结果,再进行操作呢?
Stream<String> stream1 = list.stream().filter(name -> name.startsWith("张"));
Stream<String> stream2 = stream1.filter(name -> name.length() == 3);
stream2.forEach(System.out::println);
测试后发现输出结果和上文相同,但如果我们想在下文再用一次stream1,系统就会报错:stream has already been operated upon or closed。正因为每个stream流都只能使用一次,所以我们就没有必要使用变量记录该流,而是直接使用链式编程进行操作。
获取limit/跳过skip
这两个都比较简单,传入对应的参数,即可获取X个/跳过X个参数,直接演示:
// 创建一个ArrayList集合并添加初始数据
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌", "周芷若", "赵敏", "张强", "张三丰","张翠山","王二麻子","谢广坤");
// 使用Stream流对集合进行操作
list.stream()
// 限制流中元素的最大数量为6个
.limit(6)
// 跳过流中的前3个元素
.skip(3)
// 打印剩余的元素
.forEach(System.out::println);
}
//输出:张强 张三丰 张翠山
去重distinct
直接调用该方法即可,无需传入任何参数,其会自动去除流中的重复元素:
// 使用Stream流对集合进行操作
list.stream()
// 去除流中重复的元素(基于equals方法)
.distinct()
// 打印去重后的元素
.forEach(System.out::println);
再次提醒:因为本例中使用的是String类型,所以可以直接去重。如果是自定义对象,则一定要重写hashCode和equals方法。
翻阅该方法的源码即可发现其基于hashset进行去重,所以必须重写上述两方法。
直接ctrl+B/左键会跳转到接口中的方法,该方法没有方法体。如果想跳过接口直接看方法体按ctrl+alt+B/左键该方法即可
合并concat
在合并时我们需尽可能的保持两流中的数据类型保持一致,如果不同,大流中的数据类型会变为两数据类型共同的父类。相当于一次类型提升,这样就无法使用子类中的特有功能了。
使用该方法只需将两流作为参数传入即可:
// 创建第一个ArrayList并添加初始数据
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1, “一”, “二”, “三”);
// 创建第二个ArrayList并添加初始数据
ArrayList<String> list2 = new ArrayList<>();
Collections.addAll(list2, “1”, “2”, “3”);
// 使用Stream.concat合并两个流
Stream.concat(list1.stream(), list2.stream())
// 打印合并后的流中的所有元素
.forEach(System.out::println);
//输出结果一 二 三 1 2 3
转换map
因为使用过程较复杂,所以该方法仍从匿名内部类开始编写代码,然后再变为lambda表达式:现有一集合:
Collections.addAll(list, "张无忌-11", "周芷若-22", "赵敏-33", "张强-44", "张三丰-55","张翠山-66");
要求只获取字符串中的数字,并转为int类型。
我们可以使用map()操作将字符串转换为int。可以使用匿名类实现Function<String, Integer>接口,其中有两个泛型,前者是流中原本的数据类型,第二个则是要转成的数据类型。因为泛型中不能写基本数据类型,所以我们使用Integer代替int。
然后使用apply()方法定义转换逻辑:用split("-")按"-"分割字符串得到数组(例如:"张三-25" → ["张三", "25"]),再取数组第二个元素arr[1](数字部分)用Integer.parseInt()将字符串转为整数。apply()的返回类型也需是Integer,与上文中的第二个泛型保持一致。
// 使用Stream流对集合进行操作
list.stream()
// 使用map操作将字符串转换为整数年龄
.map(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
// 按照分隔符"-"分割字符串
String[] arr = s.split("-");
// 获取年龄部分(数组的第二个元素)
String ageString = arr[1];
// 将字符串年龄转换为整数
int age = Integer.parseInt(ageString);
// 返回转换后的年龄
return age;
}
})
// 打印转换后的年龄
.forEach(System.out::println);
然后再变为lambda表达式的形式:
// 遍历list中的每个元素,使用流处理
list.stream()
// 将每个字符串元素转换为整数,具体操作是:
// 1. 按"-"分割字符串
// 2. 取分割后的第二个元素(索引为1)
// 3. 将该字符串转换为整数
.map(s -> Integer.parseInt(s.split("-")[1]))
// 将转换后的每个整数打印到控制台
.forEach(System.out::println);
终结方法
名称 | 说明 |
---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计 |
toArray() | 收集流中的数据,放到数组中 |
collect(Collector collector) | 收集流中的数据,放到集合中 |
遍历forEach()
首先是上文经常用到的forEach()方法,其形参是Consumer,是一个由@FunctionalInterface修饰的函数式接口。该接口的泛型就是流中的数据类型。
该接口中的方法accept(String s)其形参s依次表示流中的每一个数据,所以该形参的类型也需与上文保持一致。在该方法的方法体中我们可以编写对每一个数据的处理操作。
// 使用 stream() 将 List 转换为流,然后通过 forEach 方法遍历流中的每个元素
list.stream()
// 创建一个 Consumer 接口的匿名实现类,用于定义对每个元素的操作
.forEach(new Consumer<String>() {
@Override
// accept 方法接收流中的每个元素,这里实现为打印元素到控制台
public void accept(String s) {
System.out.println(s);
}
});
因为其是终结方法,所以其返回值为void,也就是说该方法结束后不能再调用其他方法了,其是流中的最后一步。
接下来将其改为lambda表达式的形式,其实就是上文我们使用的样式:
list.stream().forEach(System.out::println);
统计count()
该方法的返回类型是一个long型的整数,表示流中有多少个元素,所以在该方法后也不能再调用其他方法了:
// 使用 stream() 将 List 转换为流,然后调用 count() 方法统计元素数量
long count = list.stream().count();
System.out.println(count);
收集toArray()
该方法有两个重载版本,一个为无参,表示收集到Object类型的数组中:
// 使用 stream() 将 List 转换为流,然后调用 toArray() 方法将流转换为数组
Object[] array = list.stream().toArray();
// 使用 Arrays.toString() 将数组转换为字符串形式,并打印到控制台
System.out.println(Arrays.toString(array));
另一个版本的形参也是一个由@FunctionalInterface修饰的函数式接口IntFunction,其泛型表示要收集到什么类型的数组中,本文为String[]。
其中的方法apply()形参为流中的数据的个数,该个数要和流中的数据长度保持一致。该方法的返回类型就是具体类型的数组,同样需与上文保持一致。而方法体则负责创建数组:
// 使用 stream() 将 List 转换为流,然后通过 toArray() 方法将流转换为 String 数组
// 这里使用 IntFunction<String[]> 接口动态创建指定长度的 String 数组
String[] array = list.stream().toArray(new IntFunction<String[]>() {
@Override
// apply 方法接收流中元素的数量(value),并返回一个长度为 value 的 String 数组
public String[] apply(int value) {
return new String[value];
}
});
// 使用 Arrays.toString() 将数组转换为字符串形式并打印
System.out.println(Arrays.toString(array));
// 直接打印数组对象,会输出数组的内存地址
System.out.println(array);
toArray方法的作用就是负责创建一个指定类型的数组,其底层会依次得到流里面的每一个数据,并把数据放到数组当中。返回值则是一个装着流里面所有数据的数组。
接下来将其修改为lambda表达式的形式,首先来回忆lambda表达式的基础形式:()->{},其中()内为形参,若只有一个形参,则可省略()和数据类型,也就是说直接写value,表示流中元素的个数。{}同理,因为只有一句:创建数组new String[value],所以可以省略{}:
String[] array = list.stream().toArray(value -> new String[value]);
// 使用 Arrays.toString() 将数组转换为字符串形式,并打印到控制台
System.out.println(Arrays.toString(array));
这就是收集到数组中的方法,接下来来看收集到集合中的方法。
收集collect()
collect()方法可以收集到单列集合List、set以及双列集合Map中,我们依次来学习。
这里已经事先准备好了数据,要求收集所有性别为男的数据,和上文一样,我们同样可以使用s.split("-")[1]来与"男"进行比较来判断是否相等。
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "张无忌-男-15", "周芷若-女-14", "赵敏-女-13", "张强-男-20", "张三丰-男-100", "张翠山-男-40", "张良-男-35", "王二麻子-男-37", "谢广坤-男-41");
Stream<String> stream = list.stream()
.filter(s -> "男".equals(s.split("-")[1]));
在进行比较时,我们通常使用固定数据调用equals()方法与其他数据进行比较,因为固定数据不可能为null,而其他数据有可能为null,一旦为null则会导致空指针异常。
进行判断后需要调用collect()方法将其收集起来,在该方法中需要传递Collectors.toList(),其中Collectors为stream流中的工具类,toList()则为其中的静态方法,其可以创建一个ArrayList集合。最后新建一个List用于接收即可:
// 使用 stream() 将 List 转换为流,然后通过 filter 方法筛选符合条件的元素
List<String> newList = list.stream()
// 筛选条件:使用 split("-") 分割字符串,并检查第二部分是否为 "男"
.filter(s -> "男".equals(s.split("-")[1]))
// 将筛选后的流收集为新的 List
.collect(Collectors.toList());
// 打印筛选后的结果
System.out.println(newList);
收集到set集合中同理,只需将Collectors.toList()替换为Collectors.toSet(),并用Set集合接收即可:
Set<String> newList2 = list.stream()
.filter(s -> "男".equals(s.split("-")[1]))
.collect(Collectors.toSet());
两种收集方法极为相似,但收集到List中的元素会有重复,但收集到Set中的元素则不会重复。
接下来看如何收集到Map集合中,我们首先要确定谁作为键,谁作为值。因为题目中要求只收集男性,所以性别全部都为男不需要记录,所以我们将名字作为键,年龄作为值。
同理,collect()中的参数替换为Collectors.toMap(),而toMap()方法中也需要传入两参,两者都为@FunctionalInterface修饰的函数式接口Function(),参数一表示键的生成规则,参数二表示值的生成规则。
Function()有两个泛型,第一个泛型为流中数据的类型,第二个则为map键的数据类型。同理,第二个参也是Function(),第一个泛型为流中数据的类型,第二个则为map值的数据类型。
其中的方法apply()中的形参为流中的每一个数据,其方法体则用于生成键/值,返回值就是已经生成的键/值。
// 使用 stream() 将 List 转换为流,筛选性别为 “男” 的元素,并转换为 Map
Map<String, Integer> map = list.stream()
// 筛选条件:分割字符串并检查性别部分是否为 “男”
.filter(s -> "男".equals(s.split("-")[1]))
// 使用 Collectors.toMap 将流转换为 Map
.collect(Collectors.toMap(
// 第一个 Function 提取键(姓名部分)
new Function<String, String>() {
@Override
public String apply(String s) {
return s.split("-")[0]; // 返回姓名部分作为键
}
},
// 第二个 Function 提取值(年龄部分)
new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.split("-")[2]); // 返回年龄部分作为值
}
}
));
// 打印转换后的 Map
System.out.println(map);
接下来写成lambda表达式的形式:
// 使用 stream() 将 List 转换为流,筛选性别为 "男" 的元素,并转换为 Map
Map<String, Integer> map = list.stream()
// 筛选条件:分割字符串并检查性别部分是否为 "男"
.filter(s -> "男".equals(s.split("-")[1]))
// 使用 Collectors.toMap 将流转换为 Map
// 第一个 Function 提取键(姓名部分)
.collect(Collectors.toMap(
s -> s.split("-")[0], // 键:姓名(s.split("-")[0])
s -> Integer.parseInt(s.split("-")[2]) // 值:年龄(s.split("-")[2])
));
// 打印转换后的 Map
System.out.println(map);
练习
定义一个集合,并添加整数:1、2、3、4、5、6、7、8、9,要求过滤奇数,保留偶数并将结果保存起来。
// 创建一个ArrayList集合并添加初始数据
ArrayList<Integer> list = new ArrayList<>();
Collections.addAll(list, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// 使用 stream() 将 List 转换为流,通过 filter 方法筛选偶数
List<Integer> numList = list.stream()
// 筛选条件:保留能被 2 整除的数(偶数)
.filter(num -> num % 2 == 0)
// 将筛选后的流收集为新的 List
.collect(Collectors.toList());
// 打印筛选后的偶数列表
System.out.println(numList);
创建一个ArrayList集合,并添加以下字符串,字符串中前面是姓名,后面是年龄"zhangsan,23","lisi,24","wangwu,25"保留年龄大于等于24岁的人,并将结果收集到Map集合中,姓名为键,年龄为值。
// 创建一个ArrayList集合并添加初始数据
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list, "zhangsan,23", "lisi,24", "wangwu,25");
// 使用 stream() 将 List 转换为流,通过 filter 方法筛选年龄大于等于24岁的人
Map<String, Integer> map = list.stream()
// 筛选条件:保留年龄大于等于24岁的人
.filter(s -> Integer.parseInt(s.split(",")[1]) >= 24)
// 将筛选后的流收集为 Map 集合,姓名为键,年龄为值
.collect(Collectors.toMap(
s -> s.split(",")[0], // 提取姓名作为键
s -> Integer.parseInt(s.split(",")[1]) // 提取年龄作为值
));
// 打印筛选后的 Map 集合
System.out.println(map);
现在有两个ArrayList集合,第一个集合中:存储6名男演员的名字和年龄。第二个集合中:存储6名女演员的名字和年龄。姓名和年龄中间用逗号隔开:
"刘德华,62", "张学友,61", "郭富城,60", "黎明,59", "周星驰,63", "成龙,68"
"杨颖,38", "杨幂,38", "杨超越,24", "杨千嬅,53", "杨采妮,49", "刘亦菲,39"
要求完成如下的操作:
1,男演员只要名字为3个字的前两人
2,女演员只要姓杨的,并且不要第一个
3,把过滤后的两流合并到一起
4,将上一步的演员信息封装成Actor对象。
5,将所有的演员对象都保存到List集合中。
备注:演员类Actor,属性有:name,age
public class Main {
public static void main(String[] args) {
// 创建男演员和女演员的ArrayList集合
ArrayList<String> maleActors = new ArrayList<>();
ArrayList<String> femaleActors = new ArrayList<>();
// 使用Collections.addAll方法向男演员集合中添加初始数据
Collections.addAll(maleActors,
"刘德华,62", "张学友,61", "郭富城,60", "黎明,59", "周星驰,63", "成龙,68");
// 使用Collections.addAll方法向女演员集合中添加初始数据
Collections.addAll(femaleActors,
"杨颖,38", "杨幂,38", "杨超越,24", "杨千嬅,53", "杨采妮,49", "刘亦菲,39");
// 使用Stream API过滤男演员:筛选名字为3个字的前两人
Stream<String> stream1 = maleActors.stream()
.filter(s -> s.split(",")[0].length() == 3) // 筛选名字长度为3的演员
.limit(2); // 只取前两人
// 使用Stream API过滤女演员:筛选姓杨的演员并跳过第一个
Stream<String> stream2 = femaleActors.stream()
.filter(s -> s.startsWith("杨")) // 筛选姓杨的演员
.skip(1); // 跳过第一个符合条件的演员
// 合并两个流,并将结果转换为Actor对象列表
List<Actor> newList = Stream.concat(stream1, stream2) // 合并两个流
.map(s -> new Actor( // 将每个字符串转换为Actor对象
s.split(",")[0], // 提取名字
Integer.parseInt(s.split(",")[1]) // 提取年龄并转换为整数
))
.collect(Collectors.toList()); // 收集为List集合
// 打印最终结果
System.out.println(newList);
}
}
/**
* 演员类,包含姓名和年龄属性
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
class Actor {
private String name; // 演员姓名
private Integer age; // 演员年龄
}
更多推荐
所有评论(0)