Java 8 Stream 新特性
1. 前言
(1)Stream 使用一种类似用 SQL 语句从数据库查询数据的直观方式来提供一种对 Java 集合运算和表达的高阶抽象。
(2)在我们日常使用Java的过程中,免不了要和集合打交道。对于集合的各种操作有点类似于SQL——增删改查以及聚合操作,但是其方便性却不如SQL。
(3)所以有没有这样一种方式可以让我们不再使用一遍又一遍的循环去处理集合,而是能够便捷地操作集合?
答案是有的,它就是——Java 8引入的Stream,亦称为流 。
流的功能核心:创建基于该集合的Stream对象,通过该Stream对象的操作来实现对该集合中数据的筛查、映射、排序等操作,产生一个符合某种条件的新的数据源,最后按照所需要的格式或条件终止操作并输出
2. 什么是流?
2-1. 流的定义
A Stream is a sequence of elements from a source.
Stream(流)是一个来自数据源的元素队列并支持聚合操作
简单来说,流是对数据源的包装,它允许我们对数据源进行聚合操作,并且可以方便快捷地进行批量处理。
(数据源:流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。)
日常生活中,我们看见水流在管道中流淌。Java中的流也是可以在“管道”中传输的。并且可以在“管道”的节点进行处理,比如筛选,排序等。
+--------------------+ +------+ +------+ +---+ +-------+
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect|
+--------------------+ +------+ +------+ +---+ +-------+
元素流在管道中经过中间操作(intermediate opertaion)的处理,最后由终端操作(terminal opertaion)得到前面处理的结果(每一个流只能有一次终端处理)。
中间操作可以分为无状态操作和有状态操作,前者是指元素的处理不受之前元素的影响;后者是指该操作只有拿到所有元素才能继续下去。
终端操作也可分为短路与非短路操作,前者是指遇到符合条件的元素就可以得到最终结果,而后者必须处理所有元素才能得到最终结果。
下图为我们展示了中间操作和终端操作的具体方法。
① 创建Stream
创建一个基于某一数据源(如:集合、数组···)的流
② 中间操作
一个中间操作链,对该数据源的数据进行处理
③ 终止操作(终端操作)
一旦执行终止操作,就执行中间操作链,并产生结果。之后,不会再被使用(即:如果再想使用,需要从新再创建Stream)。
如何快速区分中间操作和终端操作?
看方法的返回值,返回值为Stream的一般都是中间操作,否则是终端操作。
2-2.Stream的特征:
流并不存储数据,所以它不是一个数据结构,它也不会修改底层的数据源,它为了函数式编程而生。
Stream是处理数组和集合的API,Stream具有以下特点:
- 不是数据结构,没有内部存储
- 不支持索引访问
- 延迟计算
- 支持过滤,查找,转换,汇总等操作
对于StreamAPI的学习,首先需要弄清楚lambda的两个操作类型:中间操作和终止操作。 下面通过一个demo来认识下这个过程。
Stream st=Arrays.asList(1,2,3,4,5).stream().filter(x->{
System.out.print(x);
return x>3;
});
当我们执行这段代码的时候,发现并没有任何输出,这是因为lambda表达式需要一个终止操作来完成最后的动作。 我们修改代码:
Stream st=Arrays.asList(1,2,3,4,5).stream().filter(x->{
System.out.print(x);
return x>3;
});
st.forEach(t-> System.out.print(t));
//对应的输出结果是:1234455
为什么会有这个输出呢?因为在filter函数的时候并没有真正的执行,在forEach的时候才开始执行整个lambda表达式,所以当执行到4的时候,filter输出之后,forEach也执行了,最终结果是1234455
对于Java中的lambda表达式的操作,可以归类和整理如下:
中间操作:
过滤 filter
去重 distinct
排序 sorted
截取 limit、skip
转换 map/flatMap
其他 peek
终止操作
循环 forEach
计算 min、max、count、 average
匹配 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny
汇聚 reduce
收集器 toArray collect
3. 创建一个流
创建一个Java流有许多方式。一旦流被创建了,那么它是无法修改数据源的,所以针对一个数据源我们可以创建多个流。
3-1. 创建一个空的流
我们可以使用empty() 方法来创建一个空的流:
Stream<String> emptyStream = Stream.empty();
我们还可以用empty() 方法来返回一个空流从而避免返回null:
public Stream<String> streamOf(List<String> list) {
return list == null || list.isEmpty() ? Stream.empty() : list.stream();
}
3.2 使用数组创建流
我们可以使用数组的全部或者一部分来创建流:
String[] arr = new String[]{"1", "2", "3","4", "5"};
Stream<String> entireArrayStream = Arrays.stream(arr);
Stream<String> partArrayStream = Arrays.stream(arr, 1, 4);
3.3. 使用集合创建流
我们也可以使用集合来创建流:
Collection<String> collection = Arrays.asList("1", "2", "3");
Stream<String> collectionStream = collection.stream();
3.4 使用Stream.Builder()来创建流
使用这种方式创建流的时候请注意,一定要声明好你想要的类型,否则创建的会是Stream的流:
Stream<String> streamBuilder =
Stream.<String>builder().add("1").add("2").add("3").build();
3.5 使用File来创建流
我们可以通过Files.lines()方法来创建流。文件的每一行都会成为流的每一个元素。
Path path = Paths.get("C:\\tmp\\file.txt");
Stream<String> fileStream = Files.lines(path);
Stream<String> fileStreamWithCharset = Files.lines(path, Charset.forName("UTF-8"));
3.6 Stream.iterate()
我们还可以使用iterate() 来创建一个流:
Stream<Integer> iteratedStream = Stream.iterate(10, n -> n + 1).limit(10);
在上面这段代码中,将会创造一个连续元素的流。
第1个元素是10,第2个元素是11,依此类推,直到元素数量达到size。
3.7 基本类型的流
1. range()和rangeClosed()
在Java8中,三种基本类型——int,long,double可以创建对应的流。
因为Stream是泛型接口,所以无法用基本类型作为类型参数,因为我们使用IntStream,LongStream,DoubleStream来创建流。
IntStream intStream = IntStream.range(1, 3);//1,2
LongStream longStream = LongStream.rangeClosed(1, 3);//1,2,3
range(int start, int end) 方法会创建一个从start到end的有序流,它的步长是1,但是它不包括end。
rangeClosed(int start, int end) 与range() 方法的区别在于,前者会包括end。
2.of() 方法
此外,基本类型还可以通过of() 方法来创建流。
int[] intArray = {1,2,3};
IntStream intStream = IntStream.of(intArray);//1,2,3
IntStream intStream2 = IntStream.of(1, 2, 3);//1,2,3
long[] longArray = {1L, 2L, 3L};
LongStream longStream = LongStream.of(longArray);//1,2,3
LongStream longStream2 = LongStream.of(1L, 2L, 3L);//1,2,3
double[] doubleArray = {1.0, 2.0, 3.0};
DoubleStream doubleStream = DoubleStream.of(doubleArray);
DoubleStream doubleStream2 = DoubleStream.of(1.0, 2.0, 3.0);//1.0,2.0,3.0
3. Random类
另外,从Java8开始,Random类也提供了一系列的方法来生成基本类型的流。例如:
Random random = new Random();
IntStream intStream = random.ints(3);
LongStream longStream = random.longs(3);
DoubleStream doubleStream = random.doubles(3);
3.9 字符串的流
1.字符的流
因为Java没有CharStream,所以我们用InStream来替代字符的流。
IntStream charStream = "abc".chars();
2. 字符串的流
我们可以通过正则表达式来创建一个字符串的流。
Stream<String> stringStream = Pattern.compile(",").splitAsStream("a,b,c");
4. 流的用法
4.1 基本用法
4.1.1 forEach()方法
我们对forEach() 方法应该很熟悉了,在Collection中就有。它的作用是对每个元素执行指定的动作,也就是对元素进行遍历。
Arrays.asList("Try", "It", "Now")
.stream()
.forEach(System.out::println);
输出结果:
Try
It
Now
1.方法引用
可能会有读者疑惑System.out::println是什么写法,正常的写法不应该都是下面这样嘛?
Arrays.asList("Try", "It", "Now")
.stream()
.forEach(ele -> System.out.println(ele));
其实两者写法是等价的,只不过前者是后者的简写方式。前者这种语法形式叫做方法引用(method references),这种语法用来替代某些特定形式的lambda表达式。
如果lambda表达式的全部内容就是调用一个已有方法,那么可以用方法引用来替代lambda表达式。
这一点很重要,也很值得学习,在下面的内容中也会有很多这样的简写。
我们插个题外话,我们可以将方法引用细分为以下四类:
类别 | 例子 |
---|---|
引用静态方法 | Integer::sum |
引用某个对象的方法 | list::add |
引用某个类的方法 | String::length |
引用构造方法 | HashMap::new |
而System.out::println就是引用了某个对象的方法。
4.1.2 filter()方法
filter() 方法的作用是返回符合条件的Stream。
Arrays.asList("Try", "It", "Now")
.stream()
.filter(ele -> ele.length() == 3)
.forEach(System.out::println);
输出结果:
Try
Now
4.1.3 distinct()方法
distinct() 方法返回一个去重的stream。
Arrays.asList("Try", "It", "Now", "Now")
.stream()
.distinct()
.forEach(System.out::println);
4.1.4 sorted()方法
排序函数有两个,一个是自然顺序,还有一个是自定义比较器排序。
Arrays.asList("Try", "It", "Now")
.stream()
.sorted((str1, str2) -> str1.length() - str2.length())
.forEach(System.out::println);
输出结果:
It
Try
Now
4.1.5 map()方法
map() 方法对每个元素按照某种操作进行转换,转换后流的元素不会改变,但是元素类型取决于转换之后的类型。
Arrays.asList("Try", "It", "Now")
.stream()
.map(String::toUpperCase)
.forEach(System.out::println);
输出结果:
TRY
IT
NOW
4.1.6 flatMap()方法
flat的英文就是”平坦的“意思,而flatMap()方法的作用就是将流的元素摊平,借助下面这个例子我们更好理解:
Stream.of(Arrays.asList("Try", "It"), Arrays.asList("Now"))
.flatMap(list -> list.stream())
.forEach(System.out::println);
输出结果:
Try
It
Now
在上述这段代码中,原来的stream有两个元素,分别是两个List,执行了flatMap()之后,将每个List都”摊平“成了一个个的元素,所以会产生一个有三个字符串组成的流。
4.2 归约操作
上一小节介绍了Stream的基本用法,但是如此强大的流又怎么能止步于此呢? 下面让我们看看流的重头戏——归约操作。
归约操作(reduction operation)也被称为折叠操作(fold),是通过某种连接动作将所有元素汇总成一个结果的过程。元素求和、求最大值、求最小值、求总数,将所有元素转换成一个集合等都属于归约操作。
Stream类库有两个通用的归约操作reduce()和collect() ,也有一些为简化书写而设计的专用归约操作,比如sum()、max()、min()、count()等。
这些都比较好理解,所以我们会重点介绍reduce()和collect()。
4.2.1 reduce()
reduce操作可以实现从一组元素中生成一个值,比如sum()、max()、min()、count()等都是reduce操作。
reduce()方法定义有三种形式:
Optional<T> reduce(BinaryOperator<T> accumulator)
T reduce(T identity, BinaryOperator<T> accumulator)
<U> U reduce(U identity, BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)
-
identity-初始值
-
accumulator-累加器
-
combiner-拼接器,只有并行执行时才会用到。
4.2.2 collect()方法
collect()应该算是Stream里的最终王牌选手了,基本上你想要的功能都能在这里找到。
而且使用它也是Java函数式编程入门一个绝好的途径。
下面让我们从实际的例子出发吧!
List<Student> students = Arrays.asList(new Student("Jack", 90)
, new Student("Tom", 85)
, new Student("Mike", 80));
1. 常规归约操作
- 获取平均值
Double averagingScore = students.stream().collect(Collectors.averagingDouble(Student::getScore));
- 获取和
Double summingScore = students.stream().collect(Collectors.summingDouble(Student::getScore));
- 获取分析数据
DoubleSummaryStatistics doubleSummaryStatistics = students.stream().collect(Collectors.summarizingDouble(Student::getScore));
你可以从doubleSummaryStatistics 获取最大值、最小值、平均值等常见统计数据。
Collectors提供的这些方法省去了额外的map() 方法,当然你也可以先使用map() 方法,再进行操作。
2. 将流转换成Collection
通过以下的代码我们可以提取集合中的Student的Name属性,并且装入字符串类型的集合当中。
List<String> studentNameList = students.stream().map(Student::getName).collect(Collectors.toList());//[Jack, Tom, Mike]
还可以通过Collectors.joining() 方法来连接字符串。 并且Collector会帮你处理后最后一个元素不应该再加分隔符的问题。
String studentNameList = students.stream().map(Student::getName).collect(Collectors.joining(",", "[", "]"));//打印出来就是[Jack,Tom,Mike]
3. 将流转换成Map
Map不能直接转换成Stream,但是Stream生成Map是可行的,在生成Map之前,我们应该先定义好Map的Key和Value分别代表什么。
通常在下面三种情况下collect()的结果会是Map:
- Collectors.toMap(),使用者需要指定Map的key和value;
- Collectors.groupingBy(),对元素进行group操作;
- Collectors.partitioningBy(),对元素进行二分区操作。
Collectors.toMap()
下面这个例子为我们展示了怎么将students列表转换成<Student student, double score>组成的map。
Map<Student, Double> collect = students
.stream()
.collect(Collectors.toMap(Function.identity(), Student::getScore));
Collectors.groupingBy()
这个操作有点类似于SQL中的groupBy操作,按照某个属性对数据进行分组,而属性相同的元素会被分配到同一个key上。
而下面这个例子将会把Student按照Score进行分组:
Map<Double, List<Student>> nameStudentMap = students.stream().collect(Collectors.groupingBy(Student::getScore));