jdk8 stream文档(开发中常用)

引言
在学习Kotlin语言的过程中,有被Kotlin中函数式编程的语法糖甜到,因此学习了Kotlin“本家”,Java相关的函数式编程知识。在学习Lambda表达式时接触到了Stream,通过阅读Java文档,博客等方式学习了相关API并且自己实操后,决定记录自己学习成果,也向不知道Stream的同学介绍一些Stream相关知识。

我也是一名学习者,在文章中如果有说明不正确或是不恰当的地方还请大佬指正,谢谢。

一、什么是Stream
1. Stream在Oracle Java 8 官方文档中的定义
Stream(中文翻译:流)是从Java 8 时起加入的Java,也正是从Java 8 起,Java中正式有了函数式编程、Lambda表达式等概念。

Stream的官方定义如下:

A sequence of elements supporting sequential and parallel aggregate operations.

—— Java 8 官方文档

个人渣翻:一个由一个或多个元素组成的、有顺序的、支持并行聚合的序列。

2. Stream的个人理解
上面文档中的定义翻译成人话就是,Stream不同于数组和List等,它是一个有顺序的长队,你可以对这个长队进行排序,或者是合并两个长队。你还可以把长队中的元素以一种特定的方法同一变为另一种元素。

二、Stream有什么用
对于我自己来说,我很多时候是把Stream作为一个工具来使用。

在不考虑时间成本的情况下,使用Stream的函数式编程相较于传统的过程式编程代码要更简洁,使用更加方便,但是相对的,代码可读性就会变得相对较差。

例如下面这段代码:

@Test
public void test() {
int[] arr = {-5, -3, - 1, 0, 2, 4};
for (int i = 0; i < arr.length; i++) {
arr[i] *= arr[i];
}
Arrays.sort(arr);
for (int i : arr) {
System.out.println(i);
}
}
这段代码的功能是将有序数组中元素做乘方运算并有序输出运算后的元素。

如果我们使用Stream编写同样功能的代码呢?

@Test
public void test() {
int[] arr = {-5, -3, - 1, 0, 2, 4};
Arrays.stream(arr).map(i -> i * i).sorted().forEach(System.out::println);
}
我们可以先不管这段代码中使用的方法是什么意思,我们暂时只需要知道它可以实现和上一段代码一样的效果就可以了。可以看到,除去Junit注解,方法的定义和数组的定义,实现同样的功能,for循环(过程式编程)使用了7行,而使用Stream(函数式编程)只使用了1行。

这还只是比较简单的数组操作,如果换成更加复杂的业务、数组、或是集合,以for循环为代表的过程式编程就会显得更加臃肿、繁杂。使用Stream就可以使我们的代码简洁一些,也就不怎么会出现一个循环几十行的现象。

当然上面所说的一切都是建立在对时间要求不高的前提下,如果在算法比赛或是平时做算法题的过程中,偷懒使用Stream,就看你超不超时就完事儿了。

三、Stream的分类
在Java的官方文档中,我们可以知道,Stream是一个继承了BaseStream接口的泛型接口,其中泛型T是Stream中元素的类型。因此,不存在Stream<int>,Stream<double>这种Stream,但是可以有Stream<Integer>,Stream<Double>。因为Stream<T>流中只能存在对象,所以我喜欢将Stream<T>类型的流叫做对象流(非官方叫法,区别于IO操作中的输入输出对象流)。

public interface Stream<T> extends BaseStream<T,Stream<T>>
—— Java 8 官方文档

那如果我不想把基本类型装箱成包装类,我就是要用int,怎么办呢?Java给提供我们了对应的“特殊”Stream。以int为例,Java提供了专属int的IntStream。从Java官方文档中不难看出,IntStream也继承自BaseStream接口,并且不是一个泛型接口。因此,我喜欢将这个流称为int流(非官方叫法)。当然也有对应的double流,long流,但是没有float流,byte流等其他基本类型的流。基本类型的流只有三种。

public interface IntStream extends BaseStream<Integer,IntStream>
—— Java 8 官方文档

