Java8的Stream API使用


前言

这次想介绍一下Java Stream的API使用,最近在做一个新的项目,然后终于可以从老项目的祖传代码坑里跳出来了。项目用公司自己的框架搭建完成后,我就想着把JDK版本也升级一下吧(之前的项目,最高就能用JDK7),但是后来发现公司的项目部署打包平台最高只支持到JDK8。那好吧,既然就支持到JDK8,也能满足日常需求了(要啥自行车),升级到JDK8后,在搭建完项目架构后,就开始写一些基础逻辑。其中就用到了一些JDK8的Stream。但是我的同事在看我的代码的时候表示看不懂。确实,这个我也承认,Lambda表达式虽然代码简洁,但是不会用的人会觉得它的可读性不是太好。所以这次就结合自己使用经验来介绍一下Java Stream的一些功能。

从遍历到Stream操作

Oracle 公司于 2014 年 3 月 18 日发布 Java 8,Java8主要是在原来面向对象的基础上增加了函数式编程的能力。这样就出现了在Java中使用Lambda表达式,将一个函数作为方法的参数来进行传递。Java8的Stream就是典型的例子,Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。

例子:

List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(4);
numbers.add(8);
numbers.add(16);
numbers.add(19);
numbers.add(27);
numbers.add(23);
numbers.add(99);
numbers.add(15);
numbers.add(32);
numbers.add(5);
numbers.add(232);
numbers.add(56);
int count = 0;
for(Integer i:numbers){
if(i>20){
count++;
}
}
System.out.println("count:"+count);

如上遍历的代码转换成使用Stream的API来实现如下:

long count = numbers.stream().filter(i->i>20).count();
System.out.println("count:"+count);

正常的遍历用Stream一行就可以实现了。

下面是一个使用了Stream API实现的流程图。

转换成Java代码就是

Integer transactionsIds =
                roomList.stream()
                        .filter(b -> b.getLength() == 10)
                        .sorted((x,y) -> x.getHigh() - y.getHigh())
                        .mapToInt(Room::getWidth).sum();

创建Stream

Arrays.stream()

当在日常编程中面对的是一个数组,也可以使用Arrays.stream()方法来使用Stream

Integer[] array = new Integer[]{3,4,8,16,19,27,23,99,76,232,33,96};
long count = Arrays.stream(array).filter(i->i>20).count();

Stream.of()

当面对数组时除了可以使用Arrays.stream()方法外,还可以使用Stream将需要的数组转成Stream。这个方法不但支持传入数组,将数组转成Stream,也支持传入多个参数,将参数最终转成Stream

Integer[] array = new Integer[]{3,4,8,16,19,27,23,99,76,232,33,96};
long count = Stream.of(array).filter(i->i>20).count();
long sum = Stream.of(12,77,59,3,654).filter(i->i>20).mapToInt(Integer::intValue).sum();
System.out.println("count:"+count+",sum:"+sum);

其实Stream.of()也是调用的Stream.of()方法来实现的。

Stream.generate()

Stream接口有两个用来创建无限Stream的静态方法。generate()方法接受一个参数函数,可以使用类似如下代码来创建一个你需要的Stream。

Stream<String> stream = Stream.generate(() -> "test").limit(10);
String[] strArr = stream.toArray(String[]::new);
System.out.println(Arrays.toString(strArr));

运行结果

[test, test, test, test, test, test, test, test, test, test]

Stream.iterate()

Stream接口的另一用来创建无限Stream的静态方法就是iterate()方法。iterate()方法也是接受一个参数函数,可以用类似如下代码来创建一个你需要的Stream。

Stream<BigInteger> bigIntStream = Stream.iterate(BigInteger.ZERO, n -> n.add(BigInteger.TEN)).limit(10);
BigInteger[] bigIntArr = bigIntStream.toArray(BigInteger[]::new);
System.out.println(Arrays.toString(bigIntArr));

运行结果

[0, 10, 20, 30, 40, 50, 60, 70, 80, 90]

Collection.stream()

