一、概述
stream流是jdk8的新增特性,为的就是方便对集合、数组的操作
二、Stream流的思想
流,其实可以理解成工厂的流水线,在流水线上,会对物品进行各种操作
java中流的思想与这个一样,我们会先对数据(集合、数组)开启流(相当与是将数据放在流水线上),然后调用过滤方法(对数据进行操作),最后调用终结方法(对剩余的数据进行输出操作)
使用步骤:
- 先得到一条Stream流(流水线),并把数据放上去
- 使用中间方法,对流水线上的数据进行操作
- 使用终结方法,对流水线上的数据进行操作
三、流的种类
流,有串行流和并行流两种
串行流:在串行流中,对于每个元素,依次执行完所有中间操作,然后再处理下一个元素。这意味着在一个元素上执行完所有中间操作之后,才会开始下一个元素的处理。
并行流:在并行流中,元素会被分割成多个子集,在不同的线程中并行处理。这意味着一个元素的中间操作可能与其他元素的中间操作交叉进行,因为它们在不同的线程中执行。
四、获取流
不同的数据结构获取流的方式有所不同
单列集合
单列集合,直接调用Collection中的默认方法.stream()
// 单列集合获取流
ArrayList<String> list = new ArrayList<>();
/*
// 获取到一条流水线,并把集合数据放到流水线上
Stream<String> stream = list.stream();
// 调用终结方法打印流水线上的数据
stream.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
*/
list.stream().forEach(s-> System.out.println(s));
如果想开启并行流,可以
// 调用.parallelStream()方法开启并行流
list.parallelStream().forEach(System.out::println);
双列集合
双列集合,map,无法直接获取流,但是可以对map中所有的键获取流(键流),所有的值获取流(值流),所有的键值对获取流。
其实将map分为键、值、键值对之后,就是单列集合,所以开启并行流的方式也和单列集合一样。
// 双列集合获取流
HashMap<String, Integer> map = new HashMap<>();
Set<String> keys = map.keySet(); //键
Collection<Integer> values = map.values(); //值
Set<Map.Entry<String, Integer>> entrySet = map.entrySet(); //键值对
// 分别对键、值、键值对 开启流,并调用终结方法输出
keys.stream().forEach(System.out::println);
values.stream().forEach(System.out::println);
entrySet.stream().forEach(System.out::println);
数组
数组获取流,需要通过Arrays工具类中所提供的stream()方法
// 数组获取流
int[] str = {1,2,3,4,5,6};
Arrays.stream(str).forEach(System.out::println);
默认情况下,Stream.of
返回的是串行流,除非你显式地调用 .parallel()
方法将其转换为并行流。
Arrays.stream(str).parallel().forEach(System.out::println);
零散数据
零散数据想要获取stream流,这些零散数据的类型需要是一样的
// 零散数据获取stream流
// 使用Stream接口提供的.of()方法
Stream.of(1,2,3,4,5,6).forEach(System.out::println);
Stream.of("1","2","3","4","5","6").forEach(System.out::println);
默认情况下,Stream.of
返回的也是串行流,除非你显式地调用 .parallel()
方法将其转换为并行流。
Stream.of(str).parallel().forEach(System.out::println);
小细节:of()
方法的形参是一个可变类型的参数,可以传递一堆零散的数据,也可以传递数组。但是数组必须是引用类型的,如果传递基本数据类型,会把整个数组当作一个元素放到stream中
五、中间方法
- 中间操作是指那些在流上执行的操作,但不会立即触发流中的元素进行处理。相反,它们会返回一个新的流对象,这个新的流对象记录了在中间操作中所定义的处理步骤。这些中间操作是延迟执行的,也就是说,只有当终端操作被调用时,中间操作才会真正被触发执行。
- 这种延迟执行的机制有助于优化性能,因为它允许流操作在真正需要结果时才进行处理。例如,你可以在一个流上执行多个中间操作,然后根据需要选择一个终端操作来生成结果。这样可以避免在不必要的情况下进行处理,从而提高效率。
- 可以连续调用多个中间方法,调用中间方法之后,返回新的Stream流,原来的Stream流只能使用一次,建议使用链式编程。
- 修改了Stream流中的数据,不会影响原来的集合或者数组中的数据
Stream流中常用的6种中间方法
名称 | 说明 |
---|---|
Stream filter(Predicate<? super T> predicate) | 过滤 |
Stream limit(long maxSize) | 获取前几个元素 |
Stream skip(long n) | 跳过前几个元素 |
Stream distinct() | 元素去重,依赖(hashCode和equals方法) |
Stream Stream concat(Stream a,Stream b) | 合并a和b两个流为一个流 |
Stream map(Function<T, R> mapper) | 转换流中的数据类型 |
-
filter
filter
的形参是一个函数式接口,返回值为true,表示留下当前数据,当前数据可以继续往下执行,返回值为false,表示舍弃当前元素
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张三丰","张三","隔壁老王","狗蛋");
list.stream().filter(new Predicate<String>() {
@Override
public boolean test(String s) {
//返回true,表示当前数据留下
// 返回false,表示当前数据舍弃不要
return s.startsWith("张"); //"张无忌","张三丰","张三",可以继续往下执行Foreach
}
}).forEach(System.out::println);
// Lambda表达式简便写法
list.stream().filter(s->false).forEach(System.out::println);
-
limit
获取集合中前几个元素
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张三丰","张三","隔壁老王","狗蛋");
// 选择list中的前三个元素
list.stream().limit(3).forEach(System.out::println); //"张无忌","张三丰","张三"
-
skip
跳过集合中前几个元素
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张三丰","张三","隔壁老王","狗蛋");
// 跳过list中的前三个元素
list.stream().skip(3).forEach(System.out::println); //"隔壁老王","狗蛋"
-
distinct
去掉集合中的重复元素
//distinct底层实现源码
if (seenNull.get()) {
// TODO Implement a more efficient set-union view, rather than copying
keys = new HashSet<>(keys);
keys.add(null);
}
return Nodes.node(keys);
我们可以看到list.stream().distinct()
的底层实现使用了 Set
来进行去重操作。具体来说,它使用了一个 HashSet
或类似的数据结构来存储已经出现过的元素,从而确保流中的元素保持唯一。
Set
是一种不允许重复元素的集合数据结构,它通过使用元素的 hashCode()
和 equals()
方法来确定元素的唯一性。当你调用 distinct()
操作时,流会通过这些方法来判断流中的元素是否已经在 Set
中存在,如果存在则会被跳过,从而实现去重。
尽管底层使用了 Set
数据结构,但仍然需要注意,元素的 hashCode()
和 equals()
方法必须正确地实现,以便确保去重操作的准确性。如果这些方法没有正确地被重写,即使使用了 Set
,仍然可能出现错误的去重结果。
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张无忌","张无忌","张无忌","张三丰","张三","隔壁老王","狗蛋");
// 去掉list中重复的元素
// 注意:string底层是重写了hashCode()和equals()方法的,如果我们需要去重的是我们自己写的类,则需要重写这些类中的hashCode()和equals()方法
list.stream().distinct().forEach(System.out::println);
-
concat
合并流
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张三丰");
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1,"张三","隔壁老王","狗蛋");
Stream<String> stream1 = list.stream();
Stream<String> stream2 = list1.stream();
Stream.concat(stream1,stream2).forEach(System.out::println); // 张无忌张三丰张三隔壁老王狗蛋
-
map
映射,将stream流中的元素映射成新的流
new Function<String, Integer>(){}
第一个泛型表示流中原本的数据类型,第二个泛型表示转换后的流的数据类型
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌","张三丰");
list.stream().map(new Function<String, Integer>() { // <String, Integer>第一个参数是形参类型,第二个参数是返回值类型
@Override
public Integer apply(String s) {
return 6;
}
}).forEach(System.out::print); // 66
// Lambda表达式简便写法
list.stream().map(s->6).forEach(System.out::println);
六、终结方法
终端操作是流操作的最后一步,它们会触发实际的数据处理,并产生最终的结果。
以下是一些常见的流终端操作:
名称 | 说明 |
---|---|
void forEach(Consumer action) | 遍历 |
long count() | 统计 |
toArray() | 收集流中的数据,放到数组中 |
collect(Collector collector) | 收集流中的数据,放到集合中 |
-
forEach()
遍历流中的元素
//forEach()的参数是Consumer,消费型接口,表示只有输入值,没有输出值
//Consumer的泛型:表示流中数据的类型
//accept方法的形参s:依次表示流里的每一个数据
//方法体:对每个数据的处理操作(打印)
list.stream().forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//Lambda表达式的简便写法
list.stream().forEach(System.out::println);
-
count()
统计流中数据的个数
long count = list.stream().count();
System.out.println(count); // 2
-
toArray()
收集流中的数据,放到数组中。
toArray()有两种用法
// toArray()的空参用法
//说明:将list中的数据放到数组中,并返回这个数组
Object[] array = list.stream().toArray();
System.out.println(array.toString());
//IntFunction的泛型:具体类型的数组
//apply的形参:流中数据的个数,需要和数组的长度保持一致
//apply的返回值:具体类型的数组
//方法体:创建数组
// toArray的参数的作用:创建一个指定类型的数组
// 底层实现:依次得到流中的每一个数据,并把数据放到数组中
// 返回值:一个装着流里面所有数据的数组
String[] arr = list.stream().toArray(new IntFunction<String[]>() {
@Override
public String[] apply(int value) {
return new String[value];
}
});
// Lambda表达式简便写法
String[] arr1 = list.stream().toArray((value) -> new String[value]);
- collect()
收集到LIST集合中
List<String> list1 = list.stream().collect(Collectors.toList());
收集到SET集合中,由于set的特性,收集的集合里的元素是去重的
Set<String> set = list.stream().collect(Collectors.toSet());
收集到MAP集合中
Map<String, String> map = list.stream().collect(Collectors
/**
* toMap(参数1,参数2)
* 参数一:表示键的生成规则
* 参数二:表示值的生成规则
*
* 参数一: Function泛型一:表示流中每一个数据的类型
* 泛型二:表示MAP中键的数据类型
* apply()形参:依次表示流中的每一个数据
* 方法体:生成键的代码
* 返回值:生成的键
* 参数二和参数一 一样
*/
.toMap(new Function<String, String>() {
@Override
public String apply(String s) {
return s;
}
},
new Function<String, String>() {
@Override
public String apply(String s) {
return s;
}
}));
// Lambda表达式简便写法
Map<String, String> map = list.stream().collect(Collectors.toMap(
s -> s,
s -> s
));
七、注意点
- 串行流的处理方式是,依次对每个元素执行中间操作,并在所有元素都完成中间操作后,再执行终端操作。这意味着中间操作会逐个应用于每个元素,然后等所有元素都完成中间操作后,终端操作才会被触发。这种方式确保了处理顺序的一致性。
// 在这个例子中,map操作会被逐个应用于每个元素,然后reduce操作会在所有元素都完成map操作后被触发。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
int sum = numbers.stream()
.map(n -> n * 2) // 中间操作:将每个元素加倍
.reduce(0, Integer::sum); // 终端操作:计算总和
System.out.println(sum); // 输出结果:30