注意:Stream<T>和IntStream(以IntStream为例)虽然可以相互转换,但是IntStream不是Stream<T>,他们都继承自BaseStream,是兄弟关系,不是父子。(以下使用的Stream如不进行指出,则代指两者)

四、常用的Stream创建方法
常用的(我会用的)创建Stream的方式主要有三种,如表1:

表1 Stream的创建方式
创建方式 可以创建的Stream类型
Stream.of() Stream<T>
Arrays.stream IntStream,DoubleStream,LongStream,Stream<T>
集合对象中的stream()方法 Stream<T>
1. Stream.of()方法
我们先看官方文档:

 

—— Java 8 官方文档

在文档中我们可以看出of()是静态方法,可以直接通过Stream<T>类名调用,并且of()的形参可以输入一个或者多个,然后返回一个元素类型为形参类型的Stream<T>。

代码示例如下:

Stream<Integer> stream = Stream.of(1, 2, 3, 4);

Stream<String> stream = Stream.of("aaa", "bbb");
注意:不能使用如下代码创建Stream(除非你想要Stream<int[]>类型的流)。

int[] arr = {-5, -3, - 1, 0, 2, 4};
Stream.of(arr);
使用这种方法创建出来的Stream类型是Stream<int[]>,不是Stream<Integer>。

2. Arrays.stream()
这种方式使用的是Java自带的数组工具类Arrays中的静态方法stream()进行创建。

 

—— Java 8 官方文档

Arrays类中对stream()方法进行了多次覆写,接收不同类型的(包括有对应基本类型流的三种基本类型)数组,返回对应的Stream。输入对象数组返回对象流,输入基本类型数组返回基本类型流。

代码示例:

Stream<String> stringStream = Arrays.stream(new String[]{"aa", "bb", "cc"});
IntStream intStream = Arrays.stream(new int[]{1, 2, 3});
//报错
Arrays.stream(new char[]{'a', 'b', 'c'});
3. 集合对象中的stream()方法
这种方法使用的是一些集合对象中自带的stream()方法将集合对象转换为Stream<T>类型的流。因为大多数集合也都是泛型,所以,使用这种方法只能创建Stream<T>类型的流。

代码示例:

//常见的集合转Stream
List<Integer> list = new ArrayList<>();
Stream<Integer> listStream = list.stream();
Set<Integer> set = new HashSet<>();
Stream<Integer> setStream = set.stream();
Map<String, Integer> map = new HashMap<>();
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
注意:Map类型的集合不能直接转换为Stream<T>,必须先将Map对象转换为EntrySet才能使用 stream()方法进行转换。

五、Stream的常见操作
创建了流对象后,我们就得学会使用它。

!!!在Stream的使用中会大量地使用到Lambda表达式。不知道Lambda表达式的同学可以先学习Lambda表达式。

流的操作非常多,其中比较常用(会用)的操作如下(随缘排序):

表2 常用的Stream通用操作
Stream操作 释义
map() 将流中每一个元素通过指定方式应成为另一个元素
forEach() 遍历流(结束操作)
peek() 遍历流
collect() 将流转换为集合对象(结束操作)
count() 返回流中元素的个数,返回值为long(结束操作)
fliter() 过滤流中不符合条件的元素
skip() 跳过流中的前几个元素
limit() 限制流的长度,超出长度时去掉超出的元素
sorted() 对流中的元素进行排序,可自定义排序规则
distinct() 去除流中相同的元素
max()、min() 求出根据条件得到流中最大(最小)元素,返回一个Optional对象(结束操作)
toArray() 将流对象转换为数组(结束操作)
findFirst() 获取流中的第一个元素,返回一个Optional对象
findAny() 随机获取流中的一个元素,返回一个Optional对象
mapToInt()、mapToDouble()、mapToLong() 将Stream<T>按照映射方法转换为对应的基本类型的流
表3 基本类型流中常用操作
基本类型流操作 释义
boxed() 将基本类型的流包装为Stream<T>类型流
average() 求出流中所有元素的平均数,返回一个DoubleOptional对象(结束操作)
sum() 求出流中所有元素的和,返回值类型由流的类型决定(结束操作)
在这些操作中,标记“结束操作”的方法不会返回Stream对象,也就是说,当前流使用了该方法后就无法通过链式调用的方法对当前流进行操作。并且,调用了结束操作的流无法被再次使用,即流在调用了结束操作后会被关闭。强制执行会报以下错误:

