java流基础篇

1 java流简介

流是java API的新成员,它允许以声明式方式处理数据集合(通过查询语句来表达,,而不是临时编写一个实现方法)。可以把他们看做遍历数据集的高级迭代器。
java流支持两种类型的操作:中间操作(如 filter 或 map )和终端操作(如 count 、 findFirst 、 forEach 和 reduce )。中间操作可以链接起来,将一个流转换为另一个流。这些操作不会消耗流,其目的是建立一个流水线。与此相反,终端操作会消耗流,以产生一个最终结果,例如返回流中的最大元素。它们通常可以通过优化流水线来缩短计算时间。

1.1 什么是元素序列

就像集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。因为集合是数据结构,所以它的主要目的是以特定的时间/空间复杂度存储和访问元素(如 ArrayList 与 LinkedList )。但流的目的在于表达计算.集合讲的是数据,流讲的是计算。

1.2 什么是源

流会使用一个提供数据的源,如集合、数组或输入/输出资源。 请注意,从有序集合生成流时会保留原有的顺序。由列表生成的流,其元素顺序与列表一致。

1.3 什么是中间操作

诸如 filter 或 sorted 等中间操作会返回另一个流。这让多个操作可以连接起来形成一个查询。重要的是,除非流水线上触发一个终端操作,否则中间操作不会执行任何处理——它们很懒。这是因为中间操作一般都可以合并起来,在终端操作时一次性全部处理。

1.4 什么是终端操作

终端操作会从流的流水线生成结果。其结果是任何不是流的值,比如 List 、Integer ,甚至 void。

1.5 数据处理特点

流的数据处理功能支持类似于数据库的操作,以及函数式编程语言中的常用操作,如 filter 、 map 、 reduce 、 find 、 match 、 sort 等。流操作可以顺序执行,也可并行执行。
流的操作有两个特点,流水线内部迭代
流水线——很多流操作本身会返回一个流,这样多个操作就可以链接起来,形成一个大的流水线。流水线的操作可以看作对数据源进行数据库式查询。

一次遍历

和迭代器类似,流只能遍历一次。遍历完之后,我们就说这个流已经被消费掉了。

内部迭代和外部迭代

使用 Collection 接口需要用户去做迭代(比如用 for-each ),这称为外部迭代。 相反,Streams库使用内部迭代——它帮你把迭代做了,还把得到的流值存在了某个地方,你只要给出一个函数说要干什么就可以了。

1.6 流的使用要素

1.一个数据源(如集合)来执行一个查询;
2.一个中间操作链,形成一条流的流水线;
3.一个终端操作,执行流水线,并能生成结果。

扫描二维码关注公众号,回复: 10246502 查看本文章

1.7 中间操作函数和终端操作函数汇总

操作 类型 返回类型 操作参数 函数描述符 目的
filter 中间 Stream Predicate T -> boolean
map 中间 Stream Function<T, R> T -> R
limit 中间(有状态有界) Stream
sorted 中间 Stream Comparator (T, T) -> int
distinct 中间(有状态无界) Stream
skip 中间(有状态有界) Stream long
flatMap 中间 Stream Function<T, Stream> T -> R
forEach 终端 void Consumer T->void 消费流中的每个元素并对其应用 Lambda。这一操作返回 void
count 终端 long 返回流中元素的个数。这一操作返回 long
collect 终端 R Collector<T, A, R>
anyMatch 终端 boolean Predicate T->boolean
noneMatch 终端 boolean Predicate T->boolean
allMatch 终端 boolean Predicate T->boolean
findAny 终端 Optional
findFirst 终端 Optional
reduce 终端(有状态有界) Optional BinaryOperator T

3 流的操作介绍

3.1筛选和切片操作

1、谓词筛选

Streams 接口支持 filter 方法。该操作会接受一个谓词(一个返回boolean 的函数)作为参数,并返回一个包括所有符合谓词的元素的流。

List<Dish> vegetarianMenu = menu.stream().filter(Dish::isVegetarian).collect(toList());

2、筛选各异的元素

流还支持一个叫作 distinct 的方法,它会返回一个元素各异(根据流所生成元素的hashCode 和 equals 方法实现)的流。

List<Integer> numbers = Arrays.asList(1, 2, 1, 3, 3, 2, 4);
numbers.stream().filter(i -> i % 2 == 0).distinct().forEach(System.out::println);

3、截断流

流支持 limit(n) 方法,该方法会返回一个不超过给定长度的流。所需的长度作为参数传递给 limit 。如果流是有序的,则最多会返回前 n 个元素。
请注意 limit 也可以用在无序流上,比如源是一个 Set 。这种情况下, limit 的结果不会以任何顺序排列

List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).limit(3).collect(toList());

4、跳过元素

流还支持 skip(n) 方法,返回一个扔掉了前 n 个元素的流。如果流中元素不足 n 个,则返回一个空流。请注意, limit(n) 和 skip(n) 是互补的!

