java核心技术-II-1-Java8的流库

迭代与流

与集合相比,流提供了一种可以让我们在更高的概念级别上指定任务的数据视图。

流提供了几种操作函数,其调用要比一般的迭代更加简洁。并且其可以链式调用,每一次调用都返回一个新的流对象。

如:统计一个List中长度大于12的单词数量。

迭代

1
2
3
4
5
6
int count = 0;
for(String w : words){
if(w.length() > 12){
count++;
}
}

1
long count = words.stream().filter(w -> w.length() > 12).count();

流与集合的差异

  1. 流并不存储其元素。这些元素可能存储在底层的集合中,或者是按需生成的。
  2. 流的操作并不会修改其数据源。如filter方法不会从流中移除元素,二十会生成一个新的流,其中不包含过滤掉的元素。
  3. 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如,如果只是想查找前5个单词而不是所有的单词,那么filter方法会在匹配到第5个单词后就停止过滤。因此,我们甚至可以操作无限流。

流的创建

从集合Collections接口创建流

对于实现了Collection接口的类。可以使用Stream.of方法来获得一个接口。详见java核心技术-I-9-集合

如:

1
2
3
4
5
ArrayList<String> arrayList = new ArrayList<>();
arrayList.add("apple");
arrayList.add("banana");
arrayList.add("pear");
Stream stream = arrayList.stream();

从数组生成Stream

用所有元素生成Stream

如果要用所有的数组元素来生成一个Stream,则可以使用Stream.of

如:

1
2
String[] arr = {"apple", "banana", "pear"}; 
Stream stream1 = Stream.of(arr);

用部分数组元素(连续)生成Stream

如果只需要数组的一部分元素(连续)生成Stream,则可以使用Array.stream(array, from, to)

如:

1
2
String[] arr = {"apple", "banana", "pear"}; 
Stream stream2 = Arrays.stream(arr, 0, 1);

直接从基本元素生成Stream

可以通过Stream.of来将基本元素转变为Stream

如:

1
Stream stream2 = Stream.of("apple", "banana");

创建一个空流

可以通过Stream.empty()来生成一个不包含任何元素的流。

如:

1
Stream<String> silence = Stream.empty();

创建一个无限流

可以通过Stream.generate(FunctionInterface)来获取一个无线流,其来源是参数中的函数式接口。

如-通过一个字符串创建流:

1
Stream<String> echos = Stream.generate(() -> "Echo");

如-通过随机函数来获取一个包含无限随机数的流:

1
Stream<Double> echos = Stream.generate(Math::random);

创建一个无线的序列流

可以通过Stream.iterate(Iterable)来通过迭代器创建一个指定的序列流。

如:

1
Stream<BigInteger> integers = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.ONE));

创建一个长度仅为0或1的流

通过Stream.ofNullable(T t)方法可以创建一个长度仅为0或1的流。当参数t为null时,则长度为0,否则长度为1,即是包含该对象。

注意

值得注意的是,在执行流的操作的时候,我们并没有修改流背后的集合。但是如果手动修改了流的集合(指在流操作外部修改,在流操作,比如forEach中,则会报错),那么流的操作就会变成不可预知的。JDK文档称之为不干涉性。

操作

filter(Predicate<T>)

filter函数会按照参数中的函数式接口来过滤所有元素。然后将符合条件的元素组合为新的流返回。

如-过滤所有长度大于12的单词:

1
2
List<String> words = ...;
Stream<String> longWords = words.stream().filter(w -> w.length() > 12);

注意filter的参数为一个Predicate<T>函数式接口。即从T到Boolean的函数。

map(FunctionInterface)

map(FunctionInterface)会对流中的每个元素应用参数的函数式接口。并且将所有的结果组合为一个新的流返回。

如-截取所有单词的首字母:

1
2
List<String> words = ...;
Stream<String> firstLetter = words.stream().map(s -> s.substring(0, 1));

flatMap(FunctionInterface)

flatMap(FunctionInterface)是对流中的每个元素应用参数的函数式接口(其都返回一个流),然后将每个参数的返回值组合为一个流。

codePoints("boat")返回的流是["b", "o", "a", "t"]

所以Stream<Stream<String>> result = words.stream().map(w -> codePoints(w))会返回一个流的嵌套。[...["a", "b"], ["c", "d"]]

