Java8新特性——Stream流看这一篇就行辣

前言

作为Java8添加的一个新特性,Stream流提供了一种声明的方式来处理数据。
其基于函数式编程思想,将复杂的语句代码通过简洁的方法调用来表示,让程序员写出的代码更加的高效、简洁并具备可读性。

先来看Javadoc对其的定义:

To perform a computation, stream operations are composed into a stream pipeline. A stream pipeline consists of a source (which might be an array, a collection, a generator function, an I/O channel, etc), zero or more intermediate operations (which transform a stream into another stream, such as filter(Predicate)), and a terminal operation (which produces a result or side-effect, such as count() or forEach(Consumer)).

为了执行计算,流操作被组合成一个流管道。流管道由源(这可能是一个数组,一个集合,一个生成器函数,一个I/O通道,等等),零个或多个中间业务(变换流到另一个流,如 filter(Predicate)),和一个终端操作(产生结果或副作用,如count()或forEach(Consumer))。

可以看到,官方对其的定义是一个处理管道,其将特定的元素经过一定的处理包装后“变为”一个新值并返回。(注意,这里的“变为”并不是真正意义上的改变,steam流并不会改变原有的参数,而是将变化后的新值作为返回值返回)

其将该操作分为三个步骤:

  • 首先是源的创建,确定数据源类型并将其加入流管道中,从而围绕该数据源进行一系列操作;
  • 其次是中间业务,其实也就是你所要进行的筛选、变换操作;
  • 最后是终端操作,输出操作之后的结果。

我们可以把这三个步骤简便的分为源、变换、收集三个操作:
在这里插入图片描述
将一个特定的数据类型,经过一系列的“变换”,转化为一个新的数据并收集起来。

函数式编程

在讲Stream流对象之前,我们先来介绍一下函数式编程思想。

函数式编程是声明式的编程,其主要思想是将一系列的指令操作转为一系列的函数调用
你不需要去提供一些数据处理的指令而只需要声明你想要做的事情,调用对应函数处理即可。

例如你去找出两个数中较大的一个并把它的值赋给另一个变量,不考虑三目运算符的话,正常的写法是:

int a = 1;
int b = 2;
int c = -1;

if(a > b) {
	c = a;	
} else {
	c = b;
}

而用函数来表达的写法为:

int a = 1;
int b = 2;

int c = Math.max(a, b);

这就是函数式编程,逻辑封装在函数中,你不需要考虑你要怎么做,而只需要考虑你要做什么

在 Stream 中,比如我们需要获得一个用户集合中年龄大于等于50的前十个用户并打印
正常的写法如下:

List<Person> filterPersonList = new ArrayList<>();
for (Person person : personList) {
    if (person.getAge() >= 50) {
        filterPersonList.add(person);
    }

    if (filterPersonList.size() == 10) {
        break;
    }
}

filterPersonList.forEach(person -> System.out.println("name: " + person.getName() + ", age: " + person.getAge()));

可以看到,筛选、迭代等操作杂糅在了一起,既暴露了过多细节,又不好进行修改
而使用 Stream ,则写法如下:

personList.stream()
          .filter(person -> person.getAge() >= 70) // 筛选操作
          .limit(10) // 取前10个元素
          .forEach(person -> 
              System.out.println("name: " + person.getName() + ", age: " + person.getAge())); // 打印结果

对比看出,在 Stream 中我们只需要关注如何去做,并将筛选、迭代等操作清楚的分隔开来,以后要做其他变换只需要在其中加入方法即可。

流的类图

类图

流的分类及其方法详解

这里我们主要介绍基本的对象引用流Stream中的方法,并简单介绍对其做了特定类型修改的IntStream、LongStream和DoubleStream

Stream 类

Stream类最常用的是其对各个数组以及集合的处理,它可以很方便的去对数组以及集合中的元素进行筛选、处理、聚合并返回一个全新的数组/集合,其中对该数据类型的处理并不会影响原数组/集合本身

源的创建

对于Stream流常见起始创建方法有3种:

  1. 由集合类进行创建