这个就是最常见的Stream了。因为Collection是Java中集合接口的父接口,Java中的集合都继承或实现了此接口。所以Java中的集合都可以使用此方法来创建一个Stream;

      /**
        * @see     Set
        * @see     List
        * @see     Map
        * @see     SortedSet
        * @see     SortedMap
        * @see     HashSet
        * @see     TreeSet
        * @see     ArrayList
        * @see     LinkedList
        * @see     Vector
        * @see     Collections
        * @see     Arrays
        * @see     AbstractCollection
        * @since 1.2
        */
        public interface Collection<E> extends Iterable<E> {
            /**
             * Returns a sequential {@code Stream} with this collection as its source.
             *
             * 

This method should be overridden when the {@link #spliterator()} * method cannot return a spliterator that is {@code IMMUTABLE}, * {@code CONCURRENT}, or late-binding. (See {@link #spliterator()} * for details.) * * @implSpec * The default implementation creates a sequential {@code Stream} from the * collection's {@code Spliterator}. * * @return a sequential {@code Stream} over the elements in this collection * @since 1.8 */ default Stream<E> stream() { return StreamSupport.stream(spliterator(), false); } }

例子

List<Integer> numbers = new ArrayList<>();
numbers.add(3);
numbers.add(4);
numbers.add(8);
numbers.add(16);   
numbers.stream().forEach(number->{
    System.out.println(number);
});

StreamSupport.stream()

通过查看Collection.stream()的方法,我们可以看出来,Colleciton.stream()其实是调用了StreamSupport.stream()来实现的。所以我们也可以使用StreamSupport.stream()来创建一个Stream。当我们面对的是一个迭代器的时候,使用StreamSupport.stream()就可以创建一个Stream。第一个参数是传入一个迭代器,第二个参数是true代表使用并行来进行处理。false代表串行来处理Stream。

List<Integer> numbers = new ArrayList<>();
numbers.add(3); numbers.add(4); numbers.add(8); numbers.add(16); numbers.add(19); numbers.add(27); numbers.add(23); Spliterator<Integer> integers = numbers.spliterator(); StreamSupport.stream(integers,false).forEach(number->{   System.out.println(number); });

流的转换

filter方法

从名字上就能看出来,这是一个Stream的过滤转换,此方法会生成一个新的流,其中包含符合某个特定条件的所有元素。

List<Integer> integerList = Lists.newArrayList();
integerList.add(15);
integerList.add(32);
integerList.add(5);
integerList.add(232);
integerList.add(56);
List<Integer> after = integerList.stream()
                    .filter(i->i>50)
                    .collect(Collectors.toList());
System.out.println(after);

运行结果:

[232, 56]

map方法

map方法指对一个流中的值进行某种形式的转换。需要传递给它一个转换的函数作为参数。

List<Integer> integerList = Lists.newArrayList();
integerList.add(15);
integerList.add(32);
integerList.add(5);
integerList.add(232);
integerList.add(56);
//将Integer类型转换成String类型
List<String> afterString = integerList.stream()
                .map(i->String.valueOf(i)).collect(Collectors.toList());
System.out.println(afterString);

flatMap方法

上面用map方法进行流转换的时候,是对每个元素应用一个函数,并将返回的值收集到一个新的流中。但是如果有一个函数,它返回的不是一个值,而是一个包含多个值的流。但是你需要的是一个包含多个流中的元素的集合。

例如

List<Integer> oneList = Lists.newArrayList(),
twoList = Lists.newArrayList();
oneList.add(34);
oneList.add(23);
oneList.add(87);

twoList.add(29);
twoList.add(48);
twoList.add(92);
Map<String,List<Integer>> testMap = Maps.newHashMap();
testMap.put("1",oneList);
testMap.put("2",twoList);
//返回的是一个流的集合,但是我需要的是List这样一个集合
List<Stream<Integer>> testList = testMap.values().stream()
                    .map(number->number.stream()).collect(Collectors.toList());

这个时候就应该使用flatMap将多个流进行合并,然后再收集到一个集合中。