而使用Stream<Stream<String>> result = words.stream().flatMap(w -> codePoints(w))会返回一个所有子流的组合流,即[..., "a", "b", "c", "d"]

stream.limit(n)

stream.limit(n)会截取流中的前n个元素(如果其长度小于n,则会返回流中的全部元素组成的新流)。

这个方法可以用于裁剪无限流。

如-获取100个随机数的流

1
Stream<Double> randoms = Streams.generate(Math::random).limit(100);

stream.skip(n)

stream.skip(n)方法正好相反,其会丢弃前n个元素。

如-跳过单词表中的前5个元素

1
Stream<String> newWords = words.stream().skip(5);

stream.takeWhile(predicate)

stream.takeWhile(predicate)会在谓词为真时获取该元素。

如-收集分割字符串中的所有数字元素

1
Stream<String> initialDigits = codePoints(str).takeWhile(s -> "0123456789".contains(s));

stream.takeWhile(predicate)

takeWhile方法正好相反,他会在条件为真的时候丢弃元素。

如-收集分割字符串中的所有非数字元素

1
Stream<String> initialDigits = codePoints(str).dropWhile(s -> "0123456789".contains(s));

Stream.concat(Stream stream1, Stream stream1)

与字符串一样,conact方法可以将两个流的元素连接在一起。

1
Stream<String> s = Stream.concat(codePoints("hello"), codeoPints("world"));

stream.disinct()

stream.disinct()会将原流中的重复元素剔除之后,返回一个新的无重复元素的流。

如:剔除重复字母

1
2
Stream<String> s = Stream.of("a", "b", "c", "b", "a").disinct();
//["a", "b", "c"]

stream.sorted(comparator)

与其他集合排序一样,流的排序也是接受一个comparator。其会根据这个comparator,返回排序后的元素。

如-将最长的字符串排在前面

1
Stream<String> longsFirst = words.stream().sorted(Comparator.comparing(String::length).reverse());

stream.peek(FunctionInterface)

stream.peek(FunctionInterface)的即预览这个流的元素,但是并不会将其取出。(其本质是生成一个新的流,然后获取新流中的元素,所以原来的流并无变化)然后对这个元素应用参数的函数式接口。

如-打印流中的元素

1
Stream<String> s = Stream.of("a", "b", "c", "b", "a").peek(e -> System.out.println("item-" + e));

简单约简(注意以前方法均返回Option对象,将在下一章介绍)

通过约简可以获得通过前面处理的流的指定结果。约简是一种终结操作,他们会将流约简为可以在程序中使用的非流值。

其有以下API:

stream.max(comparator)

通过比较器返回流中最大的元素。

stream.min(comparator)

通过比较器返回流中最小的元素。

stream.findFirst()

返回流中的第一个元素。如果流为空,则返回空的Option对象。

stream.findAny()

返回流中的任意一个元素。如果流为空,则返回空的Option对象。

stream.anyMatch(predicate)

流中的是否存在一个元素满足给定谓词。

stream.allMatch(predicate)

流中是否所有元素是否都满足给定谓词。

stream.noneMatch(predicate)

流中是否没有元素满足给定谓词。

Optional类型

Optional<T>对象是一种包装器对象,其可能包装了T对象或者没有包装任意对象。其被视为更安全的方式,用来替代类型T的引用。

获取Optional

其有3个方法来获取Opitons的值。

Opions.orElse(T t)

该方法会在options存在值的时候返回该值,否则返回传递的t。

Options.orElseGet(FunctionInterface)

该方法会在options存在值的时候返回该值,否则返回传递的函数参数调用的结果。

Options.orElseThrow(exceptionSupplier)

该方法会在options存在值的时候返回该值,否则抛出传递参数的异常。

消费Optional

除了直接获取optional包装的值,还可以接受一个函数接口,让函数接口直接处理该函数。

optional.ifPresent(Consumer action)

该方法会在值存在的情况下,将其传递给参数函数。否则不会有任何效果。

optional.ifPresentOrElse(Consumer action, Runnable emptyAction)

该方法会在值存在的情况下,将其传递给第一个参数函数;否则会调用第二个空的处理方法。

管道化Optional

管道化即获取optional的值,但不消费。可以使用与管道相似的方法来操作Optional对象,同时返回一个新的0ptional对象。此时可以将Optional对象看为一个长度为0或1的流。

