给大忙人看的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、

 

posted on 2016-07-07 10:14  多看多学  阅读(776)  评论(0编辑  收藏  举报

导航