List<Integer> testList = testMap.values().stream()
                .flatMap(number->number.stream()).collect(Collectors.toList());

limit方法和skip方法

limit(n)方法会返回一个包含n个元素的新的流(若总长小于n则返回原始流)。

List<Integer> myList = Lists.newArrayList();
myList.add(1);
myList.add(2);
myList.add(3);
myList.add(4);
myList.add(5);
myList.add(6);
List<Integer> afterLimit = myList.stream().limit(4).collect(Collectors.toList());
System.out.println("afterLimit:"+afterLimit);

skip(n)方法正好相反,它会丢弃掉前面的n个元素。

List<Integer> afterSkip = myList.stream().skip(4).collect(Collectors.toList());
System.out.println("afterSkip:"+afterSkip);

运行结果:

afterLimit:[1, 2, 3, 4]
afterSkip:[5, 6]

用limit和skip方法一起使用就可以实现日常的分页功能:

List<Integer> pageList = myList.stream()
                  .skip(pageNumber*pageSize)
                  .limit(pageSize).collect(Collectors.toList());

distinct方法和sorted方法

上面介绍的流的转换方法都是无状态的。即从一个已经转换的流中取某个元素时,结果并不依赖于之前的元素。除此之外还有两个方法在转换流时是需要依赖于之前流中的元素的。一个是distinct方法一个是sorted方法。

distinct方法会根据原始流中的元素返回一个具有相同顺序、去除了重复元素的流,这个操作显然是需要记住之前读取的元素。

List<Integer> myTestList = Lists.newArrayList();
myTestList.add(10);
myTestList.add(39);
myTestList.add(10);
myTestList.add(78);
myTestList.add(10);
List<Integer> distinctList = myTestList.stream()
                        .distinct().collect(Collectors.toList());
System.out.println("distinctList:"+distinctList);

运行结果:

distinctList:[10, 39, 78]

sorted方法是需要遍历整个流的,并在产生任何元素之前对它进行排序。因为有可能排序后集合的第一个元素会在未排序集合的最后一位。

List<Integer> myTestList = Lists.newArrayList();
myTestList.add(39);
myTestList.add(78);
myTestList.add(10);
myTestList.add(22);
myTestList.add(56);
List<Integer> sortList = myTestList.stream()
                .sorted(Integer::compareTo).collect(Collectors.toList()); System.out.println("sortList:"+sortList);

运行结果:

sortList:[10, 22, 39, 56, 78]

聚合操作

前面已经介绍了流的创建和转换,下面介绍流的聚合,聚合是指将流汇聚为一个值,以便在程序中使用。聚合方法都是终止操作。

max方法和min方法

在前面的代码例子中使用的count方法和sum方法都属于流从聚合方法。还有两个聚合方法是max方法和min方法,分别返回流中最大值和最小值。


List<Integer> hearList = Lists.newArrayList();
hearList.add(15);
hearList.add(32);
hearList.add(5);
hearList.add(232);
hearList.add(56);
hearList.add(29);
hearList.add(94);
Integer maxItem = hearList.stream().max(Integer::compareTo).get();
Integer minItem = hearList.stream().min(Integer::compareTo).get();
System.out.println("max:"+maxItem+",min:"+minItem);

运行结果:

max:232min:5

findFirst方法

findFirst方法返回非空集合中的第一个值,它通常与filter方法结合起来使用。

List<Integer> hearList = Lists.newArrayList();
hearList.add(15);
hearList.add(32);
hearList.add(5);
hearList.add(232);
hearList.add(56);
hearList.add(29);
hearList.add(104);
Integer first = hearList.stream().filter(i->i>100).findFirst().get();

findAny方法

findAny方法可以在集合中只要找到任何一个所匹配的元素,就返回,此方法在对流并行执行时十分有效(任何片段中发现第一个匹配元素都会结束计算,串行流中和findFirst返回一样)。

Integer anyItem = hearList.parallelStream().filter(i->i>100).findAny().get();

anyMatch方法

anyMatch方法可以判定集合中是否还有匹配的元素。返回结果是一个boolean类型值。