//错误示范
public void test() {
List<Integer> list = new ArrayList<>(){{
add(1);
add(2);
}};
Stream<Integer> stream = list.stream();
stream.max(Integer::compareTo);
List<Integer> collect = stream.collect(Collectors.toList());
}


也就是说,结束操作必须在最后调用。

API 实操记录(标红的是结束操作)
1. map()
在前面表2中,我说是“随缘排序”,但也是夹带了一点私货。我觉得map()可以说是Stream的精髓,会用了map(),其他Stream的方法怎么用、是干什么的基本都写在方法名上了。正是因为map()之于Stream,正如锅巴之于煲仔饭,土豆之于英国,是精髓之所在,所以在表中将其放在了第一位。

Java 8 官方文档中对Stream中map()的描述如下:

<R> Stream<R> map(Function<? super T,? extends R> mapper)
Returns a stream consisting of the results of applying the given function to the elements of this stream.

—— Java 8 官方文档

本人渣翻:返回一个包含了将所有元素应用于给定函数所返回的结果的流。

看不懂也没关系,结合实操,很快就能明白。

观察如下代码:

public void test() {
List<Integer> list = new ArrayList<>(){{
add(1);
add(2);
add(3);
}};
Stream<Integer> integerStream = list.stream();
Stream<String> stringStream = integerStream.map(i -> String.valueOf(i));
}
我们创建了一个{1, 2, 3}的List集合,通过集合自身的stream()方法获得了一个Stream<Integer>类型的integerStream,然后integerStream调用了map()方法,然后就得到了一个Stream<String>类型的stringStream,发生了什么?重点就在map中的Lambda表达式。在这个表达式“i -> String.valueOf(i)”中,"i"代表的就是integerStream中的一个元素,而“String.valueOf()”就是map()执行映射的方法。在调用了map()方法后,流对象会从第一个元素执行执行映射方法,并且把映射方法的返回值作为流中新的元素。

再讲通俗一点就是,“i”代表流中的一个元素,“->”可以理解为“变成”,这个Lambda表达式的意思就是,流中的元素i变成了“->”后面这个方法的返回值。而map()会对流中的每一个元素执行这种操作。

注意:因为map()会将映射方法的返回值作为流中的新元素,因此,被引用的映射方法必须有返回值。

情景实例(因为懒,Student采用内部类的形式,下面可能会多次用到):

你的手中现在有四个学生的名字,使用这四个名字构建一个Student对象流,分数采用随机数,然后再将Student对象流映射为只包含成绩的Double对象流。

