Stream API是Java8中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以进行非常复杂的查找、过滤和数据映射等操作。简而言之,Stream API提供了一种高效且易于使用的数据处理方式。
ps:
①、Stream自己不会存储元素。 ②、Stream不会改变源对象,相反,它们会返回一个持有结果的新Stream。 ③、Stream操作是延迟执行的,这意味着它们会等到需要结果的时候才执行。
使用Stream API操作的三个步骤
一、创建Stream:一个数据源(集合、数组),获取一个流。
二、中间操作:一个中间操作链,对数据源的数据进行处理。
三、终止操作:一个终止操作,执行中间操作链,并产生结果。
这里需要注意的是,中间操作不会执行任何操作,直到终止操作,一次性执行全部内容。这就叫做“延迟加载”。
创建Stream
创建一个Stream有多种方式:
①、通过Collection系列集合提供的stream()或parallelStream()方法。
stream():创建一个顺序流。
parallelStream():创建一个并行流。
至于两者的差异,在后面的使用中会说到。
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();
②、通过Arrays中的静态方法stream()获取数组流
Book[] books = new Book[10];
Stream<Book> bookStream = Arrays.stream(books);
通过查看Arrays的源码,看到返回Stream对象的方法:public static Stream stream(T[] array)。
这个使用泛型,表示,可以创建任何类型的数组流,比如:String[],Integer[],Long[],自定义对象数组。上述代码中,就创建了一个Book类型的数组流。
③、通过Stream中的静态方法of()
Stream<String> stringStream = Stream.of("a", "b", "c");
stringStream.forEach(System.out::println);
④、创建无限流(迭代)
Stream<Integer> integerStream = Stream.iterate(0, (x) -> x + 2);
integerStream.forEach(System.out::println);
什么叫无限流呢,不像从固定集合创建的流那样有固定的大小,而是会根据生成规则,无限执行。
上面的代码,就会根据规则:从0开始,产生到(x) -> x+2的流。这是一个Lambda表达式,是UnaryOperator这个函数式接口,表示一个一元运算。传入一个参数,根据Lambda体的处理,返回一个值。因为没有限制结束的位置,所以会一直执行下去。可以理解为一个迭代的过程。
⑤、创建无限流(生成)
Stream<Double> doubleStream = Stream.generate(() -> Math.random());
doubleStream.limit(10).forEach(System.out::println);
使用Math.random()随机生成一个[0,1)的数,类型为double,这里多了一个limit(10)方法,这个方法就是控制流生成的个数,就好像MySQL中的limit一样。如果不控制数据的个数,这个过程将一直执行下去。
同理,上面的迭代过程,也可以使用limit()这个方法。
中间操作
上面讲完了Stream的创建,下面就讲中间操作。因为下面的操作都会用到公共的集合,所以,这里将代码抽取出来。跟前三篇博文用到的案例一样。
private final Book[] books = new Book[]{
new Book("深入理解Java虚拟机", 54.50, "周志明", 10000L),
new Book("Java多线程编程核心技术", 47.60, "高红岩", 666L),
new Book("Spring实战(第四版)", 59.60, "Craig Walls", 3500L),
new Book("Python编程(从入门到实践)", 62.00, "埃里克·马瑟斯", 888L),
new Book("大话数据结构", 40.70, "程杰", 560L),
new Book("大话数据结构", 40.70, "程杰", 560L),
new Book("大话数据结构", 40.70, "程杰", 560L)
};
private List<Book> bookList = Arrays.asList(books);
筛选和切片
filter:接收Lambda,从流中排除某些元素
@Test
public void test1() {
Stream<Book> stream = bookList.stream()
.filter((book) -> book.getStock() >= 3000);
//内部迭代:迭代操作由Stream api完成。
stream.forEach(System.out::println);
}
上面说了Stream API需要注意,中间操作会生成中间操作链,但是不会执行任何操作,只有终止操作,才能执行全部内容。所以,为了看到效果,这里使用了终止操作,即打印语句。
上面的filter方法将库存小于3000的过滤掉,保留条件相反的结果。
limit:截断流,使其元素不超过给定的数量
@Test
public void test2() {
bookList.stream().limit(3).forEach(System.out::println);
}
创建无限流的时候,使用了这个方法,上面的代码,就是获取Book集合中前3个结果。个人理解为:限流。
skip(n):跳过元素
返回一个扔掉了前n个元素的流,若流中元素不足n个,则返回一个空流。与limit互补。
@Test
public void test3() {
bookList.stream().skip(3).forEach(System.out::println);
}
上面代码的结果就是跳过前三个结果,取之后的结果。
distinct:筛选
通过流所生成元素的hashCode()和equals()去除重复元素。
@Test
public void test4() {
bookList.stream()
.filter((book) -> book.getPrice() < 50)
.distinct()
.forEach(System.out::println);
}
上面的代码使用了filter()过滤了大于等于50的结果,保留相反的结果,这里主要为了强调,Stream并非一次只能进行一次中间操作,而是通过很多过滤条件,生成调用链,即中间操作链。
不使用distinct(),会返回4条数据,但是使用之后,将重复数据过滤掉了。
ps:
如果是引用数据类型,需要重写equals()和hashCode()方法。
映射
映射有两种方法:
①:map-接收Lambda ,将元素转换成其他形式或提取信息,接收一个函数作为参数,该函数会被应用到每一个元素上,并将其映射成一个新的元素。
②:flatMap-接收一个函数作为参数,将流中的每一个值都换成另一个流,然后把所有流连接成一个流。
@Test
public void test1() {
bookList.stream().map(Book::getBookName)
.forEach(System.out::println);
}
提取所有书名,放入一个流中。
下面案例:将字符串集合中的字符提取出来,放入集合中。
@Test
public void test2() {
List<String> list = Arrays.asList("ab", "cd", "ef", "ge");
Stream<Stream<Character>> stream1 = list.stream()
.map(MappingTest::filterChar);
//stream1.forEach(System.out::println);
stream1.forEach((sm) -> {
sm.forEach(System.out::println);
});
System.out.println("-----------");
Stream<Character> stream2 = list.stream()
.flatMap(MappingTest::filterChar);
stream2.forEach(System.out::println);
}
private static Stream<Character> filterChar(String str) {
List<Character> list = new ArrayList<>();
for (char ch : str.toCharArray()) {
list.add(ch);
}
return list.stream();
}
可以看到,这里使用了两种映射方式,map()操作之后,返回的是Stream<Stream<‘Character’>>,也就是将每一个操作都返回成了一个流。
而flatMap()返回的是Stream<‘Character’>,将所有流连接成一个流。这个可以对比集合中的add()和addAll()方法。为了方便观察,直接将遍历stream1的注释打开
可以看到,打印的是每一个Stream对象。
ps:
如果这里,你对一个Stream进行了两次终止操作,则会抛出异常。 java.lang.IllegalStateException: stream has already been operated upon or closed 原因是,Stream是单向的,数据只能遍历一次。就好像流水,一去不复返。
排序
@Test
public void test1() {
List<String> list = Arrays.asList("aa", "cc", "bb", "dd", "gg", "ee");
//自然排序
list.stream().sorted()
.forEach(System.out::println);
//定制排序
bookList.stream().sorted((x, y) -> {
if (x.getPrice().equals(y.getPrice())) {
return x.getStock().compareTo(y.getStock());
} else {
return x.getPrice().compareTo(y.getPrice());
}
}).forEach(System.out::println);
}
除了支持自然排序,还支持定制排序,因为sorted()方法中可以传入一个Comparator接口。然后排序规则,由我们自己定义。上面代码的规则就是:
先判断价格是否相等,如果相等,进行库存的比较,反之,则比较价格。
终止操作
前面讲了Stream的创建、中间操作,最后就讲一下终止操作。
查找与匹配
为了更直观的展示程序的效果,所以对Book这个类进行修改,新增属性status。
public enum Status {
/**
* 正常
*/
NORMAL,
/**
* 缺货
*/
LACK,
/**
* 下架
*/
SUSPEND
}
private final Book[] books = new Book[]{
new Book("深入理解Java虚拟机", 54.50, "周志明", 10000L, Book.Status.NORMAL),
new Book("Java多线程编程核心技术", 47.60, "高红岩", 666L, Book.Status.NORMAL),
new Book("Spring实战(第四版)", 59.60, "Craig Walls", 3500L, Book.Status.LACK),
new Book("Python编程(从入门到实践)", 62.00, "埃里克·马瑟斯", 888L, Book.Status.LACK),
new Book("大话数据结构", 40.70, "程杰", 560L, Book.Status.SUSPEND),
new Book("大话数据结构", 40.70, "程杰", 560L, Book.Status.SUSPEND),
new Book("大话数据结构", 40.70, "程杰", 560L, Book.Status.SUSPEND)
};
private List<Book> bookList = Arrays.asList(books);
allMatch:检查是否匹配所有元素
@Test
public void test1() {
boolean flag = bookList.stream()
.allMatch((book) -> book.getStatus().equals(Book.Status.NORMAL));
System.out.println(flag);
}
这个方法返回boolean类型,上面的代码,判断集合中的数据是否都是正常状态。全部匹配返回true,否则返回false。
anyMatch:匹配至少一个元素
@Test
public void test2() {
boolean flag = bookList.stream()
.anyMatch((book) -> book.getStatus().equals(Book.Status.SUSPEND));
System.out.println(flag);
}
返回类型同上,这个是只要一个匹配,就会返回true。否则返回false。
noneMatch:是否没有匹配所有元素(有匹配的元素)
@Test
public void test3() {
boolean match = bookList.stream()
.noneMatch((book) -> book.getStatus().equals(Book.Status.NORMAL));
System.out.println(match);
}
findFirst:查找第一个元素
@Test
public void test4() {
//有可能为空,就会封装到optional中去
Optional<Book> first = bookList.stream()
.sorted((b1, b2) -> -Double.compare(b1.getPrice(), b2.getPrice()))
.findFirst();
System.out.println(first.get());
}
这里看到了返回值为Optional,这个对象也是Java8提供的,一个对象判空的对象。有可能出现没有第一个元素的情况。
findAny:返回当前流中任意一个元素
@Test
public void test5() {
Optional<Book> any = bookList.parallelStream()
.filter((book) -> book.getStatus().equals(Book.Status.NORMAL))
.findAny();
System.out.println(any.get());
}
可以看到这里使用的是parallelStream()这个方法,前面提到了并行流。也就是支持并发操作。返回任意元素的方法是随机的,就像线程执行顺序是随机的一样。
所以使用parallelStream()这个方法,才能看到效果。
count:返回流中元素的总个数
@Test
public void test6() {
long count = bookList.stream().count();
System.out.println(count);
}
max:返回最大值
@Test
public void test7() {
Optional<Book> max = bookList.stream()
.max((b1, b2) -> Double.compare(b1.getPrice(), b2.getPrice()));
System.out.println(max.get());
}
min:返回最小值
@Test
public void test8() {
Optional<Double> min = bookList.stream()
.map(Book::getPrice)
.min(Double::compare);
System.out.println(min.get());
}
规约与收集
规约:将流中的元素反复结合起来,得到一个值。
收集:将流转换为其他形式,接收一个Collector接口的实现,用于给Stream中的元素做汇总。
reduce(规约)
@Test
public void test1() {
List<Integer> asList = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8);
//以起始值为6开始,x:6 y:1 x:7 y:2 x:9 y:3
Integer sum = asList.stream()
.reduce(6, (x, y) -> x + y);
System.out.println(sum);
//计算总库存量(map-reduce模式)
Optional<Long> sumValue = bookList.stream()
.map(Book::getStock)
.reduce(Long::sum);
Long aDouble = sumValue.get();
System.out.println(aDouble);
}
第一个reduce使用的是T reduce(T identity, BinaryOperator accumulator);
第一个参数设置一个初始值,让它作为x,然后从集合中取值作为y,然后进行加和,因为BinaryOperator是一个二元运算,实际上是有规则的累加。
第二段代码是将集合中的所有库存量都取出来,然后进行累加。
collect(收集)
Collectors实现类提供了很多静态方法,可以方便的创建常用的收集器实例。
@Test
public void test2() {
System.out.println("-------list-------");
//取出所有非书名,然后转为list集合
List<String> listValues = bookList.stream()
.map(Book::getBookName)
.collect(Collectors.toList());
listValues.forEach(System.out::println);
System.out.println("-------set-------");
//取出所有非书名,然后转为set集合
Set<String> setValues = bookList.stream()
.map(Book::getBookName)
.collect(Collectors.toSet());
setValues.forEach(System.out::println);
System.out.println("-------HashSet-------");
//取出所有非书名,然后转为HashSet集合
HashSet<String> hashValues = bookList.stream()
.map(Book::getBookName)
.collect(Collectors.toCollection(HashSet::new));
hashValues.forEach(System.out::println);
}
下面的操作,有的方法使用两种方式,一种是带collect()方法的,一种是上面讲过的,这里我放到了一起,做了比较,但是,输出的结果都一样。
@Test
public void test3() {
//总数
Long count1 = bookList.stream()
.collect(Collectors.counting());
long count2 = bookList.stream().count();
System.out.println(count1 + ":" + count2);
//平均值
Double priceAvg = bookList.stream()
.collect(Collectors.averagingDouble(Book::getPrice));
System.out.println(priceAvg);
//总和
Double priceSum1 = bookList.stream().collect(Collectors.summingDouble(Book::getPrice));
double priceSum2 = bookList.stream()
.mapToDouble(Book::getPrice)
.sum();
System.out.println(priceSum1 + ":" + priceSum2);
//最大值
Optional<Book> bookMax1 = bookList.stream()
.collect(Collectors.maxBy((v1, v2) -> Double.compare(v1.getPrice(), v2.getPrice())));
Optional<Book> bookMax2 = bookList.stream()
.max((v1, v2) -> Double.compare(v1.getPrice(), v2.getPrice()));
System.out.println(bookMax1.get() + "\n" + bookMax2.get());
//最小值
Optional<Double> priceMin1 = bookList.stream()
.map(Book::getPrice)
.collect(Collectors.minBy(Double::compare));
Optional<Double> priceMin2 = bookList.stream()
.map(Book::getPrice)
.min(Double::compare);
System.out.println(priceMin1.get() + ":" + priceMin2.get());
}
还能像收据库一样对结果集进行分组。
@Test
public void test4() {
Map<Book.Status, List<Book>> groupMap = bookList.stream()
.collect(Collectors.groupingBy(Book::getStatus));
Set<Map.Entry<Book.Status, List<Book>>> entrySet = groupMap.entrySet();
for (Map.Entry<Book.Status, List<Book>> entry : entrySet) {
System.out.println(entry.getKey() + ":" + entry.getValue() + "\n");
}
}
这里根据状态进行分组。
还支持多级分组
@Test
public void test5() {
Map<Book.Status, Map<String, List<Book>>> map = bookList.stream()
.collect(Collectors.groupingBy(Book::getStatus, Collectors.groupingBy((e) -> {
if (((Book) e).getStock() <= 1000) {
return "库存堪忧";
} else if (((Book) e).getStock() <= 5000) {
return "库存正常";
} else {
return "库存充裕";
}
})));
for (Map.Entry<Book.Status, Map<String, List<Book>>> entry : map.entrySet()) {
System.out.println(entry.getKey() + "::" + entry.getValue() + "\n");
}
}
首先按照状态分组,然后再按照库存量进行分组。
分区:满足条件的一个区,不满足条件的一个区
@Test
public void test6() {
Map<Boolean, List<Book>> map = bookList.stream()
.collect(Collectors.partitioningBy((e) -> e.getPrice() > 55));
for (Map.Entry<Boolean, List<Book>> entry : map.entrySet()) {
System.out.println(entry.getKey() + "::" + entry.getValue() + "\n");
}
}
获取最值的另一种方式
@Test
public void test7() {
DoubleSummaryStatistics summaryStatistics = bookList.stream()
.collect(Collectors.summarizingDouble(Book::getPrice));
double average = summaryStatistics.getAverage();
long count = summaryStatistics.getCount();
double max = summaryStatistics.getMax();
double min = summaryStatistics.getMin();
double sum = summaryStatistics.getSum();
System.out.println(average + "\n" + count + "\n" + max + "\n" + min + "\n" + sum);
}
连接字符串
@Test
public void test8() {
String str = bookList.stream()
.map(Book::getBookName)
.collect(Collectors.joining(",", "[", "]"));
System.out.println(str);
}
按照自己定的规则,连接字符串,上面就提取所有书名,然后加了前缀和后缀,以","分割。
基本上,Stream API的用法就总结完了,通过上面的代码,可以发现,Stream API真的让我们的操作变得简单化,就像写SQL语句一样,对数据进行操作。但是,方法还是挺多的,这个需要长时间的练习,才能
将各个API熟练使用,所以,重要的事情说三遍:多动手,多动手,多动手!
世上唯一治不好的病,穷病!