boolean isHas = hearList.parallelStream().anyMatch(i->i>100);

allMatch方法和noneMatch方法

allMatch方法和noneMatch方法,分别在所有元素匹配和没有元素匹配时返回true。

boolean allHas = hearList.parallelStream().allMatch(i->i>100);
boolean noHas = hearList.parallelStream().noneMatch(i->i>100);

虽然这些方法总是会检查整个流,但是仍然可以通过并行执行来提高速度。 

reduce方法

reduce方法是将流中的元素进行进一步计算的方法。

List<Integer> hearList = Lists.newArrayList();
hearList.add(15);
hearList.add(32);
hearList.add(5);
hearList.add(232);
hearList.add(56);
hearList.add(29);
hearList.add(104);
//求和
Integer sum = hearList.stream().reduce((x,y)->x+y).get();
System.out.println("sum:"+sum);
//简化一下,求和
sum = hearList.stream().reduce(Integer::sum).get();
System.out.println("sum:"+sum);
//含有初始标识的,求和
sum = hearList.stream().reduce(0,(x,y)->x+y);
System.out.println("sum:"+sum);
//对元素的长度进行求和( (total,y)->total+y.toString().length(),类似于一个累加器,会被重复调用)
sum = hearList.stream().reduce(0,(total,y)->total+y.toString().length(),(total1,total2)->total1+total2);
System.out.println("sum:"+sum);
//简化一下,对元素长度进行求和。
sum = hearList.stream().map(Objects::toString).mapToInt(String::length).sum();
System.out.println("sum:"+sum);

运行结果

sum:473
sum:473
sum:473
sum:15
sum:15

收集结果

当处理完流之后,通常是想查看一下结果,而不是将他们聚合为一个值。Collectorts类为我们提供了常用的收集类的各个工厂方法。

收集到集合

例如前面的例子用的要将一个流收集到一个List中,只需要这样写就可以。

List<Integer> thereList = hereList.stream().collect(Collectors.toList());

收集到Set中可以这样用

Set<Integer> thereSet = hereList.stream().collect(Collectors.toSet());

收集到Set时,控制Set的类型,可以这样。

TreeSet<Integer> treeSet = hereList.stream()
                    .collect(Collectors.toCollection(TreeSet::new));

拼接

将字流中的字符串连接并收集起来。

String resultString = stringList.stream().collect(Collectors.joining());

在将流中的字符串连接并收集起来时,想在元素中介添加分隔符,传递个joining方法即可。

String resultString = stringList.stream().collect(Collectors.joining(","));

当流中的元素不是字符串时,需要先将流转成字符串流再进行拼接。

String hereResultString = hereList.stream()
                .map(String::valueOf).collect(Collectors.joining(","));

收集聚合

分别收集流的总和、平均值、最大值或者最小值。

List<Integer> hereList = Lists.newArrayList();
hereList.add(15);
hereList.add(32);
hereList.add(5);
hereList.add(232);
hereList.add(56);
hereList.add(29);
hereList.add(104);

//总和、平均值,最大值,最小值
int sum = hereList.stream().collect(Collectors.summingInt(Integer::intValue));
Double ave = hereList.stream().collect(Collectors.averagingInt(Integer::intValue));
Integer max = hereList.stream().collect(Collectors.maxBy(Integer::compare)).get();
Integer min = hereList.stream().collect(Collectors.minBy(Integer::compare)).get();
System.out.println("sum:"+sum+",ave:"+ave+",max:"+max+",min:"+min);

运行结果:

sum:473,ave:67.57142857142857,max:232,min:5

一次性收集流中的结果,聚合为一个总和,平均值,最大值或最小值的对象。

IntSummaryStatistics summaryStatistics = hereList.stream()
                          .collect(Collectors.summarizingInt(Integer::intValue)); System.out.println(summaryStatistics);

运行结果:

IntSummaryStatistics{count=7, sum=473, min=5, average=67.571429, max=232}

将结果集收集到Map

当我们希望将集合中的元素收集到Map中时,可以使用Collectors.toMap方法。这个方法有两个参数,用来生成Map的key和value。