List<Integer> streamList = new ArrayList<>();
Stream<Integer> integerStream = streamList.stream();
  1. 由数组进行创建
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5)
int[] mArray = {1, 2, 3, 4, 5};
Stream<Integer> stream = Arrays.stream(mArray).boxed();

boxed()方法的作用是装箱操作,将IntStream特定类型的流转化为Stream<Integer>普遍流操作,会在下文进行详细介绍

  1. generate()方法创建
Stream<Double> dStream = Stream.generate(Math::random);

generate() 方法根据传入参数生成一个无限的无序流

中间处理方法

1. filter()方法

该方法接收一个判断式来对流中的元素进行筛选,将流中符合该判断的元素留下,不符合该判断的元素去除,并返回一个包含所有符合该判断的元素的新的流。

例子:

    /**
     * filter方法测试
     */
    private static void filterTest() {
        int[] fList = StreamTestUtils.randomGenerate();

        System.out.println("筛选前的数组:");
        Arrays.stream(fList).forEach(num -> System.out.print(num + ", "));

        System.out.println("\n筛选后的数组:");
        /* 筛选出50以内的数字 */
        Arrays.stream(fList).filter(num -> num <= 50).forEach(num -> System.out.print(num + ", "));
        System.out.println();
    }

randomGenerate()方法用来生成一个区间为[1, 100],大小为10的随机数数组)

控制台输出:

筛选前的数组:
 99, 18, 90, 85, 19, 5, 1, 54, 76, 6, 
筛选后的数组:
 18, 19, 5, 1, 6, 

可以看到,filter()方法成功将数组中小于等于50的数筛选了出来。

2. map()方法

该方法用于对流中的元素进行转换操作,用于对流中的元素进行一系列的变换,返回一个变换后的新的流。

例子:

/**
 * map方法测试
 */
