Java常⽤对象转换之Stream流
平时的开发中会经常遇到⼀些对象需要转换,创建了⼀个项⽬记录⼀些常见对象转换的⽅法,例如:⽂件转换、⽇期时间转换、stream 流转换、集合对象转换等,具体的⽰例代码见 GitHub 项⽬:zzycreate/java-convert-example 。
本⽂记录⼀些常⽤的 Stream 操作,以备需要时直接使⽤。
流的基本构成
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像⼀个⾼级版本的 Iterator。
原始版本的 Iterator,⽤户只能显式地⼀个⼀个遍历元素并对其执⾏某些操作;
⾼级版本的 Stream,⽤户只要给出需要对其包含的元素执⾏什么操作,⽐如 “过滤掉长度⼤于 10 的字符串”、“获取每个字符串的⾸字母”等,Stream 会隐式地在内部进⾏遍历,做出相应的数据转换。
Stream 就如同⼀个迭代器(Iterator),单向,不可往复,数据只能遍历⼀次,遍历过⼀次后即⽤尽了,就好⽐流⽔从⾯前流过,⼀去不复返。
⽽和迭代器⼜不同的是,Stream 可以并⾏化操作,迭代器只能命令式地、串⾏化操作。
顾名思义,当使⽤串⾏⽅式去遍历时,每个 item 读完后再读下⼀个 item。⽽使⽤并⾏去遍历时,数据会被分成多个段,其中每⼀个都在不同的线程中处理,然后将结果⼀起输出。
filter过滤对象数组Stream 流的使⽤基本分为三种操作:⽣成 Stream 流数据源、Stream 流中值操作、Stream 流结束操作。另外还有 short-circuiting 操作作为补充。
Stream 流的⽣成
想要获取 Stream 流数据源有很多种⽅式:
1. 从集合中获取:集合对象(List、Set、Queue 等)的 stream()、parallelStream() ⽅法可以直接获取 Stream 对象;
2. 从数组中获取:数据对象可以利⽤ Arrays.stream(T[] array) 或者 Stream.of() 的⼯具⽅法获取 Stream 对象;
3. 从 IO 流中获取:BufferedReader 提供了 lines() ⽅法可以逐⾏获取 IO 流⾥⾯的数据;
4. 静态⼯⼚⽅法:Stream.of(Object[])、IntStream.range(int, int)、Stream.iterate(Object, UnaryOperator) 等静态⼯⼚⽅法可以
提供 Stream 对象;
5. Files 类的操作路径的⽅法:如 list、find、walk 等;
6. 随机数流:Random.ints();
7. 其他诸如 Random.ints()、BitSet.stream()、Pattern.splitAsStream(java.lang.CharSequence)、JarFile.stream() 等⽅法;
8. 更底层的使⽤ StreamSupport,它提供了将 Spliterator 转换成流的⽅法。
Stream 流的中间操作 (Intermediate)
⼀个流可以后⾯跟随零个或多个 intermediate 操作。其⽬的主要是打开流,做出某种程度的数据映射 / 过滤,然后返回⼀个新的流,交给下⼀个操作使⽤。
这类操作都是惰性化的(lazy),就是说,仅仅调⽤到这类⽅法,并没有真正开始流的遍历。只有在 Terminal 操作执⾏时才会真正的执⾏这些 Intermediate 操作。
常⽤的 Intermediate 操作有:map (mapToInt, flatMap 等)、filter、distinct、sorted、peek、limit、skip、parallel、sequential、unordered。
Stream 流的执⾏操作 (Terminal)
⼀个流只能有⼀个 terminal 操作,当这个操作执⾏后,流就被使⽤“光”了,⽆法再被操作。所以这必定是流的最后⼀个操作。
Terminal 操作的执⾏,才会真正开始流的遍历,并且会⽣成⼀个结果,或者⼀个 side effect。
常⽤的 Terminal 操作有:forEach、forEachOrdered、toArray、reduce、collect、min、max、count、anyMatch、allMatch、noneMatch、findFirst、findAny、iterator。
Short-circuiting
当操作⼀个⽆限⼤的 Stream,⽽⼜希望在有限时间内完成操作,则在管道内拥有⼀个 short-circuiting 操作是必要⾮充分条件。
常⽤的 Short-circuiting 操作有:anyMatch、allMatch、noneMatch、findFirst、findAny、limit。
⽣成 Stream 流数据源
集合对象 -> Stream
集合对象本⾝提供了 stream() 和 parallelStream() ,两个⽅法可以直接获取 Stream 流
Stream<String> listStream = list.stream();
Stream<String> listParallelStream = list.parallelStream();
Stream<String> setStream = set.stream();
Stream<String> setParallelStream = set.parallelStream();
数组对象 -> Stream
数组对象转换需要利⽤⼯具类 Arrays、 Stream 的静态⽅法
Stream<String> arrayStream = Arrays.stream(array);
Stream<String> arrayStream1 = Stream.of(array);
IO 流 -> Stream
IO 流可以包装成 BufferedReader 转换为 Stream
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(""), StandardCharsets.UTF_8));
Stream<String> stream = reader.lines();
流对象提供的构造⽅法
IntStream intStream = IntStream.range(1, 4);
DoubleStream doubleStream = DoubleStream.builder().add(1.1).add(2.1).add(3.1).add(4.1).build();
LongStream longStream = LongStream.of(1L, 2L, 3L, 4L);
Stream 流的 Intermediate 操作
⽰例代码: StreamIntermediateExample.java
map
map 的作⽤就是把 input Stream 的每⼀个元素,映射成 output Stream 的另外⼀个元素。
// 转⼤写
List<String> stringList = list.stream()
.map(String::toUpperCase)
.List()); // [ABC, EFG, HIJ]
// 数据计算
List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9).stream()
.map(n -> n * n)
.List()); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
// 获取对象属性
List<String> list = list.stream()
.map(Item::getDetail).map(ItemDetail::getValue)
.
List()); // [v1, v5, v3, v2, v4]
flatMap
flatMap 把 input Stream 中的层级结构扁平化
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
// 将集合对象⾥⾯的数据拿出来转换为扁平结构
Stream<Integer> outputStream = inputStream.
flatMap((childList) -> childList.stream()); // [1, 2, 3, 4, 5, 6]
filter
filter 对原始 Stream 进⾏某项测试,通过测试的元素被留下来⽣成⼀个新 Stream。
Integer[] sixNums = {1, 2, 3, 4, 5, 6};
// 对 2 取模等于 0 的是偶数,filter 留下数字中的偶数
Integer[] evens = Stream.of(sixNums)
.filter(n -> n % 2 == 0)
.toArray(Integer[]::new); // [2, 4, 6]
distinct
distinct 是对元素进⾏去重,去重是利⽤了对象的 hashCode() 和 equals() ⽅法。
如果 distinct()正在处理有序流,那么对于重复元素,将保留以遭遇顺序⾸先出现的元素,并且以这种⽅式选择不同元素是稳定的。在⽆序流的情况下,不同元素的选择不⼀定是稳定的,是可以改变的。distinct()执⾏有状态的中间操作。
在有序流的并⾏流的情况下,保持 distinct()的稳定性是需要很⾼的代价的,因为它需要⼤量的缓冲开销。
如果我们不需要保持遭遇顺序的⼀致性,那么我们应该可以使⽤通过 BaseStream.unordered()⽅法实现的⽆序流。
Integer[] nums = {1, 1, 2, 3, 4, 5, 4, 5, 6};
Integer[] evens = Stream.of(nums)
.distinct()
.toArray(Integer[]::new);// [1, 2, 3, 4, 5, 6]
sorted
sorted ⽅法⽤于排序,利⽤ Comparator 类的静态⽅法可以快速构造⼀个⽐较器实现排序。
List<Integer> list = Arrays.asList(5, 2, 4, 8, 6, 1, 9, 3, 7);
// sorted() ⽆参⽅法为⾃然排序
List<Integer> sorted = list.stream().sorted().List());// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 使⽤ verseOrder() 获得⼀个⾃然逆序⽐较器,⽤于逆序排序
List<Integer> reverse = list.stream().verseOrder()).List());// [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 使⽤ Comparatorparing() 获取⼀个⾃定义⽐较器,显现⾃定义对象的排序
List<Item> codeSorted = wItems().stream()
.sorted(Comparatorparing(Item::getCode))
.List());
// [Item(name=Name1, code=1, number=1.1, detail=ItemDetail(id=101, value=v1)), Item(name=Name2, code=2, number=2.2, detail=ItemDetail(id=202, value=v2) List<Item> codeReverse = wItems().stream()
.sorted(Comparatorparing(Item::getCode).reversed())
.
List());
// [Item(name=Name5, code=5, number=5.5, detail=ItemDetail(id=505, value=v5)), Item(name=Name4, code=4, number=4.4, detail=ItemDetail(id=404, value=v4)
unordered
某些流的返回的元素是有确定顺序的,我们称之为 encounter order。这个顺序是流提供它的元素的顺序,⽐如数组的 encounter order
是它的元素的排序顺序,List 是它的迭代顺序 (iteration order),对于 HashSet,它本⾝就没有 encounter order。
⼀个流是否是 encounter order 主要依赖数据源和它的中间操作,⽐如数据源 List 和 Array 上创建的流是有序的 (ordered),但是在
HashSet 创建的流不是有序的。
sorted() ⽅法可以将流转换成 encounter order 的,unordered 可以将流转换成 encounter order 的。
注意,这个⽅法并不是对元素进⾏排序或者打散,⽽是返回⼀个是否 encounter order 的流。
可以参见 stackoverflow 上的问题: stream-ordered-unordered-problems
除此之外,⼀个操作可能会影响流的有序,⽐如 map ⽅法,它会⽤不同的值甚⾄类型替换流中的元素,所以输⼊元素的有序性已经变得没
有意义了,但是对于 filter ⽅法来说,它只是丢弃掉⼀些值⽽已,输⼊元素的有序性还是保障的。
对于串⾏流,流有序与否不会影响其性能,只是会影响确定性 (determinism),⽆序流在多次执⾏的时候结果可能是不⼀样的。
对于并⾏流,去掉有序这个约束可能会提⾼性能,⽐如 distinct、groupingBy 这些聚合操作。
peek
peek() 接受⼀个 Consumer 消费者⽅法,⽽ map() 接受⼀个 Function ⽅法;Consumer ⽅法返回值是 void,⽽ Function ⽅法有返
回值,peek 和 map ⽅法的区别主要在于流处理过程中返回值的不同。
peek() ⽅法是 Intermediate ⽅法,⽽ forEach() ⽅法是 Terminal ⽅法;如果 peek ⽅法后没有 Terminal ⽅法,则 peek 并不会真正的
执⾏,forEach ⽅法则会⽴即执⾏。
forEach 和 peek 都是接受 Consumer 对象的,因此如果在 Stream 流处理的过程中做⼀些数据操作或者打印操作,选择 peek ⽅法,该
⽅法还会返回 Stream 流,⽤于下⼀步处理;如果已经是处理的最后⼀步,则选择 forEach ⽤于最终执⾏整个流。
// [Abc, efG, HiJ] -> [Abc, efG, HiJ]
List<String> peek = wStringList().stream()
.peek(str -> {
if ("Abc".equals(str)) {
str = UpperCase();
}
}).List());
peek ⽅法对对象的修改,会影响到集合⾥⾯的元素,但如果集合中是 String 这种,则不会改变,
因为修改后的 String 在常量池中是另⼀个对象,由于 Consumer ⽆法返回该对象,Stream 内的元素仍然指向原来的 String。
对对象的修改则是改变堆中对象的数据,对象的引⽤并没有发⽣变化,Stream 中的元素任然指向原对象,只是对象内部已经发⽣了改变。
// [Name1, Name5, Name3, Name2, Name4] -> [xxx, Name5, Name3, Name2, Name4]
List<String> peek1 = wItems().stream()
.peek(item -> {
if (Code() == 1) {
item.setName("xxx");
}
})
.map(Item::getName)
.List());
limit
limit ⽅法会对⼀个 Stream 进⾏截断操作,获取其前 N 个元素,如果原 Stream 中包含的元素个数⼩于 N,那就获取其所有的元素;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 截取指定元素位置以内的元素
List<Integer> limit2 = numbers.stream().limit(2).List());// [1, 2]
List<Integer> limit6 = numbers.stream().limit(6).List());// [1, 2, 3, 4, 5, 6]
List<Integer> limit8 = numbers.stream().limit(8).List());// [1, 2, 3, 4, 5, 6]
skip
skip ⽅法会返回⼀个丢弃原 Stream 的前 N 个元素后剩下元素组成的新 Stream,如果原 Stream 中包含的元素个数⼩于 N,那么返回空Stream;
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
// 忽略指定元素位置以内的元素
List<Integer> skip2 = numbers.stream().skip(2).List());// [3, 4, 5, 6]
List<Integer> skip6 = numbers.stream().skip(6).List());// []
List<Integer> skip8 = numbers.stream().skip(8).List());// []
parallel
parallel stream 是基于 fork/join 框架的,简单点说就是使⽤多线程来完成的,使⽤ parallel stream 时要考虑初始化 fork/join 框架的时间,
如果要执⾏的任务很简单,那么初始化 fork/join 框架的时间会远多于执⾏任务所需时间,也就导致了效率的降低。
根据附录 doug Lee 的说明,任务数量*执⾏⽅法的⾏数》=10000 或者执⾏的是消耗⼤量时间操作(如 io/ 数据库)才有必要使⽤
Java 8 为 ForkJoinPool 添加了⼀个通⽤线程池,这个线程池⽤来处理那些没有被显式提交到任何线程池的任务。
它是 ForkJoinPool 类型上的⼀个静态元素,它拥有的默认线程数量等于运⾏计算机上的处理器数量。
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论