例如将一个Room对象的high作为键width作为值

Map<Integer,Integer> hwMap = roomList.stream()
                        .collect(Collectors.toMap(Room::getHigh, Room::getWidth));

但是通常还是以具体元素作为值的情况多,可以使用Function.identity()来获取实际元素。

Map<Integer,Room> roomMap = roomList.stream()
                        .collect(Collectors.toMap(Room::getHigh, Function.identity()));

如果多个元素拥有相同的键,在收集结果时会抛出java.lang.IllegalStateException异常。可以使用第三个参数来解决,第三个参数用来确定当出现键冲突时,该如何处理结果,如果当出现键冲突时只保留一个并且是保留已经存在的值时,就是如下方式。

Map<Integer,Room> rMap = roomList.stream()
                .collect(Collectors.toMap(Room::getHigh, Function.identity(),(nowValue,newValue)->nowValue));

如果想指定生成的Map类型,则还需要第三个参数。

TreeMap<Integer,Room> roomTreeMap = roomList.stream()
                .collect(Collectors.toMap(Room::getHigh, 
            Function.identity(),(nowValue,newValue)->newValue,TreeMap::new));

注意:每个toMap方法,都会有一个对应的toConCurrentMap方法,用来生成一个并发Map。

分组分片

在一个集合中,对具有相同特性的值进行分组是一个很常见的功能,在Stream的API中也提供了相应的方法。

分组

还是上面的例子,将一个Room对象集合按照高度分组。

List<Room> roomList = Lists.newArrayList(
new Room(11,23,56),
new Room(11,84,48),
new Room(22,46,112),
new Room(22,75,62),
new Room(22,56,75),
new Room(33,92,224));

Map<Integer,List<Room>> groupMap = roomList.stream().collect(Collectors.groupingBy(Room::getHigh));
System.out.println("groupMap:"+groupMap);

运行结果:

groupMap:{33=[Room(high=33, width=92, length=224)], 
22=[Room(high=22, width=46, length=112), Room(high=22, width=75, length=62), Room(high=22, width=56, length=75)],
11=[Room(high=11, width=23, length=56), Room(high=11, width=84, length=48)]}

分片 

当分类函数是一个返回布尔值的函数时,流元素会被分为两组列表:一组是返回true的元素集合,另一组是返回false的元素集合。这种情况适用partitoningBy方法会比groupingBy更有效率。

例如我们将房间集合分为两组,一组是高度为22的房间,另一组是其他房间。

Map<Boolean,List<Room>> partitionMap = roomList.stream()
                .collect(Collectors.partitioningBy(room->room.getHigh()==22));

运行结果:

partitionMap:{false=[Room(high=11, width=23, length=56), Room(high=11, width=84, length=48), Room(high=33, width=92, length=224)],
true=[Room(high=22, width=46, length=112), Room(high=22, width=75, length=62), Room(high=22, width=56, length=75)]}

扩展功能

下面要介绍的这些方法功能,无论是groupingBy方法还是partitioningBy方法都是支持的。

counting方法会返回收集元素的总个数。

Map<Integer,Long> countMap = roomList.stream()
           .collect(Collectors.groupingBy(Room::getHigh,Collectors.counting()));

summing(Int|Long|Double)方法接受一个取值函数作为参数,来计算总和。

Map<Integer,Integer> sumMap = roomList.stream().
                collect(Collectors.groupingBy(Room::getHigh,Collectors.summingInt(Room::getWidth)));

maxBy方法和minBy方法接受比较器作为参数来计算最大值和最小值。

取出分组中宽度最大和最小的房间。

Map<Integer, Optional<Room>> maxMap = roomList.stream().
                collect(Collectors.groupingBy(Room::getHigh,
                        Collectors.maxBy(Comparator.comparing(Room::getWidth))
                ));
Map<Integer, Optional<Room>> minMap = roomList.stream().
                collect(Collectors.groupingBy(Room::getHigh,
                        Collectors.minBy(Comparator.comparing(Room::getWidth))
                ));

