
Stream是 Java 8新增加的类,用来补充集合类。
Stream代表数据流,流中的数据元素的数量可能是有限的,也可能是无限的。
什么是流
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的 Iterator。原始版本的 Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的 Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、“获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
Stream 就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
而和迭代器又不同的是,Stream 可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream 的另外一大特点是,数据源本身可以是无限的。
流的创建
流的创建有6种方式,归类如下:
使用Stream.of从值的序列构建流:Stream.of(T... values) 和 Stream.of(T)
Stream.of方法传入元素的可变参数列表,定义与实现如下:
less
复制代码
@SafeVarargs @SuppressWarnings("varargs") // Creating a stream from an array is safe public static<T> Stream<T> of(T... values) { return Arrays.stream(values); }
@SafeVarargs属于java泛型的一部分,在使用数组作为参数时出现。因为用户有可能讲一个类型化的数组(typed array)赋给一个Object数组,导致添加的元素引发类型安全问题。
Stream.of(T)作为特例,创建一个只有一个元素的流。定义与实现如下:
arduino
复制代码
public static<T> Stream<T> of(T t) { return StreamSupport.stream(new Streams.StreamBuilderImpl<>(t), false); }
单个元素构成的流不是很常用。 使用Stream.of从值的序列创建流的例子如下:
arduino
复制代码
/** * 从值的序列构建流. */ @Test public void testOfValues() { Stream<Integer> stream = Stream.of(1,2,3,4,5,6,7,8,9); stream.forEach(p -> System.out.println(p)); }
作为特殊的例子,可以使用String.chars() 或者 tokens构建流:
csharp
复制代码
/** * 使用String.chars() 或者 tokens构建流. */ @Test public void testStringChars() { IntStream stream = "12345_abcdefg".chars(); stream.forEach(p -> System.out.print(p + ",")); //print 49,50,51,52,53,95,97,98,99,100,101,102,103, System.out.println(); //OR Stream<String> stream2 = Stream.of("A$B$C".split("\\$")); stream2.forEach(p -> System.out.print(p + ",")); //print A,B,C, }
从数组元素构建流:Arrays.stream(T[] array)
如果提前已经创建好了数组,可以从数组创建流。 Arrays.stream定义了多种重载形式,用于处理int,long,double类型和泛型形式的数组。
arduino
复制代码
/** * 从数组元素构建流. */ @Test public void testOfArrayElements() { Stream<Integer> stream = Stream.of( new Integer[]{1,2,3,4,5,6,7,8,9} ); stream.forEach(p -> System.out.println(p)); }
使用Collection.stream()构建流
java8中,Collection接口新增了默认方法stream,来从集合中创建Stream。这可能是最常用的创建Stream的方式了。
ini
复制代码
/** * 使用List.stream()构建流. */ @Test public void testOfListStream() { List<Integer> list = new ArrayList<>(); for (int i = 1; i < 10; i++) { list.add(i); } Stream<Integer> stream = list.stream(); stream.forEach(p -> System.out.println(p)); }
使用Stream.generate()
Stream接口定义了静态工厂方法generate,签名如下:
arduino
复制代码
//in java.util.stream.Stream public static<T> Stream<T> generate(Supplier<T> s);
generate 方法多次调用Supplier产生一个顺序的无序流。
typescript
复制代码
@Test public void testStreamGenerate() { Stream<Date> stream = Stream.generate(() -> new Date()); stream.limit(10).forEach(p -> System.out.println(p)); }
generate方法会无限执行下去,所以我们使用limit限制产生流的元素数量。 Supplier的另一个简单应用是Math.random,这个方法不需要参数返回double类型的数据。
arduino
复制代码
@Test public void testStreamGenerate2() { Stream.generate(Math::random).limit(10) .forEach(System.out::println); }
Stream.iterate()构建流
Stream接口定义另一种静态工厂方法iterate,签名如下:
swift
复制代码
//in java.util.stream.Stream public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
iterate返回一个无限顺序的有序流(infinite sequential ordered stream),它由迭代应用到初始初始元素种子的函数f产生。不废话,看例子:
csharp
复制代码
@Test public void testStreamIterate() { List<BigDecimal> nums = Stream.iterate(BigDecimal.ONE, n -> n.add(BigDecimal.ONE)) .limit(10).collect(Collectors.toList()); System.out.println(nums); //打印结果:[1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }
初始值1作为种子,n -> n.add(BigDecimal.ONE)作为函数f实现数字递增。iterate产生无限流,所以使用limit限制产生个数。下面这个例子打印出当天之后的十天:
scss
复制代码
@Test public void testStreamIterate2() { Stream.iterate(LocalDate.now(), ld -> ld.plusDays(1L)) .limit(10).forEach(System.out::println); }
range和rangeClosed
Stream接口定义了三种专门处理基本类型的子接口,分别是IntStream,LongStream,DoubleStream。IntStream和LongStream还包括另外两种创建流的方法:range和rangeClosed,二者签名如下:
arduino
复制代码
//in java.util.stream.IntStream static IntStream range(int startInclusive, int endExclusive); static IntStream rangeClosed(int startInclusive, int endInclusive); //in java.util.stream.LongStream static LongStream range(long startInclusive, final long endExclusive); static LongStream rangeClosed(long startInclusive, final long endInclusive);
rangeClosed方法包含终值,而range方法不包含终值。两种方法都返回一个顺序的有序流,从第一个参数开始逐一递增。下面展示了两个例子:
scss
复制代码
@Test public void testStreamRange() { List<Integer> ints = IntStream.range(10, 15) .boxed().collect(Collectors.toList()); System.out.println(ints); //打印结果:[10, 11, 12, 13, 14] }
rangeClosed的例子:
scss
复制代码
@Test public void testStreamRangeClosed() { List<Long> ls = LongStream.rangeClosed(10, 15) .boxed().collect(Collectors.toList()); System.out.println(ls); //打印结果:[10, 11, 12, 13, 14, 15] }
流的操作简介
流操作分为中间操作(Intermediate operation)和终结操作(Terminal operation)。 中间操作是lazy的, 不会立即执行,只不过是返回一个记录操作的新的流。 考虑下面的例子,思考一下打印结果是什么呢?
csharp
复制代码
@Test public void testStreamIntermediate() { LongStream.rangeClosed(10, 15).boxed().map(n -> { System.out.println(n); return n; }); }
我们可能期望在控制台打印出10~15这个几个数字,实际上却不会打印出任何东西!原因就在于我们上述例子中只有Stream的中间操作(intermediate),没有终结操作(terminal),没有终结操作的话,Stream实际上只做一些Stream内的标记操作,不会触发任何实际操作,因为Stream没有被使用,Stream中的数据没有被收集起来。 最终操作会最终使用流管道,使用后不能再次被使用。 大部分情况下, 最终操作都是eager的。 概括来说:
- 中间操作返回流,终结操作处理流并结束流;
- 中间操作只是一种标记,只有终结操作才会触发实际计算;
中间操作
中间操作又进一步分为无状态的操作和有状态的操作。 像filter,map都是无状态的操作, 处理一个新的元素时不需要获得先前遍历过的元素的状态。 而有状态的操作,像distinct, sorted, 需要得到先前访问的元素的状态。
有状态的操作在产生结果前需要获得完整的输入。 因此有状态的操作一个并行流时, 可能需要多次传入数据或者需要缓存数据。 而无状态的操作只需传入一次数据。
终结操作
终结操作又可以分为短路操作和非短路操作,短路操作是指不用处理全部元素就可以返回结果,比如找到第一个满足条件的元素。之所以要进行如此精细的划分,是因为底层对每一种情况的处理方式不同。
对Stream的介绍暂时到此为止,后续再介绍Stream的常用操作。
957

被折叠的 条评论
为什么被折叠?



