Stream
说到Stream很容易想到IO流,而实际上,谁规定流一定就要是IO流呢?
在Java8中,得益于Lambda带来的函数式编程,引用了一个全新的Stream概念,用于解决已有集合框架既有的弊端。
引言
传统集合的多步遍历代码
public class Demo {
public static void main(String[] args) {
List list = Lists.asList("李小龙", "张国荣" ,"高渐离", "李白");
for(String str : list) {
System.out.println(str);
}
}
}
循环遍历的弊端
Java8的Lambda让我们可以更加专注于做什么,而不是怎么做:
- for循环的语法就是怎么做
- for循环的循环体才是做什么
为什么这样讲呢?
先思考一个问题,那就是为什么要用循环?因为要进行遍历。但是循环难道是遍历的唯一方式吗?遍历是指对每一个元素都进行处理,而不是指从第一个到最后一个顺序处理。
前者是目的,后者是方式。
如果现在有一个需求,需要对集合中的元素进行过滤筛选:
- 将集合A根据条件一过滤为集合B;
- 将集合B根据条件二过滤为集合C。
// 传统做法
public class Demo {
public static void main(String[] args) {
List list = Lists.asList("李小龙", "张国荣" ,"高渐离", "李白");
List<String> list1 = new ArrayList<>();
for (String str : list) {
if (str.startWith("李"))
list1.add(str);
}
List<String> list2 = new ArrayList<>();
for (String str : list1) {
if (str.length() > 2)
list2.add(str);
}
}
}
这段代码中含有两个循环,每个作用不同,线性循环意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
Stream的优雅
对于上面同样的需求,Stream可以更加优雅的解决:(其实这个例子不是很恰当,但是get到这个意思就好,蛤蛤蛤蛤。。嗝)
public class Demo {
public static void main(String[] args) {
List<String> list = Arrays.asList("李小龙", "张国荣" ,"高渐离", "李白");
list.stream()
.filter(s -> s.startsWith("李")) // 参数是Predicate接口
.filter(s -> s.length() > 2)
.forEach(System.out::println); // 参数是Consumer接口
}
}
流式思想概述
请暂时忘记对传统IO流的固有印象。
流式思想类似于工厂车间的“流水式生产线”。
在生产线中,生产饮料会分为多个步骤,比如:放瓶子、洗瓶子、装饮料、封口、装箱。
这张图中展示了过滤、映射、跳过、计数等多步操作,这是一种集合元素的处理方案。
图中的每一个方框都是一个“流”,调用指定的方法,可以从一个流模型转换到另一个流模型,而最右侧的数字3就是最终的结果。
这里的filter
、map
、skip
都是在对函数模型进行操作,而集合元素并没有被处理。只有当终结方法count
执行时,整个模型才会按照指定策略执行操作。
这得益于lambda的延迟执行特性,所谓的lambda延迟执行是指:lambda体指定的只是一个函数模型,并非实际调用执行。
也就是说,filter、map、skip中指定的lambda体只是一个函数模型,而并不是实际执行的操作,最终方法才是执行的标志。
Stream流其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身当然也并不存储任何元素或引用值。
- Stream不会存储元素,它只是想流水生产线一样去完成既有的功能。
- Stream流的来源可以是集合、数组等。
- Pipeline:中间操作都会返回流对象(filter、map、skip等都返回一个新的stream对象,原有stream对象不变),以此来链式编程。
获取流
- 所有的
Collection
集合都可以通过stream
默认方法获取流; Stream
接口的静态方法of
可以获取数组对应的流。
根据Collection获取流
Collection
接口中加入了default方法stream
来获取流,所以其所有实现类均可获取流。
// Collection接口获取流
public class Demo {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3);
list.stream();
// 由于Map不是Collection的子接口,所以Map的子类获取Stream需要通过Set
HashMap<Object, Object> map = new HashMap<>();
Set<Object> keys = map.keySet();
keys.stream();
Collection<Object> values = map.values();
values.stream();
Set<Map.Entry<Object, Object>> entries = map.entrySet();
entries.stream();
}
}
Stream流的特点
每个stream
只能被使用一次,每次使用完后,该stream
就被关闭了,数据会流转到下一个stream
上。
// 代码示例
public class Demo {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<Integer> stream2 = stream.filter(num -> num == 1);
stream2.forEach(System.out::println);
// 此时stream已经被关闭了,再次使用会报错
stream.forEach(System.out::println);
}
}
程序日志:
1
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
at java.util.stream.AbstractPipeline.sourceStageSpliterator(AbstractPipeline.java:279)
at java.util.stream.ReferencePipeline$Head.forEach(ReferencePipeline.java:580)
at stream.Demo4.main(Demo4.java:19)
常用方法
stream的API可以分为两种:
- 延迟方法:返回值仍然是
Stream
接口自身类型的方法,因此支持链式调用。除了终结方法外,其余方法均为延迟方法。注意这里说的是Stream
类型,但不是同一个Stream
对象。 - 终结方法:返回值类型不是
Strean
接口自身类型的方法,因此不再支持类似StringBuilder
那样的链式调用。我们常接触的终结方法包括count
和forEach
方法。
逐一处理:forEach
// Stream接口源码
void forEach(Consumer<? super T> action);
该方法接收一个Consumer
参数,会将每一个流元素交给该函数进行处理。
// 基本使用
public class Demo2 {
public static void main(String[] args) {
Stream<String> stream = Stream.of("1", "2", "3");
stream.forEach(System.out::println);
}
}
过滤:filter
// Stream接口源码
Stream<T> filter(Predicate<? super T> predicate);
filter
方法接收一个Predicate
参数,以此作为筛选条件,它可以将一个流过滤为另一个子集流。
// 基本使用
public class Demo3 {
public static void main(String[] args) {
List<String> list = Arrays.asList("1", "2", "3");
list.stream()
.filter("3"::equals)
.filter(s -> s.length() > 0)
.forEach(System.out::println);
}
}
映射:map
// Stream接口源码
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
该接口需要一个Function
函数式接口参数,可以将当前流中的前置类型数据转换为另一种类型的流。
// 基本使用
public class Demo5 {
public static void main(String[] args) {
Stream<String> stream = Stream.of("1", "2", "3", "4", "5", "6");
stream.map(Integer::parseInt)
.filter(s -> s instanceof Integer)
.forEach(System.out::println);
}
}
日志打印:
1
2
3
4
5
6
统计个数:count
// Stream接口源码
long count();
// 基本使用
public class Demo6 {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
long count = stream.filter(num -> num == 1)
.count();
System.out.println(count);
}
}
日志打印:
1
取前几个:limit
limit
方法可以对流进行截取,只取前n个。
// Stream接口源码
Stream<T> limit(long maxSize);
参数是一个long型,如果集合当前长度大于参数值就进行截取,否则不进行操作。
// 基本使用
public class Demo {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.limit(3)
.forEach(System.out::println);
}
}
日志打印:
1
2
3
// 参数值大于元素个数
public class Demo {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.limit(6)
.forEach(System.out::println);
}
}
日志打印:
1
2
3
4
5
跳过前几个:skip
如果想跳过前几个元素,可以使用skip
方法取后几个的新stream。
// Stream接口源码
Stream<T> skip(long n);
参数是一个long型,如果集合当前长度大于参数值,则跳过前几个,否则会得到一个长度为0的空stream。
// 基本使用
public class Demo {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.skip(1)
.forEach(System.out::println);
}
}
日志打印:
2
3
4
5
// 参数值大于等于集合长度
public class Demo8 {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
stream.skip(5)
.forEach(System.out::println);
}
}
日志打印:
// 什么都没有
组合:concat
如果想将两个流合并为一个流,可以使用Stream
接口的静态方法concat
。
// Stream接口源码
public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b) {
Objects.requireNonNull(a);
Objects.requireNonNull(b);
@SuppressWarnings("unchecked")
Spliterator<T> split = new Streams.ConcatSpliterator.OfRef<>(
(Spliterator<T>) a.spliterator(), (Spliterator<T>) b.spliterator());
Stream<T> stream = StreamSupport.stream(split, a.isParallel() || b.isParallel());
return stream.onClose(Streams.composedClose(a, b));
}
// 基本使用
public class Demo9 {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
Stream<String> stream2 = Stream.of("6", "7", "8");
Stream<? extends Serializable> newStream = Stream.concat(stream, stream2);
newStream.forEach(System.out::println);
}
}
日志打印:
1
2
3
4
5
6
7
8