System.out.println("maxMap:"+ JSON.toJSONString(maxMap));
System.out.println("minMap:"+JSON.toJSONString(minMap));

运行结果:

maxMap:{33:{"high":33,"length":224,"width":92},22:{"high":22,"length":62,"width":75},11:{"high":11,"length":48,"width":84}}
minMap:{33:{"high":33,"length":224,"width":92},22:{"high":22,"length":112,"width":46},11:{"high":11,"length":56,"width":23}}


mapping方法会将结果应用到另一个收集器上。

取出分组中宽度最大的房间的宽度。

Map<Integer, Optional<Integer>> collect = roomList.stream().collect(Collectors.groupingBy(Room::getHigh,
                Collectors.mapping(Room::getWidth,
                        Collectors.maxBy(Comparator.comparing(Integer::valueOf)))));

System.out.println("collect:"+JSON.toJSONString(collect));

运行结果:

collect:{33:92,22:75,11:84}

无论groupingBy或是mapping函数,如果返回类型是int、long、double都可以将元素收集到一个summarystatistics对象中,然后从每组的summarystatistics对象中取出函数值的总和、平均值、总数、最大值和最小值。

Map<Integer,IntSummaryStatistics> summaryStatisticsMap = roomList.stream()
                .collect(Collectors.groupingBy(Room::getHigh,
                Collectors.summarizingInt(Room::getWidth)));

System.out.println("summaryStatisticsMap:"+summaryStatisticsMap);

运行结果:

summaryStatisticsMap:{33=IntSummaryStatistics{count=1, sum=92, min=92, average=92.000000, max=92}, 
22=IntSummaryStatistics{count=3, sum=177, min=46, average=59.000000, max=75},
11=IntSummaryStatistics{count=2, sum=107, min=23, average=53.500000, max=84}}

多级分组

上面的例子我们都是按一个条件进行的一级分组,其实groupingBy是支持多级分组的。

例如第一级我们将房间按照高度分组,第二级按照宽度分组。

Map<Integer,Map<Integer,List<Room>>> multistageMap = roomList.stream().collect(
          Collectors.groupingBy(Room::getHigh,Collectors.groupingBy(Room::getWidth))); System.out.println("multistageMap:"+JSON.toJSONString(multistageMap));

运行结果:

{
    "11": {
        "23": [
            {"high": 11,"length": 56,"width": 23}
        ],
        "84": [
            {"high": 11,"length": 48,"width": 84}
        ]
    },
    "22": {
        "46": [
            {"high": 22,"length": 112,"width": 46}
        ],
        "56": [
            {"high": 22,"length": 75,"width": 56}
        ],
        "75": [
            {"high": 22,"length": 62,"width": 75}
        ]
    },
    "33": {
        "92": [
            {"high": 33,"length": 224,"width": 92}
        ]
    }
}

并行流

Stream的建立,使得并行计算变得容易,但是并行流在使用的时候也是需要注意的。

首先,必须是一个并行流,只要在终止方法执行时,流处于并行模式,那么所有的流操作就都会并行执行。

Stream.of(roomList).parallel();

parallel方法可以将任意的串行流转换为一个并行流。

其次要确保传递给并行流操作的函数是线程安全的。

int[] words = new int[23];
Stream.of(roomList).parallel().forEach(s->{
     if(s.size()<10){
           words[s.size()]++;
     }
});

上面这个例子中的代码就是错误的,传递给并行流的操作并不是线程安全的。可以改为AtomicInteger的对象数组来作为计数器。

我们使在处理集合数据量较大的时候才能体现出并行流的优势,并且目的是为了在保证线程安全的情况下,提升效率,利用多核CPU的资源。

小扩展

使用Stream的API时,在遍历或处理流的过程中当引用外部变量的时候会默认的将变量当成fianl变量来处理。所以有些同学就会觉得在遍历的过程中取不出来集合的索引。其实可以换一种思想可以只遍历集合索引,然后在遍历中取值。

IntStream.range(0,roomList.size()).forEach(i->{
       System.out.println(roomList.get(i));
});