引入
代码中循环遍历过滤等操作是经常见到的,在说Stream流之前,先看一个例子。
有一个包含三国时期人物名字的集合,需要做以下操作:
- 筛选出名字小于三个字的人物。
- 筛选出曹姓的人物。
- 打印人物名字。
在JDK8之前我们可能这样写:
public class StreamIntroduction {
private List names;@BeforeEachpublic void init() {
names = new ArrayList<>();
names.add("曹操");
names.add("郭嘉");
names.add("夏侯惇");
names.add("曹丕");
names.add("夏侯霸");
names.add("曹安民");
names.add("司马师");
names.add("曹爽");
}@Testpublic void beforeJdk8() {// 找出名字小于三个字的
List nameLess = new ArrayList<>();for (String name : names) {if (name.length() 3) {
nameLess.add(name);
}
}
List nameCao = new ArrayList<>();// 找出出曹姓的人物for (String name : nameLess) {if (name.startsWith("曹")) {
nameCao.add(name);
}
}// 遍历打印for (String name : nameCao) {
System.out.println(name); // 曹操 曹丕 曹爽
}
}
}
可以很明显的看到,JDK8之前的写法有点臃肿,虽然有点故意夸张了。但是,我们看一下JDK8的Stream流的写法。
@Test
public void dealWithJdk8() {
names.stream()
.filter(name -> name.length() 3)
.filter(name -> name.startsWith("曹"))
.forEach(System.out::println); // 曹操 曹丕 曹爽
}
是不是简单明了很多了,直接链式调用达到我们的需求。
JDK8新引入的Stream流,可以让你以一种声明的方式处理数据。操作集合中的数据可以看成将集合中的数据放在工厂的流水线上,我们对这些数据进行加工,筛选等操作。
Stream流和集合
Java中的集合专注于如何存储数据元素以进行有效访问。而Stream流专注于对数据源中数据元素的聚合,筛选等操作。
集合是存储所有元素的内存中数据结构。Stream流没有存储空间,流按需从数据源中提取元素,并将其传递到操作管道以进行处理。
Stream流是单向的,不能重复使用。假如一个流已经被使用了,再次使用的话就会抛出异常。
以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。
Stream流和IO流没有任何关系。
Stream流的创建
Collection方式
获得Stream流的方式有很多,其中最常用的就是通过Collection集合来获得。
另外获取并行流的方式有两种:
- 调用
parallelStream
方法直接获取并行流。 - 先获得串行流,再使用
parallel
方法转换为并行流。
/**
* Collection获得
*/
@Test
public void getStream06() {
// List获得流
List nameList = Arrays.asList("司马昭", "诸葛恪", "张角");
Stream streamList = nameList.stream();
streamList.forEach(System.out::println); // [司马昭,诸葛恪,张角]// 1.直接通过Collection获得并行流
Stream parallelStream1 = nameList.parallelStream();// 2.先获得普通流,再调用parallel方法转换成并行流
Stream parallelStream2 = nameList.stream().parallel();// Set获得流
Set nameSet = new HashSet<>(nameList);
Stream streamSet = nameSet.stream();
streamSet.forEach(System.out::println); // [司马昭,诸葛恪,张角]
}
Arrays方式
Arrays工具类有个静态方法可以直接获得Stream流,传入一个数组即可。
需要注意的是假如传入的是int,double,long这三个基本数据类型,会直接帮我们转成IntStream,DoubleStream,LongStream。
public static Stream stream(T[] array)
eg.
/**
* 通过数组获得流 Arrays
*/
@Test
public void getStream08() {
String[] names = {"司马昭", "诸葛恪", "张角"};
Stream stringStream = Arrays.stream(names);// 假如我们是普通数据类型呢?只支持int,double,long这三个// 直接帮我们转换成IntStream了int[] idss = {1, 3, 4, 5, 6};
IntStream intStream = Arrays.stream(idss);
}
Stream的of方法
Stream的of有两个重载方法。
public static Stream of(T t)public static Stream of(T... values)
eg.
/**
* 通过Stream的of方法获得
*/
@Test
public void getStream01() {
Stream stream1 = Stream.of("曹植");
Stream stream2 = Stream.of("司马昭", "诸葛恪", "张角");// 由于Stream的of方法的参数是可变参,所以也可以传入数组
String[] names = {"司马昭", "诸葛恪", "张角"};
Stream stream3 = Stream.of(names);
}
基本类型流的range方法
包装基本类型的流IntStream和LongStream有两个range方法可以创建流。
以IntStream的方法为例,range方法和rangeClosed方法的区别是后者的范围包含后面的数字。
IntStream range(int startInclusive, int endExclusive)
IntStream rangeClosed(int startInclusive, int endInclusive)
eg.
/**
* 通过IntStream,LongStream
*/
@Test
public void getStream02() {
IntStream intStream1 = IntStream.range(1, 6);
intStream1.forEach(System.out::print); // [1,2,3,4,5]
IntStream intStream2 = IntStream.rangeClosed(1, 6);
intStream2.forEach(System.out::print); // [1,2,3,4,5,6]
LongStream longStream1 = LongStream.range(1, 6);
longStream1.forEach(System.out::print); // [1,2,3,4,5]
LongStream longStream2 = LongStream.rangeClosed(1, 6);
longStream2.forEach(System.out::print); // [1,2,3,4,5,6]
}
Stream的empty方法
Stream的empty方法可以创建一个空的Stream流。
/**
* IntStream,LongStream,DoubleStream,Stream的empty方法
*/
@Test
public void getStream03() {
Stream stream = Stream.empty();
stream.forEach(System.out::print); // []
IntStream intStream = IntStream.empty();
LongStream longStream = LongStream.empty();
DoubleStream doubleStream = DoubleStream.empty();
}
Stream的builder方法
/**
* IntStream,LongStream,DoubleStream,Stream的builder方法
*/
@Test
public void getStream04() {
Stream stream = Stream.builder()
.add("司马师")
.add("潘凤")
.add("郭嘉")
.build();
stream.forEach(System.out::print); // [司马师,潘凤,郭嘉]
}
创建无限连续的流
Stream类两个方法可以创建无限连续的流,它们分别是下面两种:
Stream iterate(final T seed, final UnaryOperator f)
iterate方法可以创建无限连续有序流。它的调用方式其实是seed
,f(seed)
,f(f(seed))
, f(f(f(seed)))
....
其中iterate方法的UnaryOperator类型的参数其实就是一个转换的函数式接口,继承Function的。
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static UnaryOperator identity() {return t -> t;
}
}
Stream generate(Supplier s)
generate方法可以创建无限连续的无序流,传入一个Supplier对象后续用limit函数限制个数即可。
例子:
/**
* iterate 创建无限连续有序流
* generate 创建无限连续的无序流
*/
@Test
public void getStream05() {
Stream iterateStream =
Stream.iterate(1, count -> ++count)
.limit(5);
iterateStream.forEach(System.out::print); // [1,2,3,4,5]
Stream generateStream =
Stream.generate(UUID::randomUUID).limit(2);/*
* 4391d6c8-faaf-435e-a2ae-9f8cf0f70097
* 555c7e14-2a2c-4173-acfe-a25505fe2d45
*/
generateStream.forEach(System.out::println);
}
获得Map相关的Stream流
获得Map的K-V/Entry的Stream流。
/**
* Map获得key,value,entry的流
*/
@Test
public void getStream07() {
Map map = new HashMap();
Stream keyStream = map.keySet().stream();
Stream valueStream = map.values().stream();
Stream entryStream = map.entrySet().stream();
}
通过字符串获得
String有个方法可以获得字符串每个字符的Stream流。
public default IntStream chars()
正则相关的Pattern类,有个方法可以按照正则表达式从字符串中获得Stream流。
public Stream splitAsStream(final CharSequence input)
eg.
/**
* String
*/
@Test
public void getStream09() {
String charStr = "ABCDabcd";
IntStream chars = charStr.chars();
String namesStr = "司马昭,诸葛恪,张角";
Stream stringStream = Pattern.compile(",").splitAsStream(namesStr);
}
通过文件获得
Java 8中的java.io和java.nio.file软件包添加了许多方法来支持使用流的I / O操作。
我们可以从文件中读取文本作为字符串流,流中的每个元素代表一行文本。
/**
* files 通过文件获得
*/
@Test
public void getStream10() {
Path path = Paths.get("src/main/resources/unHappy.txt");
try(Stream lineStream = Files.lines(path)){
lineStream.forEach(System.out::println);
}catch (Exception e){
e.printStackTrace();
}
}
unHappy.txt文件如下,控制台打印的也是下面的样子,因为流中的每个元素代表一行文本。
我不
想加班
又没钱
Stream流的基本操作
Stream类的方法可以分为两类:
- 非终结方法:返回值是Stream类型的方法,支持链式调用。
- 终结方法:返回值不是Stream类型的方法,不支持链式调用。如
forEach
方法。
Stream的注意事项:
- Stream流只能操作一次,重复使用会报错。
- Stream方法返回的是新的流。
- 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
- Stream不调用终结方法,中间的操作不会执行。
forEach
终结方法forEach:消费方法,传入一个Consumer类型的对象消费Stream流中的元素。
void forEach(Consumer super T> action);
eg.
@Test
public void forEachDemo(){
List names = Arrays.asList("程昱", "荀彧", "郭嘉");
names.stream()
.forEach(System.out::println);
}
控制台:
程昱
荀彧
郭嘉
forEachOrder
终结方法forEachOrdered,该方法和上面的forEach使用方式都一样。
void forEachOrdered(Consumer super T> action);
其实两者完成的功能类似,主要区别在并行处理上,forEach是并行处理的,forEachOrder是按顺序处理的,显然前者速度更快。下面给个例子:
@Test
public void forEachOrderedDemo(){
List names = Arrays.asList("程昱", "荀彧", "郭嘉");
names.parallelStream()
.forEach(System.out::println);
names.parallelStream()
.forEachOrdered(System.out::println);
}
- forEachOrdered输出的"荀彧,郭嘉,程昱"按顺序来的。
- forEach的输出顺序并不是确定的,是随机的。
count
终结方法count,返回流中元素个数。
@Test
public void countDemo() {
List names = Arrays.asList("程昱", "荀彧", "郭嘉");long count = names.stream()
.count();
System.out.println("流中元素个数为:" + count);
}
控制台
流中元素个数为:3
filter
filter过滤方法,通过Predicate对象对Stream流中的元素进行过滤。
Stream filter(Predicate super T> predicate)
eg.
@Test
public void filterDemo() {
List names = Arrays.asList("程昱", "荀彧", "郭嘉","郭敬明");
names.stream()
.filter(name -> name.startsWith("郭"))
.forEach(System.out::println);
}
limit
limit方法,需要传入一个整数类型的值,索引大于此值的元素直接丢弃。
Stream limit(long maxSize);
eg.
@Test
public void limitDemo() {
List names = Arrays.asList("程昱", "荀彧", "郭嘉","郭敬明");
names.stream()
.limit(3)
.forEach(System.out::println);
}
控制台:
程昱
荀彧
郭嘉
skip
skip方法,需要传入一个整数类型的值,跳过前面指定个数的元素。
Stream skip(long n);
eg.
@Test
public void skipDemo() {
List names = Arrays.asList("程昱", "荀彧", "郭嘉","郭敬明");
names.stream()
.skip(3)
.forEach(System.out::println);
}
sorted
Stream的sorted方法有两个重载的方法,一个是按照自然循序排序,一个可以根据我们传入的Compartor接口来排序。
Stream sorted();
Stream sorted(Comparator super T> comparator);
eg.
/**
* sorted方法
* 可以根据元素的自然顺序排序
* 也可以根据比较器指定的规则排序
*/
@Test
public void sortedDemo() {
List scores = Arrays.asList(50,90,40,65,35);
scores.stream()
.sorted() // 根据自然顺序排序
.forEach(System.out::println);
System.out.println("========分隔=======");
List persons = new ArrayList<>();
persons.add(new Person("汪胖子",190));
persons.add(new Person("狗东",100));
persons.add(new Person("郭帅",120));// 根据Person类的体重排序
persons.stream()
.sorted(Comparator.comparingInt(Person::getWeight))
.forEach(System.out::println);
}
控制台
35
40
50
65
90
========分隔=======
Person{name='狗东', weight=100}
Person{name='郭帅', weight=120}
Person{name='汪胖子', weight=190}
distinct
Stream的distinct方法可以对流中的元素进行去重操作,在对自定义类去重时需要重写hashcode和equals方法。
@Test
public void distinctDemo(){
List scores = Arrays.asList(50,40,40,60,35);
scores.stream()
.distinct()
.forEach(System.out::println);
System.out.println("========分隔=======");// 自定义对象去重,需要重写hashcode和equals方法
List persons = new ArrayList<>();
persons.add(new Person("汪胖子",190));
persons.add(new Person("狗东",100));
persons.add(new Person("郭帅",120));
persons.add(new Person("郭帅",120));
persons.stream()
.distinct()
.forEach(System.out::println);
}
控制台
50
40
60
35
========分隔=======
Person{name='汪胖子', weight=190}
Person{name='狗东', weight=100}
Person{name='郭帅', weight=120}
match
针对流中所有数据是否满足某个条件
// 所有元素都满足条件才返回true
boolean allMatch(Predicate super T> predicate);
// 所有的元素都不满足条件才返回true
boolean noneMatch(Predicate super T> predicate);
// 只要有一个元素满足就返回true
boolean anyMatch(Predicate super T> predicate);
eg.
@Test
public void matchDemo(){
List nums = Arrays.asList(3, 3, 6, 1, 7);// allMatch:需要所有元素都满足才返回trueboolean allMatchBoolean = nums.stream()
.allMatch(num -> num > 4); // false// anyMatch:只要有一个元素满足就返回trueboolean anyMatchBoolean = nums.stream()
.anyMatch(num -> num > 4); // true// noneMatch:需要所有的元素都不满足条件才返回trueboolean noneMatchBoolean = nums.stream()
.noneMatch(num -> num > 4); // false
}
find
在流中找到一个元素,可以配合条件过滤。Stream流中提供两个方法
// 找到流中的第一个元素
Optional findFirst();
// 获取流中的一个元素
Optional findAny();
第二个方法和findFirst()
的区别是,在数据量较少,串行的情况下,一般会返回第一个结果,如果是并行的情况,那就不能确保是第一个,会随机返回一个元素,这是为了提高并行操作的性能的。
@Test
public void findDemo() {
List nums = Arrays.asList(9, 3, 6, 1, 7);
Optional first = nums.stream()
.filter(num -> num 5)
.findFirst(); // findFirst方法
Integer num = first.get();
System.out.println("第一个小于5的元素为:" + num); // 3// findAny方法在数据量较少,串行地情况下,一般会返回第一个结果// 如果是并行的情况,那就不能确保是第一个
Optional one = nums.stream()
.filter(num1 -> num1 > 5)
.findAny(); // findAny方法
Integer num1 = one.get();
System.out.println("普通流第一个大于5的元素为:" + num1); // 9
Optional parallelOne = nums.parallelStream()
.filter(num2 -> num2 > 5)
.findAny(); // findAny方法
Integer num2 = parallelOne.get();
System.out.println("并行流第一个大于5的元素为:" + num2); // 6
}
控制台:
第一个小于5的元素为:3
普通流第一个大于5的元素为:9
并行流第一个大于5的元素为:6
max/min
Stream提供了获取流中最大值和最小值的方法。
Optional max(Comparator super T> comparator);
Optional min(Comparator super T> comparator);
eg.
@Test
public void maxMinDemo() {
List nums = Arrays.asList(9, 3, 6, 1, 7);
Optional max =
nums.stream().max(Comparator.comparingInt(num -> num));
System.out.println("流中的最大值为:" + max.get());
Optional min =
nums.stream().min(Comparator.comparingInt(num -> num));
System.out.println("流中的最小值为:" + min.get());
}
控制台:
流中的最大值为:9
流中的最小值为:1
reduce
此reduce方法可以对流中的所有数据进行归纳操作,归纳为一个数。
重载一:
T reduce(T identity, BinaryOperator accumulator);
这个方法其实和下面的代码等价
T result = identity;
for (T element : this stream)
result = accumulator.apply(result, element);
return result;
例如,我们可以做求和运算。
@Test
public void reduceDemo01(){
// 可以使用reduce方法进行求和运算
List nums = Arrays.asList(9, 3, 6, 1, 7);
Integer sum = nums.stream()
.reduce(0, (a, b) -> a + b);
System.out.println(sum); // 26
}
重载二:
Optional reduce(BinaryOperator accumulator);
这个方法和下面的代码等价,很简单,和上面的reduce方法的区别就是起始值是第一个元素。
boolean foundAny = false;
T result = null;
for (T element : this stream) {
if (!foundAny) {
foundAny = true;
result = element;
}
else
result = accumulator.apply(result, element);
}
return foundAny ? Optional.of(result) : Optional.empty();
重载三:
U reduce(U identity,
BiFunctionsuper T, U> accumulator,
BinaryOperator combiner);
分析三个参数:
- 第一个参数identity:一个初始化的值这个初始化的值其类型是泛型U,与Reduce方法返回的类型一致,此时Stream中元素的类型是T,与U可以不一样也可以一样。
- 第二个参数accumulator:BiFunction类型,输入是U与T两个类型的数据,而返回的是U类型。
- 第三个参数combiner:使用在并行计算的场景下,如果Stream是串行流时,第三个参数实际上是不生效的。也就是说在串行的情况下随便写都可以,主要是为了编译通过。
这个reduce方法,提供一个不同于Stream中数据类型的初始值,通过累加器规则迭代计算Stream中的数据,最终得到一个同初始值同类型的结果。当然U类型和T类型也可以是一样的。等价于:
U result = identity;
for (T element : this stream)
result = accumulator.apply(result, element);
return result;
看一个例子:
@Test
public void reduceDemo02() {
// 例子: 计算所有人名字的总长度
List names = Arrays.asList("张辽", "徐晃", "司马师");// 我初始值给的是0,是整数类型,并不是集合的String类型// 但是最终返回的是整数类型
Integer sum = names.stream()
.reduce(0,
(length, name) -> length + name.length(),
(a, b) -> null);
System.out.println("名字的总长度为6:" + sum);
}
需要注意的是,在上面的例子中的Stream流是串行流,并不是并行流,所以第三个参数其实是无效的,上面第三个参数combiner写的是(a, b) -> null
,只是为了通过编译。
并行流的reduce操作是并发进行的,为了避免竞争 ,每个reduce线程都会有独立的计算结果,combiner的作用在于合并每个线程的结果得到最终结果。并行流运行时内部使用了fork-join
框架,可以自己去看看。
好,那就看看第三个参数有什么作用。继续看一个例子:
使串行流:
@Test
public void reduceDemo03() {
List nums = Arrays.asList(5, 4, 3, 2, 1);
Integer calc = nums.stream()
.reduce(1,
(a, b) -> {
System.out.println("BiFunction a = " + a
+ ", b = " + b
+ " , a + b = " + (a + b));return a + b;
},
(a, b) -> {
System.out.println("BinaryOperator a = " + a
+ ", b = " + b
+ " , a + b = " + (a + b));return a + b;
});
System.out.println("计算结果为:" + calc);
}
计算结果是20,打印下计算流程:其实就是(1+5)+4+3+2+1)=16
BiFunction a = 1, b = 5 , a + b = 6
BiFunction a = 6, b = 4 , a + b = 10
BiFunction a = 10, b = 3 , a + b = 13
BiFunction a = 13, b = 2 , a + b = 15
BiFunction a = 15, b = 1 , a + b = 16
计算结果为:16
使用并行流:
@Test
public void reduceDemo04() {
List nums = Arrays.asList(5, 4, 3, 2, 1);
Integer calc = nums.parallelStream()
.reduce(1,
(a, b) -> {
System.out.println("BiFunction a = " + a
+ ", b = " + b
+ " , a + b = " + (a + b));return a + b;
},
(a, b) -> {
System.out.println("BinaryOperator a = " + a
+ ", b = " + b
+ " , a + b = " + (a + b));return a + b;
});
System.out.println("计算结果为:" + calc);
}
计算结果是20,打印下计算流程:其实就是(1+5)+(1+4)+(1+3)+(1+2)+(1+1)=20
BiFunction a = 1, b = 3 , a + b = 4
BiFunction a = 1, b = 4 , a + b = 5
BiFunction a = 1, b = 1 , a + b = 2
BiFunction a = 1, b = 5 , a + b = 6
BiFunction a = 1, b = 2 , a + b = 3
BinaryOperator a = 6, b = 5 , a + b = 11
BinaryOperator a = 3, b = 2 , a + b = 5
BinaryOperator a = 4, b = 5 , a + b = 9
BinaryOperator a = 11, b = 9 , a + b = 20
计算结果为:20
那么问题来了,并行和普通流的计算结果不同,是不是这个设计就没意义了呢?设计是没问题的,是我们使用方式的问题。
API中有下面的话,
/*
*
The {@code identity} value must be an identity for the combiner
* function. This means that for all {@code u}, {@code combiner(identity, u)}
* is equal to {@code u}. Additionally, the {@code combiner} function
* must be compatible with the {@code accumulator} function; for all
* {@code u} and {@code t}, the following must hold:
*
{@code
* combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
* }
*/
对于所有的u
和combiner(identity, u)
一定相等,即identity 的值等于合并运算combiner(identity, u)
,对于上面的例子,combiner
方法的方法体是identity+u
,要使identity + u == u
,我们只需要将identity
设置为0
即可。
而且,对于combiner
方法和accumulator
方法,需要满足下面条件
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(u, t)
这到底是什么意思呢?假如流中有三个元素需要进行reduce操作,假如前面两个元素已经计算完毕,即将对第三个元素进行计算。这里分串行和并行进行分析。
串行时:
并行时:
在串行时,最终的结果是通过 accumulator.(apply(r, t))
算出。
在并行时,是通过combiner.apply(u, accumlator.apply(identity, t))
正是因为这样,需要保证串行和并行的时候计算的值相同,则需要使得上面两个式子等效,即
combiner.apply(u, accumulator.apply(identity, t)) == accumulator.apply(r, t)
map
map方法通过传入一个Function转换函数,返回一个新得流。
Stream map(Function super T, ? extends R> mapper);
eg.
@Test
public void mapDemo(){
// 将persons里的Person对象的体重转换出一个新的流
List persons = new ArrayList<>();
persons.add(new Person("汪胖子", 190));
persons.add(new Person("狗东", 100));
persons.add(new Person("郭帅", 120));
persons.stream()
.map(person -> person.getWeight())
.forEach(System.out::println);
}
map还可以和reduce方法结合灵活使用
@Test
public void mapDemo02() {
// 计算出所有人体重的和
List persons = new ArrayList<>();
persons.add(new Person("汪胖子", 190));
persons.add(new Person("狗东", 100));
persons.add(new Person("郭帅", 120));
Integer sum = persons.stream()
.map(person -> person.getWeight())
.reduce(0, (x, y) -> x + y);
System.out.println("所有人体重之和为:" + sum); // 410
}
还有其他的map操作,可以将Integer等类型的流转换成基本数据类型的流。这样可以节省内存,减少自动装箱和自动拆箱。
IntStream mapToInt(ToIntFunction super T> mapper);
LongStream mapToLong(ToLongFunction super T> mapper);
DoubleStream mapToDouble(ToDoubleFunction super T> mapper);
flatMap
flatMap方法可以将一个流中的数据转换成更细的流,再将这些流合并成一个流。方法声明:
Stream flatMap(Function super T, ? extends Stream extends R>> mapper);
eg.
@Test
public void flatMapDemo(){
List words = Arrays.asList("hello,world", "hola,mundo", "你好,世界");
words.stream()
.flatMap(word -> Arrays.stream(word.split(",")))
.forEach(System.out::println);
}
控制台:
hello
world
hola
mundo
你好
世界
类似map方法,flatMap也有转换基本数据类型的方法
IntStream flatMapToInt(Function super T, ? extends IntStream> mapper);
LongStream flatMapToLong(Function super T, ? extends LongStream> mapper);
DoubleStream flatMapToDouble(Function super T, ? extends DoubleStream> mapper);
map和flatMap配合使用:
map方法声明
Stream map(Function super T, ? extends R> mapper);
可以很明显的看到flatMap方法和map方法不同的地方就是参数的泛型不同。
map方法的Function函数的返回值是R
类型,map只是提取属性放入流中。
而flatMap方法的Function函数的返回值是Stream extends R>
类型。flatMap 先提取属性放入一个比较小的流,然后再将所有的流合并为一个流。叠罗汉。
map和flatMap结合使用:需要计算学校中每个人的体重总和
@Test
public void flatMapAndMapDemo() {
// 班级1
List classOne = new ArrayList<>();
classOne.add(new Person("玉泽", 120));
classOne.add(new Person("海露", 95));// 班级2
List classTwo = new ArrayList<>();
classTwo.add(new Person("今译", 130));
classTwo.add(new Person("导读", 110));// 学校
List> school = new ArrayList<>();
school.add(classOne);
school.add(classTwo);// 数据准备好了,现在获取所有人的体重之和
Integer sum = school.stream()
.flatMap(className -> className.stream())
.map(Person::getWeight)
.reduce(0, Integer::sum);
System.out.println("学校所有人的体重和为:" + sum); // 455
}
concat
concat方法的 作用就是将两个流合并成一个流。它是一个静态方法。
static Stream concat(Stream extends T> a,
Stream extends T> b)
eg.
@Test
public void concatDemo(){
Stream streamA = Stream.of("田七");
Stream streamB = Stream.of("王八");// 合并流
Stream concat = Stream.concat(streamA, streamB);
}
peek
peek方法可以用来调试Stream流,可以看到每次操作后流中的数据。
Stream peek(Consumer super T> action);
eg.
@Test
public void peekDemo() {
Stream stream = Stream.of("太史慈","陆逊","吕蒙","诸葛子瑜","甘宁");long count = stream.filter(name -> name.length() > 2)
.peek(System.out::println) // 打印名字长度大于2的名字
.map(String::length)
.peek(System.out::println) // 打印名字长度
.map(length -> length * length)
.peek(System.out::println) // 打印名字长度的平方
.count();
}
控制台:
太史慈
3
9
诸葛子瑜
4
16
Stream流的收集操作
Stream流基本收集操作
Stream流中的元素可以收集到指定的集合中,我们可以直接调用方法收集到指定的List、Set等集合中,进而继续后面的业务操作。
**API中已经给我们提供了直接收集到List、Set、Map和ConcurrentHashMap的方法,也可以指定其他的集合类型。**具体可以去源码查看方法声明。
收集为List集合:
@Test
public void collect01() {
Stream stream = Stream.of("初", "九", "潜", "龙", "勿", "用");
List stringList = stream.collect(Collectors.toList());
System.out.println("收集后的list集合:" + stringList);
}
收集为Map集合:
@Test
public void collect03() {
Stream stream = Stream.of("九二", "现龙在田","利见大人");// 转换为Map集合,以字符串为key,字符串长度为value
Map map =
stream.collect(Collectors.toMap(str -> str, String::length));
System.out.println("收集后的map集合:" + map);
}
收集为自定义集合:
@Test
public void collect04() {
Stream stream = Stream.of("九三", "君子终日乾乾","夕惕若","厉无咎");
LinkedHashSet stringSet =
stream.collect(Collectors.toCollection(LinkedHashSet::new));
System.out.println("收集后的LinkedHashSet集合:" + stringSet);
}
收集为数组:
收集为数组的时候,可以指定数组的类型。
/**
* 收集为数组 没有指定类型
*/
@Test
public void collect05() {
Stream stream = Stream.of("九四", "或跃在渊", "无咎");
Object[] objects = stream.toArray();
System.out.println("收集后的Object类型数组:" + Arrays.toString(objects));
}/**
* 收集为数组 转换指定类型数组
*/@Testpublic void collect06() {
Stream stream = Stream.of("九五", "飞龙在天", "利见大人");
String[] strings = stream.toArray(String[]::new);
System.out.println("收集后的String类型数组:" + Arrays.toString(strings));
}
Stream流的聚合操作
我们可以对Stream流中的某些字段像MySQL一样进行聚合操作,例如取最值,平均值等。
在对Stream流操作完后调用collect方法进行聚合操作,需要传入Collector对象,我们可以通过Collectors类的静态方法完成聚合操作。方法如下:
// 统计元素个数
public static Collector counting()// 最小值public static Collector> minBy(Comparator super T> comparator)// 最大值public static Collector> maxBy(Comparator super T> comparator)// 求和public static Collector summingInt(ToIntFunction super T> mapper) public static Collector summingLong(ToLongFunction super T> mapper) public static Collector summingDouble(ToDoubleFunction super T> mapper) // 平均值public static Collector averagingInt(ToIntFunction super T> mapper) public static Collector averagingLong(ToLongFunction super T> mapper) public static Collector averagingDouble(ToDoubleFunction super T> mapper)
先初始化一个流,Person是一个对象,属性为名字和体重。
private Stream stream;/**
* 初始化
*/@BeforeEachpublic void init() {
stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 105),new Person("杨文浡", 120)
);
}
最值
/**
* 聚合操作-最大
*/
@Test
public void aggregationMax() {
// 获取体重最大的对象
Optional maxWeightPerson =
stream.collect(Collectors.maxBy(Comparator.comparingInt(Person::getWeight)));
System.out.println("最大:" + maxWeightPerson.get());
}/**
* 聚合操作-最小
*/@Testpublic void aggregationMin() {// 获取体重最小的对象
Optional minWeightPerson =
stream.collect(Collectors.minBy(Comparator.comparingInt(Person::getWeight)));
System.out.println("最小:" + minWeightPerson.get());
}
求和
/**
* 聚合操作-总和
*/
@Test
public void aggregationSum() {
// 获取体重总和
Integer sumWeight = stream.collect(Collectors.summingInt(Person::getWeight));
System.out.println("总值:" + sumWeight);
}
统计元素个数
/**
* 聚合操作-统计个数
*/
@Test
public void aggregationCount() {
// 获取流中元素个数
Long count = stream.collect(Collectors.counting());
System.out.println("元素个数:" + count);
}
求平均值
/**
* 聚合操作-平均值
*/
@Test
public void aggregationAvg() {
// 获取体重平均值
Double avgWeight = stream.collect(Collectors.averagingInt(Person::getWeight));
System.out.println("平均值:" + avgWeight);
}a
Stream流的分组操作
同样的Stream流也有分组操作。方法声明如下:
// 传入传入的Function进行分组
public static Collector>>
groupingBy(Function super T, ? extends K> classifier);// 传入传入的Function和Collector可以进行多级分组public static Collector>
groupingBy(Function super T, ? extends K> classifier,
Collector super T, A, D> downstream); // 可以通过Supplier函数指定返回的类型public static > Collector
groupingBy(Function super T, ? extends K> classifier,
Supplier mapFactory,
Collector super T, A, D> downstream);
初始化数据:
private Stream stream;/**
* 初始化
*/@BeforeEachpublic void init() {
stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 210),new Person("杨文浡", 120),new Person("阿文", 150)
);
}
分组
通过体重来进行分组操作。
/**
* 通过字段分组
*/
@Test
public void groupByField01() {
Map> map =
stream.collect(Collectors.groupingBy(Person::getWeight));
map.forEach((k, v) -> System.out.println(k + ":" + v));
}
控制台:
210:[Person{name='汪汪汪', weight=210}, Person{name='狗东', weight=210}]
150:[Person{name='阿文', weight=150}]
120:[Person{name='杨文浡', weight=120}]
多级分组
先通过名字长度进行分组,再通过体重进行多级分组。
/**
* 通过字段分组 多级分组
*/
@Test
public void groupByField03() {
// 先通过名字的长度进行分组,再用体重进行分组
Map>> map = stream.collect(
Collectors.groupingBy(person -> person.getName().length(),
Collectors.groupingBy(person -> {if (person.getWeight() > 200) {return "肥胖";
} else {return "正常";
}
})));
map.forEach((dk, dv) ->{
System.out.println(dk);
dv.forEach( (k,v) -> {
System.out.println("\t" + k +" : "+v);
});
});
}
控制台:
2
肥胖 : [Person{name='狗东', weight=210}]
正常 : [Person{name='阿文', weight=150}]
3
肥胖 : [Person{name='汪汪汪', weight=210}]
正常 : [Person{name='杨文浡', weight=120}]
分组返回指定对象
/**
* 返回指定对象,即用指定类型接收
*/
@Test
public void groupReturn() {
TreeMap> treeMap = stream.collect(Collectors.groupingBy(
Person::getWeight,
TreeMap::new,
Collectors.toList()));
treeMap.forEach((dk, dv) -> {
System.out.println(dk + ":");
dv.forEach(v -> System.out.println("\t" + v));
});
}
Stream流的分区操作
可以使用Collectors.partitioningBy方法来给Stream中的数据进行分区,根据传入的Predicate对象过滤,将true和false的数据分区。
方法声明:
// 可以通过Predicate作为条件进行分区
public static Collector>>
partitioningBy(Predicate super T> predicate);// 可以通过Predicate作为条件进行分区// 第二个参数传入一个收集器Collector对象可以对分区后的数据再次处理public static Collector>
partitioningBy(Predicate super T> predicate,
Collector super T, A, D> downstream)
初始化数据:
private Stream stream;/**
* 初始化
*/@BeforeEachpublic void init() {
stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 210),new Person("杨文浡", 120),new Person("阿文", 150)
);
}
普通分区
@Test
public void partition() {
// 通过体重来进行分区
Map> partitionMap =
stream.collect(Collectors.partitioningBy(person ->
person.getWeight() >= 180
));// 遍历打印
partitionMap.forEach((k, v) -> {
System.out.println(k + "==" + v);
});
}
控制台:
false==[Person{name='杨文浡', weight=120}, Person{name='阿文', weight=150}]
true==[Person{name='汪汪汪', weight=210}, Person{name='狗东', weight=210}]
高级分区
可以通过第二个参数传入一个收集器Collector对象进行高级分区。
@Test
public void partition02() {
// 通过体重来进行分区
Map>> partitionMap =
stream.collect(Collectors.partitioningBy(person -> person.getWeight() >= 180,
Collectors.groupingBy(person -> {int length = person.getName().length();if (length > 2) {return "大于2个字";
}return "小于等于2个字";
}))); // 遍历打印
partitionMap.forEach((dk, dv) ->{
System.out.println(dk);
dv.forEach( (k,v) -> {
System.out.println("\t" + k +" : "+v);
});
});
}
控制台:
false
小于等于2个字 : [Person{name='阿文', weight=150}]
大于2个字 : [Person{name='杨文浡', weight=120}]
true
小于等于2个字 : [Person{name='狗东', weight=210}]
大于2个字 : [Person{name='汪汪汪', weight=210}]
Stream流的拼接操作
方法声明:
// 直接拼接 没有分隔符
public static Collector joining() // 指定分隔符 拼接public static Collector joining(CharSequence delimiter) // 指定分隔符 带前缀和后缀 拼接public static Collector joining(CharSequence delimiter,
CharSequence prefix,
CharSequence suffix)
初始化数据:
private Stream stream;/**
* 初始化
*/@BeforeEachpublic void init() {
stream = Stream.of(new Person("汪汪汪", 210),new Person("狗东", 210),new Person("杨文浡", 120),new Person("阿文", 150)
);
}
拼接并以分隔符分割
@Test
public void joining01(){
// 拼接流中元素,以分隔符分割
String joinStr = stream.map(Person::getName)
.collect(Collectors.joining(","));
System.out.println(joinStr);
}
控制台:
汪汪汪,狗东,杨文浡,阿文
拼接带前后缀
@Test
public void joining02(){
// 拼接流中元素,以分隔符分割,有前缀和后缀
String joinStr = stream.map(Person::getName)
.collect(Collectors.joining(",","qAq","bAb"));
System.out.println(joinStr);
}
控制台:
qAq汪汪汪,狗东,杨文浡,阿文bAb
性能比较
关于串行流、并行流和传统for循环的性能对比,可以查看下面这篇文章。
Stream Performance
https://github.com/CarpenterLee/JavaLambdaInternals/blob/master/8-Stream%20Performance.md
嗯,强者搞的。
小结
此处只是讲了一些Stream流的API和一些简单的操作,还有未涉及到的地方,大家可以直接去查看Stream流的API。
在适当的情况下使用Stream流操作效果非常好,但是由于Stream流的设计原因,在简单的场景下,传统的for循环可能更加适合,而且并行流在不了解原理的情况下尽量不要使用,避免出现问题。