其有以下3个API:

optional.map(FunctionInterface)

通过参数函数处理原optional对象的值。然后返回新的Oprtional对象。

optional.filter(predicate)

若原Optional的值满足给定谓词,就返回一个新的一样值的Optional对象,否则,返回一个空的Optional对象。

Optional.or(supplier)

如果当前Optional对象不为空,则产生当前的Optional,否则由supplier产生一个Optional

flatMap构建Optional值的函数

flatMap会在Optional包装部位空时,产生将mapper应用于当前Optional值所产生的结果,或者在当前Optional为空时,返回一个空Optional

由于Opional<T>是一个包装类型,所以无法直接调用T的相关方法。而可以使用flatMap来直接获取到包装类内部的值。并且调用其方法,然后返回。

如:

类型S有一个f方法来产生一个Optional<T>对象。

类型T有一个g方法来产生一个Optional<U>对象。

为我们无法使用s.f().g()来获得Optional<U>对象。

但是我们可以使用下面的方式来获取该对象:

1
Optional<U> res = s.f().flatMap(T::g);

Optional转换为流

通过optional.stream()可以将Optional值转变为一个长度为0或1的流。

TIPS

  • Optional类型的便利永远都不应该为null;
  • 不要使用Optional类型的域。因为其代价是额外多出来一个对象。在累的内部,使用null表示缺失的域更易于操作。
  • 不要在集合中放置Optional对象,并且不要将他们作为map的key。应该直接收集其中的值。

收集结果

当处理完流之后,通常需要查看其结果。则可以通过以下方法来查看内部元素。

stream.iterator

可以使用stream.iterator来生成一个迭代器,则可以按照普通的迭代器方法来查看内部元素。

stream.forEach(Comsumer action)

可以通过stream.forEach来直接遍历流中的元素。

stream.forEachOrdered(Comsumer action)

使用该方法遍历时,此时会完全按照流中的顺序。但会丧失并行处理的部分甚至全部优势。

stream.toArray(Constructor)

调用此方法会返回一个数组,如果不传递参数,则返回Object[],需要强转。其参数为对应类型的构造函数,此时可以返回对应类型的数组。

如:

1
2
3
Stream<Integer> s = Stream.of(1, 2, 3, 4);
Object[] objArr = s.toArray();
Integer[] intArr = s.toArray(Ingeter::new);

stream.toList()

此方法会返回一个List用于查看内容。

如:

1
2
Stream<Integer> s = Stream.of(1, 2, 3, 4);
List<Integer> strings = stream1.toList();

stream.collect(Collector collector)

可以通过该方法将流中的元素收集到指定的集合中。其中Collector collector一般是通过Collectors.toXXX()或者Collectors.toCollection(XXX:new)来获取。

如:

1-收集到List(与上面的API作用相同)

1
List<String> res = stream.collect(Collections.toList());

2-收集到Set

1
Set<String> res = stream.collect(Collectors.toSet());

3-收集到指定的集合

1
TreeSet<String> res = stream.collect(Collectors.toCollection(TreeSet::new));

收集到字符串

还可以通过stream.collect收集到一个字符串。关键是collector必须是一个Collectors.join()产生的。

例如:

1
2
String res = stream.collect(Collectors.join());			//不加间隔符
String res = stream.collect(Collectors.join(",")); //间隔符为,

如果流中除字符串外还包含其他对象,那么我们需要先将其转化为字符串。

如:

1
String res = stream.map(Object::toString).collect(Collectors.join(", "));

收集到映射表

假设流中的对象是一个Person对象,而我们要将其收集到映射表中,keyPerson.idvaluePerson.name

则可以使用Collectors.toMap()方法。该方法接受两个参数,用来产生映射表的键和值。

如:

1
Map<Integer, String> idToName = people.collect(Collectors.toMap(Person::getId, Person::getName)); 

如果需要获取对象本身,则使用Funciton.identity()。即返回的是流中的类型元素T。

如:

1
Map<Integer, Person> idToPerson = people.collect(Collectors,toMap(Person::getId, Function.identity));

嵌套集合

假如要收集每个国家支持的语言。则我们需要一个Map<String, Set<String>>,此时我们由该如何直接从流中获取呢。

此时的Collectors.map需要3个参数:

  • key值。
  • 指定value的值。
  • 组合set的函数