private static void mapTest() {
    int[] fList = StreamTestUtils.randomGenerate();

    System.out.println("转换前的数组:");
    Arrays.stream(fList).forEach(num -> System.out.print(num + ", "));

    System.out.println("\n转换后的数组:");
    /* 将每个数字加100后输出 */
    Arrays.stream(fList).map(num -> num += 100).forEach(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

转换前的数组:
69, 44, 59, 96, 99, 40, 79, 33, 85, 76, 
转换后的数组:
169, 144, 159, 196, 199, 140, 179, 133, 185, 176, 

这里用 map() 方法使数组中的每个元素值增加100后输出新的数组值。
类似的方法还有mapToInt()mapToLong()mapToDouble(),功能相同,只不过返回的是特定类型的流(IntStream、LongStream和DoubleStream)

3. flatMap()方法

该方法用于将流的内容为一个个数组的合并为一整个数组

例子:

/**
* flatMap方法测试
*/
private static void flatMapTest() {
	 List<String[]> isList = new ArrayList<>();
	 for(int i=0; i<3; i++) {
	     String[] rNums = {"1", "2", "3", "4", "5"};
	
	     isList.add(rNums);
	 }
	
	 System.out.println("未合并前集合内容:");
	 isList.forEach(nums -> {
	     Arrays.stream(nums).forEach(num -> System.out.print(num + ", "));
	     System.out.println();
	 });
	
	 List<String> ifList = isList.stream().flatMap(Arrays::stream).collect(Collectors.toList());
	 System.out.println("合并后集合内容:\n" + ifList);
}

控制台输出:

未合并前集合内容:
1, 2, 3, 4, 5, 
1, 2, 3, 4, 5, 
1, 2, 3, 4, 5, 
合并后集合内容:
[1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]

这里生成了三个字符串数组并将其保存在了一个集合内,将该集合映射成流,其中每一个数组对应单个流,使用flatMap()方法进行扁平化处理之后,各个数组合并,所有数组的内容合在一起映射为一个流
相似的方法还有flatToInt()flatToLong()flatToDouble(),分别对应特定类型的流

4. distinct()方法

该方法用于对流中的元素进行去重操作

例子:

/**
 * distinct方法测试
 */
private static void distinctTest() {
    int[] dList = {1, 1, 2, 5, 2, 4, 3, 1};

    System.out.println("去重前的数组:");
    Arrays.stream(dList).forEach(num -> System.out.print(num + ", "));

    System.out.println("\n去重后的数组:");
    Arrays.stream(dList).distinct().forEach(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

去重前的数组:
1, 1, 2, 5, 2, 4, 3, 1, 
去重后的数组:
1, 2, 5, 4, 3, 

要特别注意流中储存的内容是以数组为单位还是以数为单位,若以数组为单位的话调用distinct()是不生效的(比如用split()方法将字符串分割成一个个字符串数组再用 map()方法进行变换),此时需要调用flatMap()方法对流进行扁平化处理,之后再调用distinct()方法去重

5. sorted()方法

该方法用于对流中内容进行排序操作,默认为升序

例子:

/**
 * sorted方法测试
 */
private static void sortedTest() {
    List<Integer> isList = StreamTestUtils.intToList(StreamTestUtils.randomGenerate());

    System.out.println("排序前的数组:");
    isList.forEach(num -> System.out.print(num + ", "));

    System.out.println("\n排序为升序的数组:");
    isList.stream().sorted().forEach(num -> System.out.print(num + ", "));
    System.out.println();

    System.out.println("排序为降序的数组:");
    isList.stream().sorted((o1, o2) -> Integer.compare(o2, o1)).forEach(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

排序前的数组:
79, 77, 36, 89, 32, 80, 12, 56, 78, 55, 
排序为升序的数组:
12, 32, 36, 55, 56, 77, 78, 79, 80, 89, 
排序为降序的数组:
89, 80, 79, 78, 77, 56, 55, 36, 32, 12, 

可传入Comparator表达式来自行拟定排序规则

小tips:对 map 的排序可以按键递增/递减
例子:

/**
 * map排序测试
 */
private static void mapSortedTest() {
    Map<Integer, Integer> map = new HashMap<>();
    Random random = new Random();

    for(int i=0; i<10; i++) {
        map.put(random.nextInt(100)+1, random.nextInt(100)+1);
    }

    System.out.println("原Map为:\n" + map);

    System.out.println("按键递升排序后的Map为:");
    Map<Integer, Integer> kMap = map.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
    System.out.println(kMap);

    System.out.println("按键递减排序后的Map为:");
    Map<Integer, Integer> kMapDown = map.entrySet().stream().sorted(Map.Entry.<Integer, Integer>comparingByKey().reversed()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (oldValue, newValue) -> oldValue, LinkedHashMap::new));
    System.out.println(kMapDown);

控制台输出:

Map为:
{82=33, 34=32, 2=4, 68=8, 25=90, 60=95, 45=21, 62=19, 47=91}
按键递升排序后的Map为:
{2=4, 25=90, 34=32, 45=21, 47=91, 60=95, 62=19, 68=8, 82=33}
按键递减排序后的Map为:
{82=33, 68=8, 62=19, 60=95, 47=91, 45=21, 34=32, 25=90, 2=4}
6. peek()方法

该方法主要用于调试,对于中间流程某个步骤的执行元素情况进行打印输出

例子:

/**
 * peek方法测试
 */
private static void peekTest() {
    int[] pList = StreamTestUtils.randomGenerate();

    System.out.println("原数组:");
    Arrays.stream(pList).sorted().forEach(num -> System.out.print(num + ", "));

    System.out.println("\n流程打印:");
    int sum = Arrays.stream(pList).filter(num -> num <= 50).peek(num -> System.out.print("[" + num + ", ")).map(num -> num+=1).peek(num -> System.out.print(num + "], ")).sum();
    System.out.println("\n和:" + sum);
}

控制台输出:

原数组:
7, 22, 35, 55, 69, 77, 82, 83, 83, 92, 
流程打印:
[22, 23], [7, 8], [35, 36], 
和:67

首先对数组中小于等于50的元素进行筛选,筛选结果为 22735
之后使用peek()方法对筛选结果进行打印查看,打印结果应为:[22,[7,[35,
调用map()方法对流中的每个结果进行加一操作,并再次调用peek()方法进行打印查看,得到最终结果
[22, 23], [7, 8], [35, 36]
此时流中的内容已经变为变换后的内容 23836,所以求和结果为 67

7. limit()方法

该方法用于截取操作,可截取数组的前n个元素

例子:

/**
 * limit方法测试
 */
private static void limitTest() {
    int[] lList = StreamTestUtils.randomGenerate();

    System.out.println("原数组:");
    Arrays.stream(lList).forEach(num -> System.out.print(num + ", "));

    System.out.println("\n截取5个字符数组:");
    Arrays.stream(lList).limit(5).forEach(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

原数组:
69, 57, 13, 46, 66, 29, 78, 76, 95, 7, 
截取5个字符数组:
69, 57, 13, 46, 66, 
8. skip()方法

该方法用于跳过元素操作,可以跳过前n个元素,返回剩余元素组成的流
(若元素数目不满n个,则返回空流)

例子:

/**
 * skip方法测试
 */
private static void skipTest() {
    int[] sList = StreamTestUtils.randomGenerate();

    System.out.println("原数组:");
    Arrays.stream(sList).forEach(num -> System.out.print(num + ", "));

    System.out.println("\n跳过5个字符后数组:");
    Arrays.stream(sList).skip(5).forEach(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

原数组:
55, 13, 36, 43, 1, 5, 64, 11, 75, 57, 
跳过5个字符后数组:
5, 64, 11, 75, 57, 

终端收集方法

1. forEach()/forEachOrdered()方法

该方法用于对流中内容的循环遍历,而 forEachOrdered()方法用于保持原有元素的既定顺序遍历

例子:

/**
 * forEachOrdered方法测试
 */
private static void forEachOrderedTest() {
    int[] fList = StreamTestUtils.randomGenerate();

    System.out.println("原数组:");
    Arrays.stream(fList).forEach(num -> System.out.print(num + ", "));

    System.out.println("\n并行流打印数组:");
    Arrays.stream(fList).parallel().forEach(num -> System.out.print(num + ", "));

    System.out.println("\n并行流顺序打印数组:");
    Arrays.stream(fList).parallel().forEachOrdered(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

原数组:
77, 49, 36, 21, 64, 36, 75, 100, 36, 46, 
并行流打印数组:
75, 36, 100, 49, 77, 36, 46, 64, 36, 21, 
并行流顺序打印数组:
77, 49, 36, 21, 64, 36, 75, 100, 36, 46, 

用普通的forEach()通过并行流打印数组,可以明显看到打印顺序为乱序的
但是利用forEachOrdered()方法即可保持数组的原有顺序打印

2. toArray()方法

主要用于将给定数据源转换为数组返回,空参方法返回的是 Object[] 类型的数组,可以传入一个指定数组类型以返回特定类型数组

例子:

/**
 * toArray方法测试
 */
private static void toArrayTest() {
    List<Integer> tList = StreamTestUtils.intToList(StreamTestUtils.randomGenerate());

    System.out.println("原集合:");
    tList.forEach(num -> System.out.print(num + ", "));

    System.out.println("\n正常方法转换Integer数组:");
    Integer[] tIns = tList.toArray(new Integer[0]);
    Arrays.stream(tIns).forEach(num -> System.out.print(num + ", "));

    System.out.println("\n经过Stream流筛选不大于50的元素后转换的Integer数组:");
    Integer[] tStreamToIns = tList.stream().filter(num -> num <= 50).toArray(Integer[]::new);
    Arrays.stream(tStreamToIns).forEach(num -> System.out.print(num + ", "));
    System.out.println();
}

控制台输出:

原集合:
74, 17, 74, 14, 92, 36, 65, 93, 29, 26, 
正常方法转换Integer数组:
74, 17, 74, 14, 92, 36, 65, 93, 29, 26, 
经过Stream流筛选不大于50的元素后转换的Integer数组:
17, 14, 36, 29, 26, 

集合本身提供了toArray()方法来将集合转化为数组,但是中间如果涉及过滤、变换等操作,该方法就显得不是那么方便,而使用流操作可以直接在中间声明过滤、变换的方法,并在最后用toArray()方法来对操作后的流进行收集操作

3. reduce()方法

该方法主要用于对流中的数据进行归约计算,其接收一个组合两个值之间操作的方法(或单纯对两个数操作的lambda表达式),如min()max()sum()等,并对流中的所有数进行操作,其操作不限于顺序执行

例子:

/**
 * reduce方法测试
 */
private static void reduceTest() {
    int[] rList = StreamTestUtils.randomGenerate();

    System.out.println("原数组:");
    Arrays.stream(rList).forEach(num -> System.out.print(num + ", "));

    System.out.print("\n累加之后的和:");
    System.out.println(Arrays.stream(rList).boxed().reduce(Integer::sum).get());

    System.out.print("加上初始值1000后累加之后的和:");
    System.out.println(Arrays.stream(rList).boxed().reduce(1000, Integer::sum));
}

控制台输出:

原数组:
74, 11, 99, 9, 78, 49, 83, 85, 65, 43, 
累加之后的和:596
加上初始值1000后累加之后的和:1596

该方法会返回一个Optional<T>容器对象,该容器对象是一个基于值的类,可以进行一些简单的如orElse()(如果值不存在则返回默认值)、ifPresent()(如果值存在则执行代码块)等方法,在此处使用该容器对象的get()方法获得其储存的泛型T类型的值

该方法可以接收一个初始值,并将该值加入对流的操作中,此时返回值的类型为传入的初始值的类型,将直接返回该类型的对象
(不带初始值的方法会将第一个值作为初始值)

reduce()不仅仅能接收一个方法,还可以接收一个对数据之间进行操作的lambda表达式,如:

Arrays.stream(rList).boxed().reduce((a, b) -> a + b).get()

// 上述与下列写法作用一致
Arrays.stream(rList).boxed().reduce(Integer::sum).get()

另外,在官方的源码中还有以下一段介绍:

While this may seem a more roundabout way to perform an aggregation compared to simply mutating a running total in a loop, reduction operations parallelize more gracefully, without needing additional synchronization and with greatly reduced risk of data races.

虽然与简单地在循环中改变运行总数相比,这似乎是一种更迂回的方式来执行聚合,但归约操作更优雅地并行化,不需要额外的同步,并且大大降低了数据竞争的风险。

官方介绍说,归约操作可以更优雅地并行化,那么具体是如何操作的呢?

我们先来探究一下,并行流操作结果是否和单线程的操作结果一致,简单的进行一个归约操作:

int[] rList = {1, 2, 3};
System.out.println("原数组:");
Arrays.stream(rList).forEach(num -> System.out.print(num + ", "));

System.out.println("\n主线程执行归约操作:");
System.out.println(Arrays.stream(rList).boxed().reduce((a, b) -> a + b * 2).get());

System.out.println("并行流执行归约操作:");
System.out.println(Arrays.stream(rList).boxed().parallel().reduce((a, b) -> a + b * 2).get());

按照正常来说,计算流程应该为:

  1. a = 1,b = 2, 1 + 2 * 2 = 5
  2. a = 5,b = 3, 5 + 3 * 2 = 11

最终结果应为 11,可真的是这样吗?看下控制台输出:

原数组:
1, 2, 3, 
主线程执行归约操作:
11
并行流执行归约操作:
17

并行流最终计算结果为17,多余的6是哪里出来的?
我们可以将工作流程打印出来,看一下具体并行流是如何工作的:

System.out.println("\n主线程执行归约操作:");
Arrays.stream(rList).boxed().reduce((a, b) -> {
    System.out.println("线程名称:" + Thread.currentThread().getName() + ";元素值:a = " + a + ",b = " + b);
    return a + b * 2;
});
System.out.println("并行流执行归约操作:");
Arrays.stream(rList).boxed().parallel().reduce((a, b) -> {
    System.out.println("线程名称:" + Thread.currentThread().getName() + ";元素值:a = " + a + ",b = " + b);
    return a + b * 2;
});

看一下控制台输出:

主线程执行归约操作:
线程名称:main;元素值:a = 1,b = 2
线程名称:main;元素值:a = 5,b = 3
并行流执行归约操作:
线程名称:main;元素值:a = 2,b = 3
线程名称:main;元素值:a = 1,b = 8

可以看到,单线程下的操作和我们预想的过程一致,可是在并行流下,操作流程却是23先执行,之后再与1进行操作,它的分组方式是(1,(2,3)),到了此时,并行流的操作方式初见端倪

我们将元素个数增多,再来分析它是如何进行分组的:

int[] rList = {1, 2, 3, 4, 5};
System.out.println("原数组:");
Arrays.stream(rList).forEach(num -> System.out.print(num + ", "));

System.out.println("\n并行流执行归约操作:");
Arrays.stream(rList).boxed().parallel().reduce((a, b) -> {
    System.out.println("线程名称:" + Thread.currentThread().getName() + ";元素值:a = " + a + ",b = " + b);
    return a + b * 2;
});

控制台输出:

原数组:
1, 2, 3, 4, 5,  
并行流执行归约操作:
线程名称:ForkJoinPool.commonPool-worker-5;元素值:a = 1,b = 2
线程名称:ForkJoinPool.commonPool-worker-4;元素值:a = 4,b = 5
线程名称:ForkJoinPool.commonPool-worker-4;元素值:a = 3,b = 14
线程名称:ForkJoinPool.commonPool-worker-4;元素值:a = 5,b = 31

可以看到,它的分组方式为:((1,2),(3,(4,5)))
从这里就可以看出,其实并行流计算分组是采用的对半分组,即不断的将数组对半平分,直到结果为两个数一组,而若数组总数为奇数,则后一组比前一组分得元素个数多 1
以这个逻辑来想,对于上述数组的分组过程其实为:

  1. (1,2) ,(3, 4, 5)
  2. (1,2) ,(3,(4,5))

我们可以进一步增加元素数量来验证这个分组逻辑的正确性:

// 数组元素为 1, 2, 3, 4, 5, 6, 7 下,控制台输出结果

并行流执行归约操作:
线程名称:main;元素值:a = 4,b = 5
线程名称:ForkJoinPool.commonPool-worker-2;元素值:a = 6,b = 7
线程名称:ForkJoinPool.commonPool-worker-2;元素值:a = 14,b = 20
线程名称:ForkJoinPool.commonPool-worker-3;元素值:a = 2,b = 3
线程名称:ForkJoinPool.commonPool-worker-3;元素值:a = 1,b = 8
线程名称:ForkJoinPool.commonPool-worker-3;元素值:a = 17,b = 54

可以从上述流程看到,其分组流程为:

  1. (1,2,3,(4,5),6,7)
  2. (1,2,3,(4,5),(6,7))
  3. (1,2,3,((4,5),(6,7)))
  4. (1,(2,3),((4,5),(6,7)))
  5. (1,(2,3)),((4,5),(6,7))

与我们猜想的一致,在不断的进行对半分组之后进行计算操作
(计算顺序不一定一致,但分组是固定的,所以最终结果是一致的,如在上述流程中,可能先执行 (2, 3),再执行 (6, 7), (4, 5) 等)

由于并行流是多线程运行,所以当方法带有初始值时,各个线程的累加器都会带有该初始值,会对每个元素进行一次初始值的计算转换
比如对于 a + b * 2 这个操作来说,对于数组{1, 2, 3},如果带有初始值1进行计算
正常流程应为:
1 + 1 * 2 + 2 * 2 + 3 * 2 = 13

而对于并行流,首先对每个元素利用初始值进行计算:
{1, 2, 3} => {1+1*2, 1+2*2, 1+3*2} => {3, 5, 7}
之后再分组计算:
3 + (5 + 7 * 2) * 2 = 41

可能会有人好奇开启的线程数量,我们可以用以下两个方法来查看总共有多少线程运行:
Runtime.getRuntime().availableProcessors() 该方法可以 输出CPU可用核心数
ForkJoinPool.getCommonPoolParallelism() 该方法可以 输出公共池默认并发线程数,通常是比CPU可用核心数少1,原因是CPU可用核心数包括了主线程,主线程要占用一个名额

System.out.println("CPU可用核心数:" + Runtime.getRuntime().availableProcessors());
System.out.println("公共池默认并发线程数:" + ForkJoinPool.getCommonPoolParallelism());

// 控制台输出
CPU可用核心数:6
公共池默认并发线程数:5

因为我电脑的CPU可用核心为6,所以公共池默认并发线程数为5

4. collect()方法

该方法主要对数组内元素进行一个可变归约操作,通常用于对数组元素的收集转化与归纳

比如可以用来将数组元素累积到集合中:

/* 基本类型数组 -> 集合 */
int[] arr = {2, 5, 1, 3, 4};
List<Integer> list = Arrays.stream(arr).boxed().collect(Collectors.toList());

也可以对数组元素进行归纳:

List<Person> personList = Arrays.asList(
        new Person("a", "北京"),
        new Person("b", "北京"),
        new Person("c", "上海")
);
Map<String, List<Person>> peopleByCity
        = personList.stream().collect(Collectors.groupingBy(Person::getCity));
System.out.println(peopleByCity);

控制台输出:

{上海=[Person{name='c', city='上海'}], 北京=[Person{name='a', city='北京'}, Person{name='b', city='北京'}]}

究其根本是利用了Collectors类的方法,其会返回一个Collector带有泛型的收集器,利用collect()方法对其中数据进行提取

小tips:可以用 CollectorstoMap 方法转换 map 并进行比如去除重复键的操作
例子:

System.out.println("==================重复键测试==================");
List<User> users = Arrays.asList(
        new User(1, random.nextInt(100)+1),
        new User(7, random.nextInt(100)+1),
        new User(4, random.nextInt(100)+1),
        new User(1, random.nextInt(100)+1),
        new User(2, random.nextInt(100)+1),
        new User(2, random.nextInt(100)+1),
        new User(9, random.nextInt(100)+1),
        new User(8, random.nextInt(100)+1),
        new User(7, random.nextInt(100)+1),
        new User(1, random.nextInt(100)+1)
);

System.out.println("未处理重复键:");
System.out.print("{");
users.forEach(user -> System.out.print(user.getKey() + "=" + user.getValue() + ", "));

System.out.println("}\n处理重复键后,保留前者:");
Map<Integer, Integer> repPreMap = users.stream().collect(Collectors.toMap(User::getKey, User::getValue, (oldValue, newValue) -> oldValue));
System.out.println(repPreMap);

System.out.println("处理重复键后,保留后者:");
Map<Integer, Integer> repNexMap = users.stream().collect(Collectors.toMap(User::getKey, User::getValue, (oldValue, newValue) -> newValue));
System.out.println(repNexMap);

控制台输出:

==================重复键测试==================
未处理重复键:
{1=73, 7=19, 4=37, 1=51, 2=69, 2=82, 9=57, 8=100, 7=7, 1=84, }
处理重复键后,保留前者:
{1=73, 2=69, 4=37, 7=19, 8=100, 9=57}
处理重复键后,保留后者:
{1=84, 2=82, 4=37, 7=7, 8=100, 9=57}

toMap()前两个入参分别为keyvalue,而第三项是对重复键的处理,上述代码分别返回oldValuenewValue分别表示从前往后比较只保留第一个和只保留最后一个,如果返回oldValue + newValue则返回的是对值的累加

5. max()/min()/count()方法

这三个方法分别可获得流中元素的最大值、最小值以及长度

例子:

/**
 * 用Stream流模拟Math类中的count、max、min方法
 */
private static void mathTest() {
    int[] mList = StreamTestUtils.randomGenerate();
    System.out.println("原数组:");

    Arrays.stream(mList).sorted().forEach(num -> System.out.print(num + ", "));

    int count = (int) Arrays.stream(mList).count();
    int max = Arrays.stream(mList).max().orElse(-1);
    int min = Arrays.stream(mList).min().orElse(-1);

    System.out.println("\n长度:" + count);
    System.out.println("最大值:" + max);
    System.out.println("最小值:" + min);
}

控制台输出:

原数组:
15, 19, 30, 39, 48, 53, 92, 94, 95, 97, 
长度:10
最大值:97
最小值:15

min()max()方法分别接收一个比较器,通过比较器来判断传参类型的比较方式(比如传入的Person类按名字取得最大/最小值,则比较器最后返回的是名字的比较值),并返回一个Optional<T>

这两个方法要求调用Optional<T>类的isPresent()判断流数据中是否存在该值,所以一般都会在其后跟orElse()方法指定不存在时的默认值
(这里为了方便所以调用的是IntStream特定流的方法,本质区别是将泛型设为Integer,即返回OptionalInt类,默认的比较器为比较两数之间的大小)

6. allMatch()/anyMatch()/noneMatch()

这三个方法是查看流中元素是否匹配某个特定逻辑
allMatch():全部匹配才返回true
anyMatch():任意元素匹配就返回true
noneMatch():全部不匹配才返回true

例子:

/**
 * match()方法测试
 */
private static void matchTest() {
    int[] mList = StreamTestUtils.randomGenerate();
    System.out.println("原数组:");
    Arrays.stream(mList).sorted().forEach(num -> System.out.print(num + ", "));

    System.out.println("\n全部匹配测试:");
    boolean match = Arrays.stream(mList).allMatch(num -> num > 0);
    System.out.println("结果:" + match);
    System.out.println("部分匹配测试:");
    match = Arrays.stream(mList).anyMatch(num -> num > 50);
    System.out.println("结果:" + match);
    System.out.println("不匹配测试:");
    match = Arrays.stream(mList).noneMatch(num -> num < 0);
    System.out.println("结果:" + match);
}

控制台打印:

原数组:
3, 8, 8, 46, 47, 52, 59, 67, 68, 77, 
全部匹配测试:
结果:true
部分匹配测试:
结果:true
不匹配测试:
结果:true

数组全部大于0,且有至少一项大于50,并且全部大于等于0,所以三个均返回true

7. findFirst()/findAny()

这两个方法返回流中数据的第一个元素,注意是流中数据操作的顺序,也就是说该流操作会以此数据作为第一个,而不是传入流的数据的顺序

例子:

/**
 * find()方法测试
 */
private static void findTest() {
    int[] mList = StreamTestUtils.randomGenerate();
    System.out.println("原数组:");
    Arrays.stream(mList).sorted().forEach(num -> System.out.print(num + ", "));

    int firstNum = Arrays.stream(mList).findFirst().orElse(-1);
    System.out.println("\n第一个数为:" + firstNum);

    int anyNum = Arrays.stream(mList).findAny().orElse(-1);
    System.out.println("任意一个数为:" + anyNum);

}

控制台打印:

原数组:
12, 19, 35, 41, 56, 58, 62, 67, 71, 80, 
第一个数为:41
任意一个数为:41

findFirst()findAny()方法的主要区别是前者对并行流的限制很多,而后者对并行流的限制较少,一般采用findAny()方法,因为使用场景较少,所以这里不做深究,有兴趣的可以自行研究

其他特定类型的流

其他特定类型的流主要有IntStreamDoubleStreamLongStream三种

关于这三种基本类型要讲的其实不多,因为其与Stream流的基本方法大致相同,只是对于个别类似于map()min()max()等方法做了自身的优化,比如在Stream流中此类方法一般返回的是特定泛型,有的还需要自行加入比较器,而在IntStream流中,指定了泛型为Integer类型,且比较器已经默认生成好

其次就是boxed()方法,该方法是将特定类型的流转换为Stream流使用,比如当你使用Arrays.stream()传入的是一个int[]数组类型的话,那么返回的就是IntStream类型的流,而你想要调用Stream流中的方法,就需要boxed()进行转换

此外,如IntStream中也可以使用asLongStream()asDoubleStream()方法转换为其他类型的流

另外就是在IntStreamLongStream中新加入了方法range(),可以对流中元素进行截取操作,左闭右开

后续要考虑的问题及总结

本篇文章只是讲解了浅层的一些知识,对于一些深层次的东西并未过多探究

比如:

  • 并行流线程安全是怎么保证的?
  • 各个方法具体是怎么实现的,时间复杂度如何?
  • 很多方法其实在collection中已经有实现,为什么还要引入Stream
  • 类的继承链是怎么设计的以及为什么这么设计?
  • Stream作为懒加载,终端操作时才执行计算是为什么,怎么做到的?

等等,诸如此类问题,以后我会专门发篇文章探究其底层源码与逻辑

而对于平常是否要用到Stream流来代替所有的操作,我的意见是,Stream流会使得代码更清晰、方便,但可能不够灵活,有些需要特定算法或者效率优先的环境下,还是要斟酌选择

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值