List<Dish> dishes = menu.stream().filter(d -> d.getCalories() > 300).skip(2).collect(toList());

3.2 映射操作

1、对流中每一个元素应用函数

流支持 map 方法,它会接受一个函数作为参数。这个函数会被应用到每个元素上,并将其映射成一个新的元素(使用映射一词,是因为它和转换类似,但其中的细微差别在于它是“创建一个新版本”而不是去“修改”)。

List<String> dishNames = menu.stream()
.map(Dish::getName)
.collect(toList());

2、流的扁平化

flatmap 方法让你把一个流中的每个值都换成另一个流,然后把所有的流连接
起来成为一个流。使用 flatMap 方法的效果是,各个数组并不是分别映射成一个流,而是映射成流的内容。所有使用 map(Arrays::stream) 时生成的单个流都被合并起来,即扁平化为一个流。

        String[] arrayOfWords = {"Hello","World"};

        //得到的是string[]的list
        List<String[]> wordList =Arrays.stream(arrayOfWords).map(word -> word.split("")).distinct().collect(Collectors.toList());

        //得到的是所有不同字母的list
        List<String> uniqueCharacters =Arrays.stream(arrayOfWords)
                        .map(w -> w.split(""))
                        .flatMap(Arrays::stream)
                        .distinct()
                        .collect(Collectors.toList());

使用map方式的处理过程
图1 使用map方式的处理过程
使用flatmap方式的处理过程
图2 使用flatmap方式的处理过程

3.3 查找和匹配操作

Stream API通过 allMatch 、 anyMatch 、 noneMatch 、 findFirst 和 findAny 方法提供了元素匹配的工具

1、检查谓词是否至少匹配一个元素

anyMatch 方法可以回答“流中是否有一个元素能匹配给定的谓词”。anyMatch 方法返回一个 boolean ,因此是一个终端操作。

if(menu.stream().anyMatch(Dish::isVegetarian)){
System.out.println("The menu is (somewhat) vegetarian friendly!!");
}

2、检查谓词是否匹配所有元素

allMatch 方法的工作原理和 anyMatch 类似,但它会看看流中的元素是否都能匹配给定的谓词。
和 allMatch 相对的是 noneMatch 。它可以确保流中没有任何元素与给定的谓词匹配。
anyMatch 、 allMatch 和 noneMatch 这三个操作都用到了我们所谓的短路,这就是大家熟悉的Java中 && 和 || 运算符短路在流中的版本

boolean isHealthy = menu.stream().allMatch(d -> d.getCalories() < 1000);

boolean isHealthy = menu.stream().noneMatch(d -> d.getCalories() >= 1000);

3、查找元素

findAny 方法将返回当前流中的任意元素。它可以与其他流操作结合使用。
可以结合使用 filter 和 findAny 方法来实现查询,流水线将在后台进行优化使其只需走一遍,并在利用短路找到结果时立即结束。

Optional<Dish> dish =
menu.stream()
.filter(Dish::isVegetarian)
.findAny();

4、查找第一个元素

有些流有一个出现顺序(encounter order)来指定流中项目出现的逻辑顺序(比如由 List 或排序好的数据列生成的流)。对于这种流,你可能想要找到第一个元素。

List<Integer> someNumbers = Arrays.asList(1, 2, 3, 4, 5);
Optional<Integer> firstSquareDivisibleByThree =
someNumbers.stream()
.map(x -> x * x)
.filter(x -> x % 3 == 0)
.findFirst(); // 9

为什么会同时有 findFirst 和 findAny 呢?答案是并行。找到第一个元素在并行上限制更多。如果你不关心返回的元素是哪个,请使用 findAny ,因为它在使用并行流时限制较少。

3.4 归约操作

需要将流中所有元素反复结合起来,得到一个值,比如一个 Integer 。这样的查询可以被归类为归约操作(将流归约成一个值)。用函数式编程语言的术语来说,这称为折叠(fold),因为你可以将这个操
作看成把一张长长的纸(你的流)反复折叠成一个小方块,而这就是折叠操作的结果。

1、元素求和

//有初始值
int product = numbers.stream().reduce(1, (a, b) -> a * b);
//五初始值
Optional<Integer> sum = numbers.stream().reduce((a, b) -> (a + b));

2、最大值和最小值

Optional<Integer> max = numbers.stream().reduce(Integer::max);

Optional<Integer> min = numbers.stream().reduce(Integer::min);

3.5 数值流

1、原始类型流特化

IntStream 、 DoubleStream 和LongStream ,分别将流中的元素特化为 int 、 long 和 double ,从而避免了暗含的装箱成本。每个接口都带来了进行常用数值归约的新方法,比如对数值流求和的 sum ,找到最大元素的 max 。
此外还有在必要时再把它们转换回对象流的方法。要记住的是,这些特化的原因并不在于流的复杂性,而是装箱造成的复杂性——即类似 int 和 Integer 之间的效率差异。