例如:

1
2
3
4
5
6
7
8
9
10
11
Map<String, Set<String>> countryLang = locales.collect(
Collecotors.toMap(
locale::getDisplayCountry,
l -> Collectors.singleton(l.getDisplayLanguage()),
(a, b) -> {
var union = new HasSet<String>(a);
union.addAll(b);
return union;
}
)
);

如果需要使用TreeMap,则需要将其构造器TreeMap::new指定为第4个参数。

组群和分组

上面嵌套集合的方式可以使用组群和分区更简洁的解决。

Collectors.groupingBy()

如:

1
2
3
Map<String, List<Locale>> countryToLocales = locales.collect(
Collectors.groupingBy(Locale::getCountry)
);

其中函数Locale::getCountry是组群的分类函数。

Collectors.partitioningBy()

当分类函数是一个断言函数(即返回Boolean值的函数),流的元素可能分为两个列表。该函数返回true的元素和其他元素。

如:将locale分成了使用英语和使用其他所有语言的两类:

1
2
3
4
Map<Boolean, List<Locale>> englishAndOther = locales.collect(
Collectors.partitioningBy(l -> l.getLanguage().equals("en"))
);
List<Locale> englishLocales = englishAndOther.get(true);

下游收集器

groupingBy方法会产生一个映射表,其每个值都是一个列表。如果想要以某种方式来处理这些列表,则需要下游收集器。

Collectors.counting()

Collectors.counting()会产生收集到的元素的个数。

如:

1
Map<String, lOng> countryToLocaleCounts = locales.collect(groupBy(Locale::getCountry, Collectors.counting()));

可以对每个国家有多少个local进行计数。

Collectors.summing(Int|Long|Double)()

上面的方法会返回收集到的数据的和。

如:

1
2
3
Map<String, Integer> stateToPopulation = cities.collect(
groupingBy(City::getState, Collectors.summingInt(city::getPopulation));
)

可以计算城市流中每个州的人口总和。

Collectors.maxBy&&Collectors.minBy

以上两个方法会接受一个比较器,并分别产生下游元素中的最大值和最小值。

如:

1
2
3
4
5
6
Map<String, Optional<City>> stateToLargestCity =  cities.collect(
groupingBy(
City::getState,
Collectors.maxBy(Comparator.comparing(City::getPopulation))
)
);

Collectors.collectAndThen

该收集器会在收集器后面添加一个最终处理步骤。例如,我们想要知道有多少种不同的结果,那么就可以将他们收集到一个集合中,然后计算其尺寸:

1
2
3
4
5
Map<Character, Integer> stringCountByStartingLetter = strings.collect(
groupingBy(s -> s.charAt(0),
Collectors.collectAndThen(toSet(), Set::size)
)
);

Collectors.mapping

Collectors.mappingCollectors.collectAndThen相反,他会将参数函数应用于收集到的每个元素,并将其结果传递给下游收集器。

如:按照首字母进行收集,咋子每个分组内部,计算字符串的长度并将其传递给下游收集器:

1
2
3
4
5
6
Map<Character, Set<Integer>> stringLengthByStartingLetter = strings.collect(
groupingBy(
s -> s.charAt(0),
Collectors.mapping(String::length, toSet())
)
);

Collectors.flatMap

此处作用也是一致,可以将每个组结果返回的结果进行铺平。

Collectors.filtering

Collectors.filtering会将过滤器应用到每个组上的元素中,如:

1
2
3
4
5
6
Map<String, Set<City>> largeCitiesByState = cities.collect(
groupingBy(City::getState,
filtering(c -> c.getPopulation() > 5000000),
toSet()
)
)

TIPS

将收集器组合起来是一种很强大的方式,但是它也可能会导致产生非常复杂的表达式。最佳用法是与groupingBypartitioningBy一起处理下游的映射表中的值。否则,应该直接在流上应用诸如mapreducecountmaxmin这样的方式。

约简操作

reduce方法是一种用于从流中计算某个值的通用机制。其有3个重载。

Optional<T> reduce(BinaryOperator<T> accumulator);

其接受一个二元表达式,其第一个操作符为上一次调用该表达式的返回值(首次为第一个元素)。

如:对一个流求和:

1
2
3
List<Integer> values = ...;
Options<Integer> sum = values.reduce((x, y) -> x + y); //v1 + v2 + v3+...
Options<Integer> sum = values.reduce(Intger::sum); //效果一样

