相信现在大多数的伙伴们,都在使用Java 8了,而 Java 8相比以前的版本,是作出了革命性的改变。Java8的特性大致可总结为,开发速度更快,代码更少,增加了Lambda,强大的Stream API,便于并行,最大化减少空指针异常。
本文主要讲解Java 8的Stream,Stream 是用函数式编程方式在集合类上进行复杂操作的工具,其集成了Java 8中的众多新特性之一的聚合操作,开发者可以更容易地使用Lambda表达式,并且更方便地实现对集合的查找、遍历、过滤以及常见计算等。
Stream 中文称为 “流”,通过将集合转换为这么一种叫 “流” 的元素序列,通过声明性方式,能够对集合中的每个元素进行一系列并行或串行的流水线操作。换句话说,你只需要告诉流你的要求,流便会在背后自行根据要求对元素进行处理,而你只需要 “坐享其成”。
整个流操作就是一条流水线,将元素放在流水线上一个个地进行处理。很多流操作本身就会返回一个流,所以多个操作可以直接连接起来,下面是一条 Stream 操作的代码:
JDK8以前,进行这一系列操作,你需要做个迭代器或者 foreach 循环,然后遍历,一步步地亲力亲为地去完成这些操作;但是如果使用流,你便可以直接声明式地下指令,流会帮你完成这些操作。就像你想查数据库User表中的某个用户,只要知道id即可通过查询语句获取,无需你去遍历查寻整个数据库。
首先,大家应该知道流的基本特性吧,那就是流是一次性的,和迭代器类似,只能迭代一次。
Stream<String> stream = list.stream().map(User::getName).sorted().limit(10);
List<String> newList = stream.collect(toList());
List<String> newList2 = stream.collect(toList());
大家看看上面的代码,能运行通过吗?实际运行结果如下:
java.lang.IllegalStateException: stream has already been operated upon or closed
这是因为在上面的代码中,第二行已经使用了流,流已被消费掉了。所以第三行再获取就会报错。
下面我们开始实战
首先我们先创建一个 User泛型的 List,实体User包含name,age。
List<User> list = new ArrayList<>();
list.add(new User("Lucy", 25));
list.add(new User("Leon", 21));
list.add(new User("Tom", 18));
最常用到的方法,将集合转换为流。
List list = new ArrayList();
// return Stream<E>
list.stream();
保留 boolean 为 true 的元素。
保留年龄大于的 user 元素
list = list.stream()
.filter(person -> person.getAge() >= 20)
.collect(toList());
打印输出 [User{name='Lucy', age=25}, User{name='Leon', age=21}]
去除重复元素,这个方法是通过类的 equals 方法来判断两个元素是否相等的。
List<User> list = new ArrayList<>();list.add(new User("Lucy", 25));
list.add(new User("Leon", 21));
list.add(new User("Tom", 18));
list.add(new User("Tom", 18));list = list.stream()
.distinct()
.collect(toList());
输出[User{name='Lucy', age=25}, User{name='Leon', age=21}, User{name='Tom', age=18}]
如果流中的元素的类实现了 Comparable 接口,即有自己的排序规则,那么可以直接调用 sorted() 方法对元素进行排序,如 Stream<Integer>。反之, 需要调用 sorted((T, T) -> int)
实现 Comparator 接口。
根据年龄大小来比较:
list = list.stream()
.sorted((p1, p2) -> p1.getAge() - p2.getAge())
.collect(toList());
当然这个可以简化为
list = list.stream()
.sorted(Comparator.comparingInt(User::getAge))
.collect(toList());
返回前 n 个元素
list = list.stream()
.limit(2)
.collect(toList());
打印输出 [User{name='Lucy', age=25}, User{name='Leon', age=21}]
去除前 n 个元素。
list = list.stream()
.skip(2)
.collect(toList());
打印输出 [User{name='Lucy', age=25}]
注意:
list = list.stream()
.limit(2)
.skip(1)
.collect(toList());
打印输出 [User{name='Leon', age=21}]
将流中的每一个元素 T 映射为 R(类似类型转换)
List<String> list01 = list.stream().map(User::getName).collect(toList());
list01里面的元素为 list 中每一个 User对象的 name 变量。
将流中的每一个元素 T 映射为一个流,再把每一个流连接成为一个流
List<String> list = new ArrayList<>();
list.add("aaa bbb ccc");
list.add("ddd eee fff");
list.add("ggg hhh iii");
list = list.stream().map(s -> s.split(" ")).flatMap(Arrays::stream).collect(toList());
上面例子中,我们的目的是把 List 中每个字符串元素以" "分割开,变成一个新的 List<String>。 首先 map 方法分割每个字符串元素,但此时流的类型为 Stream<String[ ]>,因为 split 方法返回的是 String[ ] 类型;所以我们需要使用 flatMap 方法,先使用Arrays::stream将每个 String[ ] 元素变成一个 Stream<String> 流,然后 flatMap 会将每一个流连接成为一个流,最终返回我们需要的 Stream<String>。
流中是否有一个元素匹配给定的 T -> boolean 条件。
是否存在一个 user对象的 age 等于 20:
boolean b = list.stream().anyMatch(user -> user.getAge() == 20);
流中是否所有元素都匹配给定的 T -> boolean
条件。
是否所有user对象的 age 都大于18;
boolean b = list.stream().allMatch(user -> user.getAge() > 18);
流中是否没有元素匹配给定的 T -> boolean
条件。
是否有user对象的 age 小于18;
boolean b = list.stream().noneMatch(user -> user.getAge() < 18);
值得注意的是,这两个方法返回的是一个 Optional<T> 对象,它是一个容器类,能代表一个值存在或不存在,这个后面会讲到。
用于组合流中的元素,如求和,求积,求最大值等。
计算年龄总和:
int sum = list.stream().map(User::getAge).reduce(0, (a, b) -> a + b);
与之相同:
int sum = list.stream().map(User::getAge).reduce(0, Integer::sum);
其中,reduce 第一个参数 0 代表起始值为 0,lambda (a, b) -> a + b
即将两值相加产生一个新值。
同样地:
计算年龄总乘积:
int sum = list.stream().map(User::getAge).reduce(1, (a, b) -> a * b);
当然也可以
Optional<Integer> sum = list.stream().map(User::getAge).reduce(Integer::sum);
即不接受任何起始值,但因为没有初始值,需要考虑结果可能不存在的情况,因此返回的是 Optional 类型。
返回流中元素个数,结果为 long 类型。
long num = list.stream().count();
返回结果为 void,很明显我们可以通过它来干什么了,比方说:
打印出各个元素:
list.stream().forEach(System.out::println);
再比如说 MyBatis 里面访问数据库的 mapper 方法:
向数据库插入新元素:
list.stream().forEach(UserMapper::insertUser);
前面介绍的如list.stream().map(User::getAge).reduce(0, Integer::sum); 计算总和时暗含了装箱成本,map(User::getAge) 方法过后流变成了 Stream<Integer> 类型,而每个 Integer 都要拆箱成一个原始类型再进行 sum 方法求和,这样大大影响了效率。
针对此问题,Java 8引入了数值流IntStream,DoubleStream,LongStream,这种流中的元素都是原始数据类型,分别是 int,double,long。
IntStream intStream =
list.stream().mapToInt(User::getAge);
IntStream 与 LongStream 拥有 range 和 rangeClosed 方法用于数值范围处理
这两个方法的区别在于一个是闭区间,一个是半开半闭区间:
我们可以利用 IntStream.rangeClosed(1, 100) 生成 1 到 100 的数值流。
求 1 到 100 的数值总和:
IntStream intStream = IntStream.rangeClosed(1, 100);int sum = intStream.sum();
之前我们得到一个流是通过一个原始数据源转换而来,其实我们还可以直接构建得到流。
生成字符串流
Stream<String> stream = Stream.of("tom", "lily", "hahahaha");
根据参数的数组类型创建对应的流:
只取索引第 1 到第 2 位的:
int[] a = {1, 2, 3, 4};
Arrays.stream(a, 1, 3).forEach(System.out :: println);
打印 2 ,3
Stream<String> stream = Files.lines(Paths.get("test.txt"));
有如下方法:
Stream.iterate(0, n -> n + 1)
生成流,第一个为0,后面的依次加1
Stream.generate(Math :: random)
生成流,为 0 到 1 的随机双精度数
Stream.generate(() -> 1)
生成流,元素全为 1
Collect 方法作为终端操作,接受的是一个 Collector 接口参数,能对数据进行一些收集归总操作。
收集最常用的方法,是把流中所有元素收集到一个 List, Set 或 Collection 中。如下:
List newlist = list.stream.collect(toList());
Map<Integer, User> map = list.stream().collect(toMap(User::getAge, p -> p));
//注意:如果Map的Key重复了,会报错
long l = list.stream().collect(counting());
或:
long l = list.stream().count();//推荐使用
summing,没错,也是计算总和,不过这里需要一个函数参数
int sum = list.stream().collect(summingInt(User::getAge));
可简化为(推荐):
int sum = list.stream().mapToInt(User::getAge).sum();
Double average = list.stream().collect(averagingInt(User::getAge));
可写成:OptionalDouble average = list.stream().mapToInt(User::getAge).average();
注意:这两种返回的值是不同类型的。
这三个方法比较特殊,比如 summarizingInt 会返回 IntSummaryStatistics 类型
IntSummaryStatistics l = list.stream().collect(summarizingInt(User::getAge));
IntSummaryStatistics 包含了计算出来的平均值,总数,总和,最值,可以通过下面这些方法获得相应的数据。
通过maxBy,minBy 两个方法,需要一个 Comparator 接口作为参数
Optional<Person> optional = list.stream().collect(maxBy(comparing(User::getAge)));
我们也可以直接使用 max 方法获得同样的结果
Optional<User> optional = list.stream().max(comparing(User::getAge));
也是一个比较常用的方法,对流里面的字符串元素进行连接,其底层实现用的是专门用于字符串连接的 StringBuilder。
String s = list.stream().map(User::getName).collect(joining());
结果:LucyLeonTom
String s = list.stream().map(User::getName).collect(joining(","));
结果:Lucy,Leon,Tom
joining 还有一个比较特别的重载方法:
String s = list.stream().map(User::getName).collect(joining(" and ", "Today ", " play games."));
结果:Today Lucy and Leon and Tom play games.
即 Today 放开头,play games. 放结尾,and 在中间连接各个字符串。
groupingBy 用于将数据分组,最终返回一个 Map 类型。
Map<Integer, List<User>> map = list.stream().collect(groupingBy(User::getAge));
例子中我们按照年龄 age 分组,每一个 User 对象中年龄相同的归为一组。
另外可以看出,User::getAge
决定 Map 的键(Integer 类型),list 类型决定 Map 的值(List<User> 类型)。
groupingBy 可以接受一个第二参数实现多级分组:
Map<Integer, Map<T, List<User>>> map = list.stream().collect(groupingBy(User::getAge, groupingBy(...)));
其中返回的 Map 键为 Integer 类型,值为 Map<T, List<Person>> 类型,即参数中 groupBy(...) 返回的类型
Map<Integer, Integer> map = list.stream().collect(groupingBy(User::getAge, summingInt(User::getAge)));
该例子中,我们通过年龄进行分组,然后 summingInt(User::getAge))
分别计算每一组的年龄总和(Integer),最终返回一个 Map<Integer, Integer>。
根据这个方法,我们可以知道,前面我们写的:
groupingBy(User::getAge)
等同于:
groupingBy(User::getAge, toList())
分区与分组的区别在于,分区是按照 true 和 false 来分的,因此partitioningBy 接受的参数的 lambda 也是 T -> boolean
根据年龄是否小于等于20来分区
Map<Boolean, List<User>> map = list.stream()
.collect(partitioningBy(p -> p.getAge() <= 20));
打印输出
{
false=[User{name='Lucy', age=25}, User{name='Leon', age=21}], true=[User{name='Tom', age=18}]}
同样地 partitioningBy 也可以添加一个收集器作为第二参数,进行类似 groupBy 的多重分区等等操作。
最后,我们再来谈谈效率问题,很多人可能听说过有关 Stream 效率低下的问题。其实,对于一些简单的操作,比如单纯的遍历,查找最值等等,Stream 的性能的确会低于传统的循环或者迭代器实现,甚至会低很多。
但是对于复杂的操作,比如一些复杂的对象归约,Stream 的性能是可以和手动实现的性能匹敌的,在某些情况下使用并行流,效率可能还远超手动实现。好钢用在刀刃上,在适合的场景下使用,才能发挥其最大的用处。
函数式接口的出现主要是为了提高编码开发效率以及增强代码可读性;与此同时,在实际的开发中,并非总是要求非常高的性能,因此 Stream 与 lambda 的出现意义还是非常大的。