Java 8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利。
文章目录
一、引言
1.1 传统集合的多步遍历代码
对于集合的操作,除了添加、删除、获取外,最常用的就是集合遍历。
- 需求:打印姓名为张且名字为3个字的人的名字
- 集合元素:张无忌、周芷若、赵敏、张强、张三丰
在 java 8 之前做法可能为
public class Demo01List {
public static void main(String[] args) {
//创建一个List集合,存储姓名
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
//对list集合中的元素进行过滤,只要以张开头的元素,存储到一个新的集合中
List<String> listA = new ArrayList<>();
for(String s : list){
if(s.startsWith("张")){
listA.add(s);
}
}
//对listA集合进行过滤,只要姓名长度为3的人,存储到一个新集合中
List<String> listB = new ArrayList<>();
for (String s : listA) {
if(s.length()==3){
listB.add(s);
}
}
//遍历listB集合
for (String s : listB) {
System.out.println(s);
}
}
}
这段代码中含有三个循环,每一个作用不同:
-
首先筛选所有姓张的人;
-
然后筛选名字有三个字的人;
-
最后进行对结果进行打印输出。
每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环只是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。 这是一种远不够高效、笨拙的方法。
1.2 Stream写法
关注的是做什么,而不是怎么做,借助Java 8的Stream API
对流中的元素进行操作,代码更加简洁易读,而且使用并发模式,程序执行速度更快。
public class Demo02Stream {
public static void main(String[] args) {
//创建一个List集合,存储姓名
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
//对list集合中的元素进行过滤,只要以张开头的元素,存储到一个新的集合中
//对listA集合进行过滤,只要姓名长度为3的人,存储到一个新集合中
//遍历listB集合
list.stream()
.filter(name->name.startsWith("张"))
.filter(name->name.length()==3)
.forEach(name-> System.out.println(name));
}
}
直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。
二、Stream概念
-
Stream 是Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。使用
Stream API
对集合数据进行操作,就类似于使用SQL 执行的数据库查询。也可以使用Stream API
来并行执行操作。简而言之,Stream API
提供了一种高效且易于使用的处理数据的方式。 -
Stream(流)是数据渠道,用于操作数据源(集合、数组等)所生成的元素(特定类型的对象)序列。“集合讲的是数据,流讲的是计算!”
注意:
- Stream 其实是一个集合元素的函数模型,并不是集合元素,也不是数据结构,其本身并不存储任何元素(或其地址值),它是有关算法和计算的
- 数据源本身可以是无限的。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行
-
和以前的Collection操作不同,Stream操作还有两个基础的特征:
- Pipelining :中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style)。这样做可以对操作进行优化,比如延迟执行(laziness)和短路(short-circuiting)。
- 内部迭代 :以前对集合遍历都是通过
Iterator
或者增强for
的方式,显式的在集合外部进行迭代,这叫做外部迭代。Stream
提供了内部迭代的方式,流可以直接调用遍历方法。
三、Stream操作步骤
当我们使用一个流的时候,通常包括三个基本步骤:
- 创建Stream:获取一个数据源(如:集合、数组)
- 中间操作:一个中间操作链,对数据源的数据进行处理【数据转换】
- 终止操作(终端操作):一个终止操作,执行中间操作链,并产生结果
注意:每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。
四、创建Stream
java.util.stream.Stream<T>
是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
获取一个流非常简单,有以下几种常用的方式:
-
所有的
Collection
集合都可以通过stream
默认方法获取流;default Stream<E> stream()
: 返回一个顺序流default Stream<E> parallelStream()
: 返回一个并行流
-
Stream
接口的静态方法of
可以获取数组对应的流。static <T> Stream<T> of(T... values)
参数是一个可变参数,那么我们就可以传递一个数组 -
Arrays
中的静态方法stream()
获取数组流static <T> Stream<T> stream(T[] array)
: 返回顺序Stream
与指定的数组作为源
4.1 根据Collection获取流
java.util.Collection
接口中加入了default
方法 stream
用来获取流,所以其所有实现类均可获取流。
import java.util.*;
import java.util.stream.Stream;
public class Demo04GetStream {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
// stream()
Stream<String> stream1 = list.stream();
// parallelStream()
Stream<String> parallelStream1 = list.parallelStream();
Set<String> set = new HashSet<>();
// stream()
Stream<String> stream2 = set.stream();
// parallelStream()
Stream<String> parallelStream2 = set.parallelStream();
Vector<String> vector = new Vector<>();
// stream()
Stream<String> stream3 = vector.stream();
// parallelStream()
Stream<String> parallelStream3 = vector.parallelStream();
}
}
4.2 根据Map获取流
java.util.Map
接口不是 Collection
的子接口,且其 K-V 数据结构不符合流元素的单一特征,所以获取对应的流需要分 key
、value
或 entry
等情况:
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;
public class Demo05GetStream {
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
// ...
Stream<String> keyStream = map.keySet().stream();
Stream<String> valueStream = map.values().stream();
Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream();
}
}
4.3 根据数组获取流
如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream
接口中提供了静态方法 of
,还有Arrays
数组工具类也提供了静态方法 stream
来获取流,使用方法也很简单:
import java.util.stream.Stream;
public class Demo06GetStream {
public static void main(String[] args) {
String[] array = {"张三", "李四", "王五", "赵六", "田七"};
// of
Stream<String> stream1 = Stream.of(array);
// stream
Stream<String> stream2 = Arrays.stream(array);
}
}
五、中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。
中间操作常用方法:
filter()
: 对元素进行过滤map()
:元素映射,用于类型转换limit()
:用于截取流中的元素skip()
:用于跳过元素sorted()
:对元素排序distinct()
:去除重复的元素
5.1 过滤:filter
-
可以通过
filter
方法将一个流转换成另一个子集流。方法签名:Stream<T> filter(Predicate<? super T> predicate);
该接口接收一个
Predicate
函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。 -
Predicate中的抽象方法 :
boolean test(T t);
该方法将会产生一个
boolean
值结果,代表指定的条件是否满足。如果结果为true
,那么Stream
流的filter
方法将会留用元素;如果结果为false
,那么filter
方法将会舍弃元素。 -
代码演示
import java.util.stream.Stream; public class Demo07Stream_filter { public static void main(String[] args) { //创建一个Stream流 Stream<String> stream = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌"); //对Stream流中的元素进行过滤,只要姓张的人 Stream<String> stream2 = stream.filter((String name)->{return name.startsWith("张");}); //遍历stream2流 stream2.forEach(name-> System.out.println(name)); //遍历stream流 (抛异常) stream.forEach(name-> System.out.println(name)); } }
注意:
Stream流属于管道流,只能被消费(使用)一次。
第一个 Stream 流调用完毕方法,数据就会流转到下一个Stream上,而这时第一个 Stream 流已经使用完毕,就会自动关闭了,所以第一个 Stream 流就不能再调用方法了,如果调用就会抛
IllegalStateException: stream has already been operated upon or closed
异常
5.2 映射:map
-
将流中的元素映射到另一个流中,使用
map
方法。方法签名:<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个
Function
函数式接口参数,可以将当前流中的T
类型数据转换为另一种R
类型的流。 -
Function中的抽象方法:
R apply(T t);
这可以将一种 T
类型转换成为 R
类型,而这种转换的动作,就称为 映射 。
-
代码演示:
import java.util.stream.Stream; public class Demo08Stream_map { public static void main(String[] args) { //获取一个String类型的Stream流 Stream<String> stream = Stream.of("1", "2", "3", "4"); //使用map方法,把字符串类型的整数,转换(映射)为Integer类型的整数 Stream<Integer> stream2 = stream.map((String s)->{ return Integer.parseInt(s); }); //遍历Stream2流 stream2.forEach(i-> System.out.println(i)); } }
5.3 截取:limit
-
limit
方法可以对流进行截取,只取用前n个。方法签名:Stream<T> limit(long maxSize);
参数是一个
long
型,如果集合当前长度大于参数则进行截取;否则不进行操作。 -
代码演示:
import java.util.stream.Stream; public class Demo09Stream_limit { public static void main(String[] args) { //获取一个Stream流 String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"}; Stream<String> stream = Stream.of(arr); //使用limit对Stream流中的元素进行截取,只要前3个元素 Stream<String> stream2 = stream.limit(3); //遍历stream2流 stream2.forEach(name-> System.out.println(name)); } }
5.4 跳过:skip
-
skip
方法可以对流进行截取,跳过前n个元素。方法签名:Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为 0 的空流。
-
代码演示:
import java.util.stream.Stream; public class Demo10Stream_skip { public static void main(String[] args) { //获取一个Stream流 String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"}; Stream<String> stream = Stream.of(arr); //使用skip方法跳过前3个元素 Stream<String> stream2 = stream.skip(3); //遍历stream2流 stream2.forEach(name-> System.out.println(name)); } }
5.5 排序:sorted
-
sorted
方法可以对流进行排序。方法签名:Stream<T> sorted()
:返回由此流的元素组成的流,根据自然顺序排序。Stream<T> sorted(Comparator<? super T> comparator)
:返回由该流的元素组成的流,根据提供的Comparator
进行排序。
-
代码演示
import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Demo11Stream_sorted { public static void main(String[] args) { List<Integer> list = Arrays.asList(2, 3, 1, 5,4); //sorted():产生一个新流,其中按自然顺序排序 Stream<Integer> stream=list.stream(); Stream<Integer> stream2 = stream.sorted(); stream2.forEach(i-> System.out.println(i)); // sorted(Comparator):产生一个新流,其中按比较器顺序排序 Stream<Integer> stream3=list.stream(); Stream<Integer> stream4 = stream3.sorted((param1,param2) -> (param1 < param2 ? 1 : -1 )); stream4.forEach(i-> System.out.println(i)); } }
5.6 去重:distinct
-
distinct
方法可以对去除流中的重复元素。方法签名:
Stream<T> distinct()
:返回该流的不同元素 -
代码演示
import java.util.Arrays; import java.util.List; import java.util.stream.Stream; public class Demo11Stream_distinct { public static void main(String[] args) { List<Integer> list = Arrays.asList(2, 2, 1, 4,4); //sorted():产生一个新流,其中按自然顺序排序 Stream<Integer> stream=list.stream(); Stream<Integer> stream2 = stream.distinct(); stream2.forEach(i-> System.out.println(i)); } }
六、终止操作
执行中间操作链,并产生结果。
常用方法:
forEach()
:遍历每个元素。reduce()
:把Stream
元素组合起来。例如,字符串拼接,数值的 sum,min,max ,average 都是特殊的 reduce。concat()
:合并流min()
:找到最小值。max()
:找到最大值。count()
:返回流中元素总数
6.1 逐一处理:forEach
-
虽然方法名字叫
forEach
,但是与for
循环中的“for-each”昵称不同。 方法签名:void forEach(Consumer<? super T> action);
该方法接收一个
Consumer
接口函数,会将每一个流元素交给该函数进行处理。 -
Consumer接口中包含抽象方法:
void accept(T t)
,意为消费一个指定泛型的数据。
6.2 元素组合:reduce
这个方法的主要作用是把 Stream元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面Stream的第一个、第二个、第n个元素组合。从这个意义上说,字符串拼接、数值的 sum、min、max、average都是特殊的reduce。例如Stream的sum就相当于:
Integer sum = integers.reduce(0, (a, b) -> a+b);
或
Integer sum = integers.reduce(0, Integer::sum);
也有 没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。
// reduce 的用例
// 字符串连接,concat = "ABCD"
String concat = Stream.of("A", "B", "C", "D").reduce("", String::concat);
// 求最小值,minValue = -3.0
double minValue = Stream.of(-1.5, 1.0, -3.0, -2.0).reduce(Double.MAX_VALUE, Double::min);
// 求和,sumValue = 10, 有起始值
int sumValue = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);
// 求和,sumValue = 10, 无起始值
sumValue = Stream.of(1, 2, 3, 4).reduce(Integer::sum).get();
// 过滤,字符串连接,concat = "ace"
concat = Stream.of("a", "B", "c", "D", "e", "F").
filter(x -> x.compareTo("Z") > 0).
reduce("", String::concat);123456789101112131415161718
上面代码例如第一个示例的reduce(),第一个参数(空白字符)即为起始值,第二个参数(String::concat)为 BinaryOperator。这类有起始值的 reduce() 都返回具体的对象。而对于第四个示例没有起始值的 reduce(),由于可能没有足够的元素,返回的是一个 Optional
类型的对象,可以通过 get()
方法获得值。
6.3 流组合:concat
如果有两个流,希望合并成为一个流,那么可以使用 Stream
接口的静态方法 concat
:
static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
注意::这是一个静态方法,与
java.lang.String
当中的concat
方法是不同的。
public class Demo08Stream_concat {
public static void main(String[] args) {
//创建一个Stream流
Stream<String> stream1 = Stream.of("张三丰", "张翠山", "赵敏", "周芷若", "张无忌");
//获取一个Stream流
String[] arr = {"美羊羊","喜洋洋","懒洋洋","灰太狼","红太狼"};
Stream<String> stream2 = Stream.of(arr);
//把以上两个流组合为一个流
Stream<String> concat = Stream.concat(stream1, stream2);
//遍历concat流
concat.forEach(name-> System.out.println(name));
}
}
6.4 最值:max/min
Optional<T> max(Comparator<? super T> comparator)
:根据提供的Comparator
返回此流的最大元素。Optional<T> min(Comparator<? super T> comparator)
:根据提供的Comparator
返回此流的最小元素。
public class Demo12Stream_zuizhi {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(2, 3, 1, 5,4);
Stream<Integer> stream=list.stream();
Optional min=stream.min((param1,param2) -> (int)param1 > (int)param2 ? 1:-1 );
System.out.println(min.get());//1
Stream<Integer> stream2=list.stream();
Optional max=stream2.max((param1,param2) -> (int)param1 > (int)param2 ? 1:-1 );
System.out.println(max.get());//5
}
}
6.5 统计个数:count
正如旧集合 Collection
当中的 size
方法一样,流提供 count
方法来数一数其中的元素个数:
long count();
该方法返回一个 long
值代表元素个数(不再像旧集合那样是int值)。
import java.util.stream.Stream;
public class Demo13Stream_count {
public static void main(String[] args) {
Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
Stream<String> result = original.filter(s ‐> s.startsWith("张"));
System.out.println(result.count()); // 2
}
}
七、练习
现在有两个 ArrayList
集合存储队伍当中的多个成员姓名,Stream流式处理方式进行以
下若干操作步骤:
-
第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
-
第一个队伍筛选之后只要前3个人;存储到一个新集合中。
-
第二个队伍只要姓张的成员姓名;存储到一个新集合中。
-
第二个队伍筛选之后不要前2个人;存储到一个新集合中。
-
将两个队伍合并为一个队伍;存储到一个新集合中。
-
根据姓名创建 Person 对象;存储到一个新集合中。
-
打印整个队伍的Person对象信息。
代码如下:
person类
public class Person {
private String name;
public Person() {
}
public Person(String name) {
this.name = name;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
'}';
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
测试类
import java.util.ArrayList;
import java.util.stream.Stream;
public class StreamTest {
public static void main(String[] args) {
//第一支队伍
ArrayList<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("宋远桥");
one.add("苏星河");
one.add("石破天");
one.add("石中玉");
one.add("老子");
one.add("庄子");
one.add("洪七公");
//1. 第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
//2. 第一个队伍筛选之后只要前3个人;存储到一个新集合中。
Stream<String> oneStream = one.stream().filter(name -> name.length() == 3).limit(3);
//第二支队伍
ArrayList<String> two = new ArrayList<>();
two.add("古力娜扎");
two.add("张无忌");
two.add("赵丽颖");
two.add("张三丰");
two.add("尼古拉斯赵四");
two.add("张天爱");
two.add("张二狗");
//3. 第二个队伍只要姓张的成员姓名;存储到一个新集合中。
//4. 第二个队伍筛选之后不要前2个人;存储到一个新集合中。
Stream<String> twoStream = two.stream().filter(name -> name.startsWith("张")).skip(2);
//5. 将两个队伍合并为一个队伍;存储到一个新集合中。
//6. 根据姓名创建Person对象;存储到一个新集合中。
//7. 打印整个队伍的Person对象信息。
Stream.concat(oneStream,twoStream).map(name->new Person(name)).forEach(System.out::println);
}
}
上述采用了链式编程,减少了中间变量,极大的简化了代码量,本文其他地方也可采用链式编程来简化代码。