class Tests {

private static final class Student {

private String name;
private double score;

Student(String name, double score) {
this.name = name;
this.score = score;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public double getScore() {
return score;
}

public void setScore(double score) {
this.score = score;
}
}

@Test
public void test() {
Stream<String> stringStream = Stream.of("小明", "小红", "小亮", "小刚");
Stream<Student> studentStream = stringStream.map(name -> new Student(name, Math.random() * 100 + 1));
Stream<Double> doubleStream = studentStream.map(student -> student.getScore());
}

}

我们可以采用链式调用的方式将代码精简(以下都将采用链式调用):

Stream<Double> doubleStream = Stream.of("小明", "小红", "小亮", "小刚")
.map(name -> new Student(name, Math.random() * 100 + 1))
.map(student -> student.getScore());
2. forEach()
接收一个Consumer类型的参数,对流中的每一个元素执行对应的操作。

情景实操:

控制台输出四个学生对象组成的流中的每一个元素(Student已有toString()方法)。

//Student类同上
@Test
public void test() {
Stream.of("小明", "小红", "小亮", "小刚")
.map(name -> new Student(name, (int) ((Math.random() * 100 + 1) * 100) / 100.0))
.forEach(student -> System.out.println(student));
}
我们可以使用Lambda的方法引用进一步精简代码(以下都将采用Lambda方法引用):

@Test
public void test() {
Stream.of("小明", "小红", "小亮", "小刚")
.map(name -> new Student(name, (int) ((Math.random() * 100 + 1) * 100) / 100.0))
.forEach(System.out::println);
}
运行结果:

 

3. peek()
效果和forEach()一样,同样是接收一个Consumer类型的参数,都是遍历流。区别就是forEach()会关闭流,是结束操作,而peek()不会,调用peek()后,任然可以调用其他操作。

主要使用场景:debug,修改流中的某一个或某一些元素等。

4. collect()
调用该方法会对流进行包装,返回结果为包装后的集合(List、Map、Set等)。

情景实操:

将映射完成的学生对象流封装为List、Map(其中Map以name作key,score作value)。

@Test
public void test() {
List<Student> list = Stream.of("小明", "小红", "小亮", "小刚")
.map(name -> new Student(name, (int) ((Math.random() * 100 + 1) * 100) / 100.0))
.collect(Collectors.toList());
}
@Test
public void test() {
Map<String, Double> map = Stream.of("小明", "小红", "小亮", "小刚")
.map(name -> new Student(name, (int) ((Math.random() * 100 + 1) * 100) / 100.0))
.collect(Collectors.toMap(Student::getName, Student::getScore));
}
3. count()
真正的人如其名,不做作,无参方法,返回一个long型的值,该值为流的长度或者说是流中元素的个数。

4. fliter()
该方法接收一个Predicate类型的参数,根据条件过滤掉不符合条件的元素。

情景实操:

在学生对象流中过滤掉分数低于80分的学生并输出。

@Test
public void test() {
Stream.of("小明", "小红", "小亮", "小刚")
.map(name -> new Student(name, (int) ((Math.random() * 100 + 1) * 100) / 100.0))
.peek(s -> System.out.println("过滤之前 ====>" + s))
.filter(student -> student.getScore() > 80)
.forEach(s -> System.out.println("过滤之后 ====>" + s));
}
运行结果:

 

从这里我们也可以看出peek()和forEach()的区别。

5. skip()
接收一个long类型的参数n,意为跳过或者说去掉前n个流中的元素。

6. limit()
接收一个long类型的参数n,意为去掉流中第n个元素之后的元素。

7. sorted()
Steam<T>中覆写了两个sorted()方法,一个是无参的,一个接收一个Comparator比较器类型的参数。我们先看无参的官方文档:

Returns a stream consisting of the elements of this stream, sorted according to natural order. If the elements of this stream are not Comparable, a java.lang.ClassCastException may be thrown when the terminal operation is executed.

For ordered streams, the sort is stable. For unordered streams, no stability guarantees are made.

—— Java 8 官方文档

本人渣翻:返回一个包含了所有这个流中的元素,并且这些元素经过自然顺序排序过。如果这些元素不是可以比较的(即没有实现Comparable接口),执行终端操作时可能会抛出一个java.lang.ClassCastException。对于有序流,排序是稳定的;对于无序流,不保证排序是稳定的。

了解了无参的sorted(),我们可以看一下有参的sorted()。它接收的比较器参数其实就是为了给没有实现Comparable接口的这些无法比较的元素一个比较规则,或者覆盖可比较元素的已有比较规则。

代码示例:

使用无参sorted()排序数字:

@Test
public void test() {
Stream.of(3, 1, 2, 4, 0)
.sorted()
.forEach(System.out::println);
}


使用无参sorted排序字符串:

@Test
public void test() {
Stream.of("kaoji", "baozaifan", "chongqinghuoguo", "hongzuilvyinggeer")
.sorted()
.forEach(System.out::println);
}
按照字典序排序,自动调用了String中的compareTo()方法;

 

使用有参sorted(),根据字符串长度排序:

@Test
public void test() {
Stream.of("kaoji", "baozaifan", "chongqinghuoguo", "hongzuilvyinggeer")
.sorted((str1, str2) -> str1.length() - str2.length())
.forEach(System.out::println);
}


8. distinct()
该方法主要用于去重。

如果类中元素的类没有重写equals()方法和hashCode()方法,该方法会比较两个元素的地址,如果地址一样,判断为重复;如果地址不一样,即使对象的内容一样,也判断为不重复。

如果重写了equals()方法和hashCode()方法,则该方法会自动调用equals方法进行判断是否重复。

9. max(),min()
注意两个方法均为有参方法,它们没有无参的覆写方法,接收参数为一个比较器。方法根据提供的比较器求出流中的最大值。

如果你的流中元素是可比较的(即实现了Comparable接口的),并且你没有特殊的比较要求,以Integer类举例,你可以这样使用:

Integer integer = Stream.of(1, 2, 3, 4, 5)
.max(Integer::compareTo).get();
因为max()方法返回值是一个Optional对象,Optional是一个泛型接口,它是对对象的进一步封装,为了防止在一切地方抛出空指针异常,因此我们需要使用Optional接口中的get()方法取出Optional封装的对象。

10. toArray()
这也是一个比较常用的方法,方法的作用显而易见,就是将流转换为一个数组。

特别注意!!!这个方法有在接口内进行了一次覆写,因此有两个toArray()方法,一个无参,一个接收IntFunction类型的参数。

先说无参方法,通过查看Java 8的官方文档我们可以知道,无参的toArray()方法返回的是一个Object[]类型的数组。因此,不管流中的元素是什么类型,最后你得到的都是一个Object[]类型的数组,切记,切记,切记!但是,如果当前流是基本类型流,则无参toArray()方法会直接返回对应类型的数组。例如IntStream的流对象调用无参toArray()方法会返回一个int数组。

再看有参的toArray()方法,我们不用知道IntFunction这个接口是干什么,我们只用知道它能够帮我们把流转换成流中元素对应类型的数组就可以了。因此我们只需要告诉方法我们需要什么类型的数组即可。具体操作如下。切记是“Integer[]::new”,不是“Integer::new”。

Integer[] array = Stream.of(1, 2, 3, 4, 5).toArray(Integer[]::new);
11. mapToInt()、mapToDouble()、mapToLong()
这三个方法的作用基本都一样,因此这里只对mapToInt()进行说明。

方法接收一个ToIntFunction类型的参数,也就是说这个方法需要我们提供一个映射方法,这个映射方法可以将流中的元素映射为int。该返回一个IntStream。

因此,mapToInt()中的映射方法的返回值必须是int。使用如下:

IntStream stream = Stream.of(1, 2, 3, 4, 5).mapToInt(Integer::intValue);
12. boxed()
这是基本类型流(IntStream等)中特有的方法,即对基本类型流中的元素进行装箱,返回对应的包装类流。

这是一个无参方法。具体效果如下:

IntStream stream = Stream.of(1, 2, 3, 4, 5).mapToInt(Integer::intValue);
Stream<Integer> boxedStream = stream.boxed();
13. average()、sum()
这两使用方法都差不多,就放一起讲了。它们都是基本类型流特意有的方法,average()方法是求出流中所有元素的平均数,sum()方法是求出流中所有元素的总和。

average()方法的返回值固定是double类型,sum()的返回值是由基本类型流的类型决定的,IntStream返回int,DoubleStream返回double,LongStream返回long。

两个方法都是无参方法,可以直接调用。

当你Stream熟练后,你就会发现大多数情况下把对象流(Stream<T>)转换为基本类型流基本都是为了使用这俩儿。
————————————————
版权声明:本文为CSDN博主「安達としまむら」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_52279910/article/details/125543330

posted @ 2023-01-30 16:49  志鸿鸣  阅读(569)  评论(0编辑  收藏  举报