Java 8新特性之Stream流
下面来看一下借助Java 8
的StreamAPl
,什么才叫优雅:
public class StreamIteratorDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("张三丰");
list.add("周芷若");
list.add("赵敏");
list.add("张三");
list.stream()
.filter(name->name.startsWith("张"))
.filter(name->name.length() == 3)
.forEach(name-> System.out.println(name));
}
}
流式思想概述
注意:请暂时忘记对传统IO
流的固有印象!
整体来看,流式思想类似于工厂车间的“生产流水线”
当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能
及便利性,我们应该首先拼好一个“模型”步骤方案,然后再按照方案去
执行它。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案,而方案就是一种“函数模型”。图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换为另一个流模型。而最右侧的数字3是最终结果。
这里的filter、map、skip都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。
备注:
Stream流
其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。
Stream
(流)是一个来自数据源的元素队列
- 元素是特定类型的对象,形成一个队列。Java中的Stream并不会存储元素,而是按需计算。
- 数据源流的来源。可以是集合,数组等。
和以前的Collection
操作不同,Stream
操作还有两个基础的特征:
Pipelining
:中间操作都会返回流对象本身。这样多个操作可以串联成一个管道,如同流式风格(fluent style
)。这样做可以对操作进行优化,比如延迟执行(laziness
)和短路(short-circuiting
)。- 内部迭代:以前对集合遍历都是通过
Iterator
或者增强for
的方式,显式的在集合外部进行迭代,这叫做外部迭代。Stream
提供了内部迭代的方式,流可以直接调用遍历方法。
当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source
)→数据转换一执行操作获取想要的结果,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。
获取流
java.uti1.stream.stream<T>
是Java8
新加入的最常用的流接口。(这并不是一个函数式接口)获取一个流非常简单,有以下几种常用的方式:
- 所有的
Collection
集合都可以通过stream
默认方法获取流; stream
接口的静态方法of
可以获取数组对应的流。
根据Collection获取流
首先,java.util.collection
接口中加入了default
方法stream
用来获取流,所以其所有实现类均可获取流。
public class GetStreamDemo {
public static void main(String[] args) {
// 把集合转为Stream流,通过stream方法获取
List<String> list = new ArrayList<>();
Stream<String> listStream = list.stream();
Set<String> set = new HashSet<>();
Stream<String> setStream = set.stream();
}
}
根据Map获取流
// 间接将map转为stream
Map<String, String> map = new HashMap<>();
Set<String> mapKeySet = map.keySet();
Stream<String> keySetStream = mapKeySet.stream();
Collection<String> mapValues = map.values();
Stream<String> collectionStream = mapValues.stream();
Set<Map.Entry<String, String>> mapEntrySet = map.entrySet();
Stream<Map.Entry<String, String>> entrySetStream = mapEntrySet.stream();
根据数组获取Stream
流
int[] helloInt = {1, 2, 3, 4, 5};
Stream<int[]> helloIntStream = Stream.of(helloInt);
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4, 5, 6);
String[] helloString = {"a", "bb", "cc"};
Stream<String> helloStringStream = Stream.of(helloString);
常用方法
流模型的操作很丰富,这里介绍一些常用的APl
。这些方法可以被分成两种:
- 延迟方法:返回值类型仍然是
Stream
接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。) - 终结方法:返回值类型不再是
stream
接口自身类型的方法,因此不再支持类似stringBuilder
那样的链式调用。本小节中,终结方法包括count
和forEach
方法。
备注:本小节之外的更多方法,请自行参考API文档。
逐一处理:forEach
public class StreamForEachDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("王五");
list.add("赵六");
list.add("二麻子");
Stream<String> stream = list.stream();
// 使用forEach对流进行遍历
stream.forEach(name -> System.out.println(name));
}
}
过滤:filter
可以通过filter
方法将一个流转换成另一个子集流。方法签名:
Stream<T> filter(Predicate<? super T> predicate);
该接口接收个Predicate
函数式接口参数(可以是一个Lambda
或方
法引用)作为筛选条件。
此前已经学习过java.util.stream.Predicate
函数式接口,其中的方法为:
boolean test(T t)
该方法将会产生一个boolean
值结果,代表指定的条件是否满足。如
果结果为true
,那么Stream
流的filter
方法将会留用元素;如果
结果为false
,那么filter
方法将会舍弃元素。
Stream流中的filter方法基本使用的代码如:
public class StreamFilterDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张三");
list.add("张三丰");
list.add("张二狗");
list.add("赵六");
list.add("二麻子");
// 执行filter后会返回Stream流,可以继续对Stream流进行操作,链式调用
Stream<String> listStream = list.stream()
.filter(name->name.startsWith("张"))
.filter(name->name.length() == 3);
listStream.forEach(name -> System.out.println(name));
}
}
Stream
属于管道流,只能被消费一次,第一个Stream
流被调用完毕,数据就会流转到下一个Stream
上,而这时第一个Stream
流已经使用完毕就关闭了,所以第一个Stream
流不能再调用方法。
// 对上述代码消费两次
listStream.forEach(name -> System.out.println(name));
listStream.forEach(name -> System.out.println(name));
执行结果为:
张三丰
张二狗
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
映射:map
如果需要将流中的元素映射到另一个流中,可以使用map方法。方法签名:
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个Function
函数式接口参数,可以将当前流中的T
类型数据转换为另一种R
类型的流。
此前我们已经学习过java.util.stream.Function
函数式接口,其中唯一的抽象方法为:
R apply(T t);
这可以将一种T
类型转换成为R
类型,而这种转换的动作,就称为“映射”。
Stream
流中的map
方法基本使用的代码如:
public class StreamMapDemo {
public static void main(String[] args) {
// 获取Stream
Stream<String> stream = Stream.of("5", "12", "34");
//将字符串Stream转为Integer类型的Stream
Stream<Integer> integerStream = stream.map(str -> Integer.parseInt(str));
integerStream.forEach(num -> System.out.println(num));
}
}
统计个数:count
正如旧集合Collection
当中的size
方法一样,流提供count
方法来数一数其中的元素个数:
long count();
该方法返回一个long
值代表元素个数(不再像旧集合那样是int
值)。基本使用:
public class StreamCountDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("A man");
list.add("needs to");
list.add("know");
list.add("when to stand up");
list.add("for himself");
long count = list.stream().count();
System.out.println(count);
}
}
结果为:
5
截取:limit
limit
方法可以对流进行截取,只取用前n
个。方法签名:
Stream<T> limit(long maxsize);
参数是一个long
型,如果集合当前长度大于参数则进行截取;否则不进行操作。基本使用:
public class StreamLimitDemo {
public static void main(String[] args) {
// 获取一个Stream流
String[] strings = {"万界神主","斗罗大陆","斗破苍穹"};
Stream<String> stringStream = Stream.of(strings);
// 使用limit方法对Stream流中的元素截取只要前2个
stringStream.limit(2).forEach(name-> System.out.println(name));
}
}
运行结果:
万界神主
斗罗大陆
跳过:skip
如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流:
Stream<T> skip(long n);
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
/**
* Stream流中的Skip方法:用于跳过元素
* 如果希望跳过前几个元素,可以使用skip方法获取一个截取后的新流
* 如果流的当前长度大于n,则跳过前n个,否则会得到一个长度为0的空流
* @author guqin
* @date 2019-07-23 20:53
*/
public class StreamSkipDemo {
public static void main(String[] args) {
String[] strings = {"万界神主","斗罗大陆","斗破苍穹","天行九歌"};
Stream<String> stringStream = Stream.of(strings);
stringStream.skip(2)
.forEach(name-> System.out.println(name));
}
}
运行结果:
斗破苍穹
天行九歌
组合:concat
如果有两个流,希望合并成为一个流,那么可以使用 stream 接口的静态方法concat
:
static <T> Stream<T> concat(Stream<? extends T) a, Stream<? extends T) b);
备注:这是一个静态方法,与
java.lang.String
当中的concat
方法是不同的。
该方法的基本使用代码如:
public class StreamConcatDemo {
public static void main(String[] args) {
Stream<String> stringStream1 = Stream.of("万界神主","斗罗大陆");
Stream<String> stringStream2 = Stream.of("斗破苍穹","天行九歌");
Stream.concat(stringStream1, stringStream2)
.forEach(name -> System.out.println(name));
}
}
运行结果:
万界神主
斗罗大陆
斗破苍穹
天行九歌