映射到数值流

将流转换为特化版本的常用方法是 mapToInt 、 mapToDouble 和 mapToLong 。这些方法和前面说的 map 方法的工作方式一样,只是它们返回的是一个特化流,而不是 Stream 。

int calories = menu.stream()
.mapToInt(Dish::getCalories)
.sum();

请注意,如果流是空的, sum 默认返回 0 。 IntStream 还支持其他的方便方法,如
max 、 min 、 average 等

转换会对象流
一旦有了数值流,你可能会想把它转换回非特化流。例如, IntStream 上的操作只能
产 生 原 始 整 数 : IntStream 的 map 操 作 接 受 的 Lambda 必 须 接 受 int 并 返 回 int ( 一 个IntUnaryOperator )。但是你可能想要生成另一类值,比如 Dish 。

IntStream intStream = menu.stream().mapToInt(Dish::getCalories);
Stream<Integer> stream = intStream.boxed();

默认值特化

对于三种原始流特化,也分别有一个 Optional 原始类型特化版本: OptionalInt 、 OptionalDouble 和 OptionalLong。

2、数字范围

两个可以用于 IntStream 和 LongStream 的静态方法,帮助生成数值范围:range 和 rangeClosed 。这两个方法都是第一个参数接受起始值,第二个参数接受结束值。但range 是不包含结束值的,而 rangeClosed 则包含结束值。

IntStream evenNumbers = IntStream.rangeClosed(1, 100).filter(n -> n % 2 == 0);

3.6 构建流

1、由值构建流

你可以使用静态方法 Stream.of ,通过显式值创建一个流。它可以接受任意数量的参数。

Stream<String> stream = Stream.of("Java 8 ", "Lambdas ", "In ", "Action");
stream.map(String::toUpperCase).forEach(System.out::println);

2、由数组构建流

你可以使用静态方法 Arrays.stream 从数组创建一个流。它接受一个数组作为参数。

nt[] numbers = {2, 3, 5, 7, 11, 13};
int sum = Arrays.stream(numbers).sum();

3、由文件生成流

Java中用于处理文件等I/O操作的NIO API(非阻塞 I/O)已更新,以便利用Stream API。java.nio.file.Files 中 的 很多 静态 方 法都 会返 回 一个 流。 例 如, 一个 很 有用 的方 法 是Files.lines ,它会返回一个由指定文件中的各行构成的字符串流。

long uniqueWords = 0;
try(Stream<String> lines =
	Files.lines(Paths.get("data.txt"), Charset.defaultCharset())){
	uniqueWords = lines.flatMap(line -> Arrays.stream(line.split(" ")))
	.distinct()
	.count();

}
catch(IOException e){
}

4、由函数生成流

Stream API提供了两个静态方法来从函数生成流: Stream.iterate 和 Stream.generate 。这两个操作可以创建所谓的无限流:不像从固定集合创建的流那样有固定大小的流。由 iterate和 generate 产生的流会用给定的函数按需创建值,因此可以无穷无尽地计算下去!一般来说,应该使用 limit(n) 来对这种流加以限制,以避免打印无穷多个值。

迭代

iterate 操作基本上是顺序的,因为结果取决于前一次应用。请注意,此操作将生成一个无限流——这个流没有结尾,因为值是按需计算的,可以永远计算下去。我们说这个流是无界的。

//生成了一个所有正偶数的流
Stream.iterate(0, n -> n + 2)
.limit(10)
.forEach(System.out::println);

生成

与 iterate 方法类似, generate 方法也可让你按需生成一个无限流。但 generate 不是依次对每个新生成的值应用函数的。

//将生成一个流,其中有五个0到1之间的随机双精度数
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);

4 流操作:无状态和有状态

诸如 map 或 filter 等操作会从输入流中获取每一个元素,并在输出流中得到0或1个结果。这些操作一般都是无状态的:它们没有内部状态(假设用户提供的Lambda或方法引用没有内部可变状态)。


但诸如 reduce 、 sum 、 max 等操作需要内部状态来累积结果。在上面的情况下,内部状态很小。在我们的例子里就是一个 int 或 double 。不管流中有多少元素要处理,内部状态都是有界的。

相反,诸如 sort 或 distinct 等操作一开始都和 filter 和 map 差不多——都是接受一个流,再生成一个流(中间操作),但有一个关键的区别。从流中排序和删除重复项时都需要知道先前的历史。例如,排序要求所有元素都放入缓冲区后才能给输出流加入一个项目,这一操作的存储要求是无界的。要是流比较大或是无限的,就可能会有问题(把质数流倒序会做什么呢?它应当返回最大的质数,但数学告诉我们它不存在)。我们把这些操作叫作有状态操作。

发布了16 篇原创文章 · 获赞 1 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/a0604030212/article/details/104925553