Java Stream API借助于Lambda表达式,为Collection操做提供了一个新的选择。若是使用得当,能够极大地提升编程效率和代码可读性。html
本文将介绍Stream API包含的方法,并经过示例详细展现其用法。java
Stream不是集合元素,它不是数据结构也不保存数据,而更像一个高级版本的迭代器(Iterator)。Stream操做能够像链条同样排列,造成Stream Pipeline,即链式操做。算法
Stream Pipeline由数据源的零或多个中间(Intermediate)操做和一个终端(Terminal)操做组成。中间操做都以某种方式进行流数据转换,将一个流转换为另外一个流,转换后元素类型可能与输入流相同或不一样,例如将元素按函数映射到其余类型或过滤掉不知足条件的元素。 终端操做对流执行最终计算,例如将其元素存储到集合中、遍历打印元素等。编程
Stream特色:数组
无存储。Stream不是一种数据结构,也不保存数据,数据源能够是一个数组,Java容器或I/O Channel等。安全
为函数式编程而生。对Stream的任何修改都不会修改数据源,例如对Stream过滤操做不会删除被过滤的元素,而是产生一个不包含被过滤元素的新Stream。数据结构
惰性执行。Stream上的中间操做并不会当即执行,只有等到用户真正须要结果时才会执行。多线程
一次消费。Stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须从新生成。dom
注意:没有终端操做的流管道是静默无操做的,因此不要忘记包含一个终端操做。ide
如下将基于《Java 8 Optional类使用的实践经验》一文中的Person
类,展现Stream API的用法。考虑到代码简洁度,示例中尽可能使用方法引用。
对于可变参数序列,经过Stream.of()建立Stream,而没必要先建立Array再建立Stream。
IntStream stream = IntStream.of(10, 20, 30, 40, 50); // 不要使用Stream<Integer> Stream<String> colorStream = Stream.of("Red", "Pink", "Purple"); Stream<Person> personStream = Stream.of( new Person("mike", "male", 10), new Person("lucy", "female", 4), new Person("jason", "male", 5) );
不用区分基础数据类型,但参数只能是数组。
int[] intNumbers = {10, 20, 30, 40, 50}; IntStream stream = IntStream.of(intNumbers);
调用parallelStream()或stream().parallel()方法可建立并行Stream。
Stream<Integer> numberStream = Arrays.asList(10, 20, 30, 40, 50).stream();
· 一般用于随机数、元素知足固定规则的Stream,或用于生成海量测试数据的场景。
· 应配合limit()、filter()使用,以控制Stream大小,不然stream长度无限。
Stream.generate(Math::random).limit(10) Stream.generate(() -> (int) (System.nanoTime() % 100)).limit(5)
· 重复对给定种子值(seed)调用指定的函数来建立Stream,其元素为
seed, f(seed), f(f(seed))...
无限循环。· 一般用于随机数、元素知足固定规则的Stream,或用于生成海量测试数据的场景。
· 应配合limit()、filter()使用,以控制Stream大小,不然stream长度无限。
// 按行依次输出:0、五、十、1五、20 Stream.iterate(0, n -> n + 5).limit(5).forEach(System.out::println);
用于IntStream、LongStream,range()不包含尾元素,rangeClosed()包含尾元素。
LongStream longRange = LongStream.range(-100L, 100L); // 生成[-100, 100)区间的元素序列
· 适用于从文本文件中逐行读取数据、遍历文件目录等场景。
· 一般配合try ... with resources语法使用,以安全而简洁地关闭资源。
try (Stream<String> lines = Files.lines(Paths.get("./file.txt"), StandardCharsets.UTF_8)) { // 跳过第一行,输出第2~4共计三行 lines.skip(1).limit(3).forEach(System.out::println); } catch (IOException e){ System.out.println("Oops!"); }
常见的操做能够归类以下:
Intermediate:Stream通过此类操做后,结果仍为Stream
map (mapToInt, flatMap 等)、 filter、 distinct、 sorted、 peek、 limit、 skip、 parallel、 sequential、 unordered
Terminal:Stream里包含的内容按照某种算法汇聚为一个值
forEach、 forEachOrdered、 toArray、 reduce、 collect、 min、 max、 count、 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny、 iterator
基本的Stream用法格式为Stream.Intermediate.Terminal(SIT)
。Java8特性详解 lambda表达式 Stream以图示形式直观描述了这种格式及若干Intermediate操做。
本节主要介绍经常使用操做及代码示例。为便于演示,首先定义以下集合对象:
List<Person> persons = Arrays.asList( new Person("mike", "male", 10).setLocation("China", "Nanjing"), new Person("lucy", "female", 4), new Person("jason", "male", 5).setLocation("China", "Xian") );
只有IntStream、LongStream和DoubleStream支持sum()方法。
// 计算年龄总和:totalAge = 19 int totalAge = persons.stream().mapToInt(Person::getAge).sum(); // 并行计算年龄总和,此处不建议使用reduce(针对复杂的规约操做) persons.stream().parallel().mapToInt(Person::getAge).reduce(0, Integer::sum); // 计算男生年龄总和:totalAge = 15 persons.stream().filter(person -> "male".equals(person.getGender())).mapToInt(Person::getAge).sum();
average()返回OptionalDouble,max()/min()返回OptionalInt或Optional
。
// 计算年龄均值,输出6.333333333333333 persons.stream().mapToInt(Person::getAge).average().ifPresent(System.out::println); // 计算字典序最大的人名,输出mike persons.stream().map(Person::getName).max(String::compareToIgnoreCase).ifPresent(System.out::println);
// 输出每一个学生姓名的大写形式,按行输出:MIKE、LUCY、JASON persons.stream() .map(Person::getName) // 将Person对象映射为String(姓名) .map(String::toUpperCase) // 将姓名转换大写 .forEach(System.out::println); // 按行输出List元素
· collect操做可将Stream元素转换为不一样的数据类型,如字符串、List、Set和Map等。
· Java 8经过Collectors类支持各类内置收集器,以简化collect操做。
// 获得字符串:Colors: Red&Pink&Purple! colorStream.collect(Collectors.joining("&", "Colors: ", "!")); // 获得ArrayList,元素为:Red, Pink, Purple // 注意,Stream转换为数组的格式形如stream.toArray(String[]::new) colorStream.collect(Collectors.toList()); // 获得HashSet,元素为:Red, Pink, Purple colorStream.collect(Collectors.toSet()); // 获得LinkedList,toCollection()用于指定集合类型 colorStream.collect(Collectors.toCollection(LinkedList::new)); // 获得HashMap,{mike=Person{name='mike'}, jason=Person{name='jason'}, lucy=Person{name='lucy'}} personStream.collect(Collectors.toMap(Person::getName, Function.identity()));
collect收集器还提供summingInt()、averagingInt()和summarizingInt()等计算方法。
// 返回流中整数属性求和,即19 persons.stream().collect(Collectors.summingInt(Person::getAge)) // 计算流中Integer属性的平均值,即6.333333333333333 persons.stream().collect(Collectors.averagingInt(Person::getAge)) // 收集流中Integer属性的统计值,即IntSummaryStatistics{count=3, sum=19, min=4, average=6.333333, max=10} persons.stream().collect(Collectors.summarizingInt(Person::getAge))
// 按照年龄升序排序:sortedpersons = [Person{name='lucy'}, Person{name='jason'}, Person{name='mike'}] List<Person> sortedPersons = persons.stream() .sorted(Comparator.comparingInt(Person::getAge)) // 按照年龄排序 .collect(Collectors.toList()); // 汇聚为一个List对象 // 按照姓名长度升序排序,按行输出:mike: 四、lucy: 四、jason: 5 persons.stream() .sorted(Comparator.comparingInt(p -> p.getName().length())) .map(Person::getName) .map(name -> name + ": " + name.length()) .forEach(System.out::println);
// 判断是否存在名为jason的人:existed = true boolean existed = persons.stream() .map(Person::getName) .anyMatch("jason"::equals); // 任意匹配项是否存在
// 将全部人按照性别分组并计数,输出:{female=1, male=2} Map<String, Long> groupBySex = persons.stream().collect(groupingBy(Person::getGender, counting())); System.out.println(groupBySex); // 将全部人按照性别分组并计算各组最大年龄,输出:Person{name='mike'} Map<String, Optional<Person>> groupBySexAge = persons.stream().collect( groupingBy(Person::getGender, maxBy(Comparator.comparingInt(Person::getAge)))); System.out.println(groupBySexAge.get("male").get()); // 将全部人按照性别分组,按行输出:female: lucy、male: mike,jason persons.stream().collect(groupingBy(Person::getGender)) .forEach((k, v) ->System.out.println(k + ": " + v.stream().map(Person::getName) .reduce((x, y) -> x + "," + y).get()));
注意,本例采用import static java.util.stream.Collectors.*;
这种静态导入的方式简化Collectors.groupingBy()
的调用,代码更简洁易读。此外,不推荐示例中forEach()
的用法。
// 计算身高比例分布:agePercentages = [52.63%, 21.05%, 26.32%] List<String> agePercentages = persons.stream() .mapToInt(Person::getAge) // 将Person对象映射为年龄整型值 .mapToDouble(age -> age / (double)totalAge * 100) // 计算年龄比例 .mapToObj(new DecimalFormat("##.00")::format) // DoubleStream -> Stream<String> .map(percentage -> percentage + "%") // 添加百分比后缀 .collect(Collectors.toList()); // 若元素数目较多,可先定义formator = new DecimalFormat("##.00"),再调用mapToObj(formator::format)
flatMap()将Stream中的集合实例内的元素所有拍平铺开,造成一个新的Stream,从而到达合并的效果。
// 传统写法(注意两层循环) private static int countPrefix(List<List<String>> nested, String prefix) { int count = 0; for (List<String> element : nested) { if (element != null) { for (String str : element) { if (str.startsWith(prefix)) { count++; } } } } return count; } // Stream写法 private static int countPrefixWithStream(List<List<String>> nested, String prefix) { return (int) nested.stream() .filter(Objects::nonNull) .flatMap(Collection::stream) .filter(str -> str.startsWith(prefix)) .count(); } List<List<String>> lists = Arrays.asList( Arrays.asList("Jame"), Arrays.asList("Mike", "Jason"), Arrays.asList("Jean", "Lucy", "Beth") ); System.out.println("以J开头的人名数:" + countPrefixWithStream(lists, "J"));
使用Stream时,需注意如下规则:
避免重用Stream。
Java 8 Stream一旦被Terminal操做消费,将不可以再使用,必须为待执行的每一个Terminal操做建立新的Stream链。在实际开发时,将共用的Stream实例定义为成员变量时,尤为容易犯错。
重用Stream将报告stream has already been operated upon or closed
的异常。
若须要屡次调用,可利用Stream Supplier实例来建立已构建全部中间操做的新Stream。例如:
Supplier<Stream<String>> streamSupplier = () -> Stream.of("d2", "a2", "b1", "b3", "c") .filter(s -> s.startsWith("a")); streamSupplier.get().anyMatch(s -> true); // 每次调用get()构造一个新stream streamSupplier.get().noneMatch(s -> true);
注意,anyMatch()
方法接受Predicate
引元,一般无需使用filter
,此处仅为示例方便。
避免建立无限流。
经过iterate或生成器建立Stream时,应配合limit()
使用,以控制Stream大小。
但distinct()
与limit()
共用时,应特别注意去重后元素数目是否知足limit限制。例如:
IntStream.iterate(0, i -> (i + 1) % 2) // 生成0和1的整数序列 .distinct() // 去重后为0和1两个元素 .limit(10) // limit(10)限制得不到知足,从而变成无限流 .forEach(System.out::println);
注意Stream操做顺序,尽量提早经过filter()
等操做下降数据规模。
如下面一段简单的代码为例:
Stream.of("a1", "b2", "c3", "d4", "e5").map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }).filter(s -> { System.out.println("filter: " + s); return s.startsWith("A"); }).forEach(s -> System.out.println("forEach: " + s));
运行输出以下:
map: a1 filter: A1 forEach: A1 map: b2 filter: B2 map: c3 filter: C3 map: d4 filter: D4 map: e5 filter: E5
可见,流中的每一个字符串都被调用5次map()
和filter()
,而forEach()
只调用一次。
再改变操做顺序,将filter()
移到Stream操做链的头部:
Stream.of("a1", "b2", "c3", "d4", "e5").filter(s -> { System.out.println("filter: " + s); return s.startsWith("a"); }).map(s -> { System.out.println("map: " + s); return s.toUpperCase(); }).forEach(s -> System.out.println("forEach: " + s));
运行输出以下:
filter: a1 map: a1 forEach: A1 filter: b2 filter: c3 filter: d4 filter: e5
可见,map()
只被调用一次。虽然Stream惰性计算的特性使得操做顺序并不影响最终结果,但合理地安排顺序能够减小实际执行次数。数据规模较大时,性能会有较明显的提高。
注意Stream操做的反作用。
大多数Stream操做必须是无干扰、无状态的。
“无干扰”是指在流操做的过程当中,不去修改流的底层数据源。例如,遍历流时不能经过添加或删除集合中的元素来修改集合。
“无状态”是指Lambda表达式的结果不能依赖于流管道执行过程当中,可能发生变化的外部做用域的任何可变变量或状态。
如下代码试图在操做流时添加和移出元素,运行时均会抛出java.util.ConcurrentModificationException
异常:
List<String> strings = new ArrayList<>(Arrays.asList("one", "two")); String concatenatedString = strings.stream() // 不要这样作,干扰发生在这里 .peek(s -> strings.add("three")) .reduce((a, b) -> a + " " + b) .get(); List<Integer> list = IntStream.range(0, 10) .boxed() // 流元素装箱为Integer类型 .collect(Collectors.toCollection(ArrayList::new)); list.stream() .peek(list::remove) // 不要这样作,干扰发生在这里 .forEach(System.out::println);
如下代码对并行Stream使用了有状态的Lambda表达式:
Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8}; List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray)); List<Integer> parallelStorage = new ArrayList<>(); //List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>()); listOfIntegers.parallelStream() // 不要这样作,此处使用了有状态的Lambda表达式 .map(e -> { parallelStorage.add(e); return e; }) .forEachOrdered(e -> System.out.print(e + " ")); System.out.println(": 1st"); parallelStorage.stream().forEachOrdered(e -> System.out.print(e + " ")); System.out.println(": 2nd");
运行结果可能出现如下几种:
// 并行执行流时,map()添加元素的顺序和随后的forEachOrdered()元素打印顺序不一样 1 2 3 4 5 6 7 8 : 1st 1 6 3 2 7 8 5 4 : 2nd // 多线程可能同时读取到相同的下标n进行赋值,致使元素数量少于预期(采用synchronizedList可解决该问题) 1 2 3 4 5 6 7 8 : 1st 1 5 8 3 6 : 2nd
《Effective Java 第三版》中指出,不要尝试并行化流管道,除非有充分的理由相信它将保持计算的正确性并提升其速度。 不恰当地并行化流的代价多是程序失败或性能灾难。
避免过分使用Stream,不然可能使代码难以阅读和维护。
常见的问题是Lambda表达式过长,可经过抽取方法等手段,尽可能将Lambda表达式限制在几行以内。