给大忙人看的Java核心技术笔记(8、Stream)
流提供了数据视图,让你可以在比集合更高的概念层上指定操作。使用流只要指定做什么,而不是怎么做。将操作的调度执行留给实现。
要点:
1、迭代器使用了一种明确的遍历策略,同时也阻止了高效的并发执行
2、你可以从集合、数组、生成器或迭代器创建流
3、使用过滤器filter来选择元素,使用map进行元素转换
4、对于转换流的其他操作还包括limit、distinct、sorted
5、要从Stream中获得结果,请使用规约操作(reduction operation),如count、max、min、findFirst、findAny。这些方法中的一些会返回Optional类型值。
6、Optional类型是作为处理null值而提供的一个安全替代者。想要安全地使用它,需要借助于ifPersent和orElse方法。
7、你可以收集集合、数组、字符串或者map中的Stream结果。
8、Collections类的groupingBy方法和partitioningBy方法允许你将流的内容划分成组,然后获得每个组的结果。
9、针对基本数据类型如int、long、double,java提供了专门的流
10、并行流自动将流操作并行化
1、从迭代到stream
1 //统计一本书中所有长单词 2 String contents = new String(Files.readAllBytes(Paths.get("alice.txt")), StandardCharsets.UTF_8);//将文件读入字符串 3 List<String> words = Arrays.asList(contents.split("\PL+"));//拆分成单词 4 int count = 0; 5 for(String w : words){ 6 if(w.length()>12) count++; 7 } 8 9 long count = words.stream().filter(w->w.length()>12).count();
用words.parallelStream()就会使得流类库并行地进行过滤和计数操作。
Stream遵循“做什么,而不是怎么去做”的原则。不要指定那个线程,怎么完成,执行顺序和执行线程都会自动由Stream实现,相比自己定义实现则会放弃了优化的机会
流表面上与集合相似,允许转换和检索数据。然而两者却有明显不同:
1、流不存储元素,他们存储在底层的集合或者按需生成。
2、流操作不改变他们的数据源。例如,filter方法不会从一个新流中删除元素,而是生成一个不包含特定元素的新流。
3、如果可能的话,Stream操作可能是延迟执行的。这以为着直到需要结果的时候,方法才会执行。
流的典型工作流程
1、创建一个stream
2、强初始流转换成其他流的中间操作,可能需要多步操作(filter方法返回另一个流)
3、应用终止操作产生结果。该操作强迫懒惰操作进行执行。在这之后流就不会再应用到了(count方法将流归纳为一个结果)
2、创建Stream
用Collection接口的Stream方法将任何集合转化成Stream。用Stream.of方法将一个数组转化为Stream。
1 Stream<String> words = Stream.of(contents.split("\\PL+"));//splits方法返回String[]
用Arrays.stream(array, from, to)方法将数组的一部分转换成Stream。用Stream.empty方法创建一个空的流。
创建无限Stream的静态方法。
generate方法接受一个无参的函数(Supplier<T>接口的对象)
1 //创建一个常量值的Stream 2 Stream<String> echos = Stream.generate(()->"echo"); 3 //创建一个含有随机数的Stream: 4 Stream<String> randoms = Stream.generate(Math::random);
interate方法,接受一个种子值和一个函数(UnaryOperator<T>接口的对象)并会对之前的值重复应用该函数
1 Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n-> n.add(BigInteger.ONE)); 2 //Pattern类有一个方法可以按照正则表达式对字符串进行分隔 3 Stream<String> words = Pattern.compile("\\PL+").splitAsStream(contents); 4 //静态方法Files.lines返回了一个包含文件中所有行的结果 5 try(Stream<String> lines = Files.lines(path)){ 6 //对lines进行处理 7 }
3、filter、map、flatMap方法
filter转换生成一个匹配一定条件的新流
//将字符串流转换成另一个只包含长单词的流 List<String> words = ...; Stream<String> longWords = word.stream().filter(w->w.length()>12);
filter的参数是一个Predicate<T>对象(从T到boolean的函数)
我们经常需要将一个流中的值进行某种形式的转换,这是用map方法,并且传递给它一个执行转换的函数
//将单词转换成小写 Stream<Stream> lowercaseWords = words.stream().map(String::toLowerCase); //产生每个单词第一个字母的流 Stream<String> firstLetters = words.stream().map(s -> s.substring(0,1));
public static Stream<String> letters(String s){ List<String> result = new ArrayList<>(); for(int i=0;i<s.length();i++){ result.add(s.substring(i,i+1)); } return result.stream(); } //等同于 Stream<Stream<String>> result = words.stream().map(w->letters(w)) //将返回一个包含多个流的流,如果要展开为一个只包含字符串的流则可以使用flatMap方法而不是map方法 Stream<String> flatResult = words.stream().flatMap(w->letters(w)); //在每个单词上调用letters方法,并展开结果
4、提取子流和组合流
stream.limit(n)会返回一个包含n个元素的新流(如果原始流的长度小于n,则会返回原始流)
1 Stream<Double> randoms = Stream.generate(Math::random).limit(100);
stream.skip(n)正好相反,它会丢弃前n个元素。
Stream<String> words = Stream.of(contents.split("\\PL+")).skip(1); //可以使用Stream类中的静态方法concat将两个流连接起来 Stream<String> combined = Stream.concat(letters("Hello"), letters("World")); //生成流["H","e","l"......] //当然第一个流不是无限的,否则第二个流永远没有机会添加到第一个流后面
5、其他流转换
distinct: 对于Stream中包含的元素进行去重操作(去重逻辑依赖元素的equals方法),新生成的Stream中没有重复的元素;
对于Stream排序来说,java 9 提供了多个sorted方法。其中一个实现了Comparable接口的流,一个接受一个Comparator对象
1 //最长的单词会出现在第一个位置 2 Stream<String> longestFirst = words.stream().sorted(Comparator.comparing(String::length).reversed());
peek: 生成一个包含原Stream的所有元素的新Stream,同时会提供一个消费函数(Consumer实例),新Stream每个元素被消费的时候都会执行给定的消费函数;
Object[] powers = Stream.iterate(1.0, p->p*2).peek(e->System.out.println("Fetching" +e)).limit(20).toArray(); //每当检索到一个元素就会调用peek里面的方法,一般用于调试,可以在peek调用的方法中设置断点。
6、简单归约
从流中或的返回值,称为归纳方法(reduction)。规约是终止操作,可以将流归纳为一个可以在程序中使用的非流值。
count:返回流中元素的数目
max,min:返回流中的最大值或最小值。
这些方法会返回一个Optional<T>类型的值,他可能是一个封装类型的值,或这是没有返回值。Optional类型是一种更好的表明缺少返回值的方式。
1 Optional<String> largest = words.max(String::compareToIgnoreCase); 2 System.out.println("largest" +largest.getOrElse(""));
findFirst方法返回非空集合中的第一个值,通常与filter方法结合起来使用。
1 Optional<String> startsWithQ =words.filter(s -> s.startsWith("Q")).findFirst();
如果想找到任何一个匹配的元素,而不必是第一个,则使用findAny方法,该方法在对流进行并行执行时非常有效
如果只想知道流中是否含有匹配元素,则使用anyMatch方法。这个方法接受一个predicate参数,所以不需要使用filter方法
boolean aWordStartsWithQ = words.parallel().anyMatch(s->s.startsWith("Q"));
如果所有元素都跟predicate匹配的话,allMatch方法将返回true,如果没有元素匹配的话,nonematch方法返回true。
两个方法都可以通过并行执行提高速度。
7、Optional类型
Optional<T>对象是一个T类型对象或者空对象的封装。Optional<T>类型是要么指向对象要么为null的T类型引用的安全替代者。
1 //封装的字符串,如果没有的话则为空字符串“” 2 String result = optionalString.orElse(""); 3 //也可以调用代码来计算默认值,函数只有在需要时才会被调用 4 String result = optionalString.orElseGet(()->System.getProperty("user.dir")); 5 //提供了一个可以产生异常对象的方法 6 String result = optionalString.orElseThrow(IllegalStateException::new);
ifPresent方法接受一个函数,如果Optional值存在的话,他会被传递给函数;否则的话,不进行任何处理。
optionalValue.ifPresent(v->Process v);
如果你希望当值存在时将其添加到一个集合中,可以调用:
optionalValue.ifPresent(v->results.add(v)); //或者简单点 optionalValue.ifPresent(results::add);
当调用ifPresent方法时,函数不会返回任何值。如果想对函数结果进行处理,则使用map方法:
Optional<Boolean> added = optionalValue.map(results::add)
add方法的值有3种可能性:封装到Optional中的true或者false;如果optionalValue存在,或者是一个空的Optional值。
创建Optional类型值有两个静态方法:Optional.of(result)和Optional.empty()
1 public static Optional<Double> inverse(Double x){ 2 return x ==0 ? Optional.empty() : Optional.of(1/x); 3 }
ofNullable方法被设计为null值和可选值之间的一座桥梁,obj不为null,Optional.ofNullable(obj),则会返回Optional.of(obj);否则返回Optional.empty()
Stream的flatMap方法,通过展开方法所返回的流,将两个方法组合起来。
假设f返回T,g返回U。则可调用s.f().g()将两个方法组合起来,返回T
如果f返回Optional<T>,g返回Optional<U>,就不能调用s.f().g()
可以调用 Optional<U> result = s.f().flatMap(T::g);
如计算平方根:
public static Optional<Double> squareRoot(Double x){ return x<0? Optional.empty() : Optional.of(Math.sqrt(x)); } //这样可以计算反转值得平方根 Optional<Double> result = inverse(x).flatMap(MyMath::squareRoot); //或者 Optional<Double> result = Optional.of(-4.0).flatMap(Demo::inverse).flatMap(Demo::squareRoot);
8、收集结果
调用iterate方法生成一个能够访问元素的传统迭代器。
调用forEach方法作用于没一个元素,在并行流上forEach方法可以以任意顺序便利元素,但是想按顺序处理需要用forEachOrdered方法。
stream.forEach(System.out::println);
调用roArray获得一个含有流中所有元素的数组。
1 String[] result = stream.toArray(String[]::new);
调用collect方法,传入Collectors类将流放到目标容器中
List<String> result = steam.collect(Collectors.toList()); Set<String> result = stream.collect(Collectors.toSet()); TreeSet<String> result = stream.collect(Collectors.toCollection(TreeSet::new)); //字符串拼接起来 String result = stream.collect(Collectors.joining()); //元素间插入分隔符 String result = stream.collect(Collectors.joining(",")); //如果流包含字符串以外对象,首先将他们转换成字符串 String result = stream.map(Object::toString).collect(Collectors.joining(","));
如果想将流规约为总和、平均值、最大值、最小值,则用summarizing方法
1 //用summarzing(Int|Long|Double)方法返回(Int|Long|Double)SummaryStatistics类型结果 2 IntSummaryStatistics summary = stream.collect(Collectors.summaringInt(String::length)); 3 double averageWordLength = summary.getAverage(); 4 double maxWordLength = summary.getMax();
9、将结果收集到Map中
Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName)); //如果设置Map的值为实际people中的对象 Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Function.identity())); //如果多元素拥有相同的键,会出异常,则提供第三个参数,更具已有的值和新值,来决定键的值。
解决键冲突问题,可以看P270,上网搜索下Collectors.toMap方法。
使用toConcurrentMap方法可以生成一个并发的map。
10、分组和分片
使用groupingBy方法,对Map进行分组
1 Map<String, List<Local>> countryToLocales = locales.collect(Collectors.groupingBy(Locale::getCountry)); 2 //Locale::getCountry是分组的分类函数 3 List<Locale> swissLocales = countryToLocales.get("CH");
※每个语言环境都有一个语言代码(en)和地区码(US)。en_us代表美国英语,en_zh代表中国英语
当分类函数是一个predicate(断言)函数时(返回一个布尔值的函数),流元素被分成两组列表,一组返回true的元素,另一组是返回false的元素。这时用partitioningBy比使用groupingBy更有效率。
//将所有语言环境分为英语和使用其他语言 Map<Boolean, List<Locale>> englishAndOtherLocals = locals.collect(Collections.partitioningBy(l->l.getLanguage().equals("en"))); List<Locale> englishLocales = englishAndOtherLocales.get(true);
调用groupingByConcurrent方法,将会得到一个并发映射。当与并行流一起使用时,可以并发地插入值。
11、下游收集器
groupingBy方法产生一个值为列表的map对象。如果想以某种方式处理这些列表,则提供一个下游(downstream)收集器。
如:想让map中的值是set类型而不是list类型,则可以使用Collectors.toSet方法:
Map<String, Set<Locale>> countryToLocaleSet = locales.collect(groupingBy(Locale::getCountry, toSet())); //Java 8还提供了以下几个收集器用来将分组元素“归约”成数字 //counting会返回所收集元素的总个数。 Map<String, Long> countryToLocaleCounts = locales.collect(groupingBy(Locale::getCountry, counting())); //summing(Int|long|Double)接受一个函数作为参数,然后将函数应用到downstream元素上,并生成他们的求和。 Map<String, Integer> stateToCityPopulation = cities.collect(groupingBy(City::getState, summingInt(City::getPopulation))); //计算每个州下属所有城市的人口数 //maxBy、minBy接受一个比较器,产生downstream元素中的最大,最小值 Map<String, City> stateToLargestCity = cities.collect(groupingBy(City::getState, maxBy(Comparator.comparing(City::getPopulation)))); //产生每个州人口最多的城市 //mapping将函数应用到downstream结果上,但是它需要另一个收集器处理其结果。 Map<String, Optional<String>> stateToLongestCityName = cities.collect(groupingBy(City::getState, mapping(City::getName,maxBy(Comparator.comparing(String::length))))); //这里讲城市按所属州进行分组,每个州内,我们生成每个城市的名称并按照其最大长度进行归约
Mapping方法还未上一节中,获取一个国家所有语言集合的问题提供了一个更好的解决方案
1 Map<String, Set<String>> countryToLanguages = locales.collect(groupingBy(Locale::getDisplayCountry, mapping(Locale::getDisplayLanguage, toSet())));
上一节使用的是toMap而不是groupingBy。在这种形式中,不必担心单独集合的合并问题。如果grouping活着mapping函数的返回类型为int、long、double则你可以将元素收集到一个summary statistics对象中
Map<String, IntSummaryStatistics>stateToCityPopulationSummary = cities.collect(groupingBy(City::getState, summarizingImt(Vity::getPopulation))); //就可以从summary statistic对象中获取函数值得总和,总数、平均、最大小值
※组合收集器功能强大,但是会导致非常复杂的表达式。应该只在通过groupingBy或者partitioningBy来处理“downstream”map值时,才使用它们。其他情况下,只需要对流直接应用map、reduce、count、max、min方法即可
12、归约操作
reduce方法是用来计算流中某个值的一种通用机制,最简单的形式是使用一个二元函数,从前两个元素开始,不断作用到流中的其他元素上。
1 //求和 2 List<Integer> values = ...; 3 Optional<Integer> sum = values.stream().reduce((x,y)->x+y); 4 //可以用reduce(Integer::sum)代替
reduce方法含有一个归约操作P,那么归约操作将生成v0 P v1 P V2.....,其中,vi P vi+1 表示函数调用 p(vi,vi+1)。操作满足结合率,即与你组合元素的顺序无关。
有许多结合操作:sum求和、product乘积、string concatenation字符串拼接、maximum最大值、minimum最小值、set union集合并集、intersection交集。
减法就不是结合操作
通常存在标识e是的 x P (y P z),可以使用该元素作为计算的起点。例如对加法来说起点就是0(标识)。
1 //带标识的reduce 2 List<Integer> values = ...; 3 Integer sum = values.stream().reduce(0 , (x,y)->x+y); 4 //如果流为空则返回标识值0
如果有一个对象流,想得到这些对象上某个属性的和,如求一个流中所有字符串的总长度,就不能用reduce方法的简单形式((T,T)->T)应为参数和返回值类型是一样的,需要提供一个累加器函数(total, word) -> total +word.length()该函数会被重复调用形成累加值。但是当开始并行计算时,会出现多个累加值,需要将他们累加起来。
1 int result = words.reduce(0, (total, word) -> total + word.length(),(total1, total2) ->total1+total2);
在实际中,不会大量地使用聚合方法。简单的方法是映射到一个数字流,使用它的方法进行求和、求最大值、最小值。在上述例子中可以调用words.mapToInt(String::length).sum()。这种方式简单高效,不涉及自动装箱。
有时候reduce方法还不够通用。假如想要将结果收集到一个BitSet(位组)中。如果收集操作是并行的,那么不能直接将元素放进单个BitSet中,因为BitSet对象不是线程安全的。所以不能使用reduce方法。每部分需要从自己的空集合开始,而reduce仅允许提供一个标识值,作为代替,可以使用collect方法,他接受三个参数
1、一个提供者,创建目标类型的实例方法,如HashSet的构造函数
2、一个累加器,将元素添加到目标的方法,如add方法
3、一个合并器,将两个对象合并成一个的方法,如addAll方法。
BitSet result = stream.collect(BitSet::new, BitSet::set, BitSet::or);
13、