T reduce(T identity, BinaryOperator<T> accumulator);

与上面用法相同,只是首次的元素会被替换为identity

1
2
List<Integer> values = ...;
Options<Integer> sum = values.reduce(1, (x, y) -> x + y); //1 + v1 + v2 + v3 +...

<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

这种形式用于当我们需要统计流中元素的某个属性进行约简操作时。其中BiFunction用于累加操作,而accumulator用于合并并行处理的结果。

如:

1
2
3
4
int res = words.reduce(0, 
(total, word) -> total + word.length,
(total1, total2) -> total1 + total2
);

基本类型流

与基本类型的包装类相似,流对于基本类型也有对应的基本类型流:

  • IntStream:Integer, Short, Char, Byte, Boolean
  • LongStream:Long
  • DoubleStream:Double, Float

与对象流一样,我们还可以使用静态的generateiterate方法。

此外,IntStreamLongStream有静态方法rangerangeClosed,可以生成步长为1的整数范围(后者包含上限)。

如:

1
2
IntStream zeroToNinetyNine = IntStream.range(0, 100);	//0-99
IntStream zeroToHundred = IntStream.range(0, 100); //0-100

基本流与对象流的转化

对象流转基本流

可以使用streammapToIntmapToLongmapToDouble来将其转换为基本流,可以将原流中的属性转化为新的基本流。

如:将单词的长度转化为新的IntStream

1
2
Stream<String> words = ...;
IntStream lengths = words.mapToInt(String::length);

基本流转对象流

可以使用boxed方法转化为对象流。

如:

1
Stream<Integer> integers = IntStream.range(0, 100).boxed();

特点

  • toArray方法会返回对应的基本类型数组。(注意:虽然上面提到IntStream可以存储Integer, Short, Char, Byte, Boolean,但是仍然返回的是intt[])。
  • 产生可选结果的方法会返回一个OptionalIntOptionalLongOptionalDouble。这些方法与Optional相似,但是具有getAsIntgetAsLonggetAsDouble方法,而不是get方法。
  • 具有分别返回总和、平均值、最大值和最小值的sum、max和min方法。对象流没有定义这些方法。
  • summaryStatistics方法会产生一个类型为IntSummaryStatisticsLongSummaryStatisticsDoubleSummaryStatistics的对象,他们可以同hi报告流的总和、数量、平均值、最大值和最小值。

并行流

流时并行处理块操作变的容易。整个过程几乎是自动的。

创建并行流

Collection.parallelStream()

可以使用Collection.parallelStream()方法从任何集合中获取一个并行流。

如:

1
Stream<String> parallelWords = words.parallelStream();

stream.parallel()

对于现有的流,可以通过stream.parallel()来获取一个并行流。

如:

1
Stream<String> parallelWords = Stream.of(wordArray).parallel();

相关注意点

只要在终结方法执行时流处于并行模式,所有的中间流操作都将被并行化。

只要是并行就会存在竞态的问题。

如下面的例子就是有问题的:

1
2
3
4
5
6
var shortWords = new int[12];
words.parallelStream().forEach(
s -> {
if(s.length() < 12) shortWords[s.length()]++;
}
);

因为shortWords会被传递到多个线程中,这就会存在竞态的问题,所以不能得到预期的结果。

排序并不排斥高效的并行处理。例如,当计算Stream.map(fun)时,流可以被划分为n部分,它们会被并行的处理。然后结果会按照顺序重新组装起来。

stream.unordered

当然,如果不需要排序的需求,并行操作的性能将会得到很大的提升。此时可以通过stream.unordered方法来标记对顺序没有要求。

例如stream.instinct就会从中获得更高效率,原因是不用考虑哪一个相同的元素将会被保留。

TIPS

  • 并行化会导致大量的开销,只有面对非常大的数据集才划算。
  • 只有在底层的数据源可以被有效地分割为多个部分时,将流并行化才有意义。
  • 并行流使用的线程池可能会因诸如文件I/O或者网络访问这样的操作被阻塞而饿死。

只有面对海量的内存数据和运算密集处理,并行流才会工作最佳。

Powered by Hexo and Hexo-theme-hiker

Copyright © 2019 - 2024 My Wonderland All Rights Reserved.

UV : | PV :