Java中的Stream流

252 阅读48分钟

函数和Stream流

1.函数的几种概念

1.1 合格函数

跟数学上的函数概念类似,对于一个输入x,都有一个唯一对应的输出y与之对应

程序上的(合格)函数同理,对于每一个(组)相同的输入,其输出都是相同的

满足以上条件的函数,就是合格的函数

下面就是一个合格的函数,如果参数a、b相同,无论调用多少次函数,返回值都是一样的

int fun1(int a, int b) {
    return a + b;
}

下面就是一个不合格的函数,函数的返回值除了跟输入参数有关,还跟外部的一个变量c有关

即使参数a、b相同,如果c的值被改变,那么函数返回值也会改变

int c = 10;
int fun2(int a, int b) {
    return a + b + c;
}

变量c变成不可变变量,那么fun2就是合格函数

在一些面向对象语言中(比如Java),没有函数的概念,取而代之的是方法,其本质是一样的,都是一个接收输入返回输出的实体

对于下面的实例方法getName,这是一个合格的函数吗?

class Person{
    final String name;
    String getName() {
        return name;
    }
}

表明上看,如果Person对象改变了,getName的返回值也会变化,并不是一个合格函数

但实际getName是合格函数,因为Java的实例方法,有一个隐藏参数this表示当前对象,对于同一个this,调用其getName方法返回值都是一样的

Java的方法就是函数

总结:

函数表示一种规则,规则的特性是不会变化

合格函数的要求:

  • 输入参数相同,输出必然相同

  • 如果函数内部引用了外部变量,那么这个外部变量必须是不可变的

1.2 *函数对象

函数表示一种规则,是一种虚无缥缈的东西

在程序中传播函数,必须将其变成实实在在的东西,也就是对象

public class Cal {
	//普通函数,代表两数相加的规则,位置是固定的,无法传播函数本身(跟传播Cal对象然后执行add方法不一样)
    //要想使用它,就要通过Cal.add找到这个函数,然后执行
    static int add(int a, int b) {
        return a + b;
    }
	
    interface Lambda {
        int add(int a, int b);
    }
    //将函数变成对象,赋值给成员变量add,add的类型是Lambda
    Lambda fun = (a, b) -> a + b;
    
    public static void main(String[] args) {
        Cal cal = new Cal();
        //拿到函数对象,这个函数对象可以赋值给其他实体,这里传播的是函数fun本身
        Lambda fun = cal.fun;
        //直接使用这个函数
        System.out.println(fun.add(1, 2));
    }
}

这里的(a, b) -> a + b;就是Lambda表达式,Lambda表达式的作用就是函数实体化

函数对象跟其他普通对象一样,必须赋值给某个类型,函数对象一般赋值给函数式接口,而函数对象的代码体,就相当于是对接口中的抽象方法的实现

并没有进行指定,函数对象怎么知道自己实现了哪个方法?

首先需要解释下什么是函数式接口,函数式接口就是只有一个抽象方法的接口,虽然没有指定函数对象代表了哪个抽象方法,但接口里就一个抽象方法,除了你还能有谁?对于这种不言自明的东西,一般都可以省略掉

所以,函数对象就代表了函数式接口里唯一的那个抽象方法,函数对象的参数列表、返回值都和这个抽象方法一一对应

函数对象,只能赋值给函数式接口

跟匿名内部类区别:

  • 函数对象fun可以序列化,然后传递给其他应用,其他应用只要有Lambda接口,就能反序列化函数对象fun,然后执行函数
  • 匿名内部类也可以序列化传递给其他应用,但是其他应用必须有这个类才能实现反序列化,也就是说其他应用必须有函数的实现

1.3 行为参数化

函数参数除了可以是数据之外,还可以是行为

怎么理解这里的行为呢?

行为就是对要做的事情的一个描述,也就是规则,而函数本质就是表示一种规则,所以这里的行为就表示函数

函数如何作为参数呢?

将函数封装成一个函数对象(有形函数),再将这个对象赋值给方法参数

要实现这个目的,需要有一个接口(函数式接口),函数对象就赋值给这个接口类型,方法参数也定义为这个接口类型,函数对象就可以赋值给方法参数

准备一个函数式接口

@FunctionalInterface
public interface Predicate<T> {
    //接收一个值,判断这个值是否符合要求
    boolean test(int n);
}

过滤函数,这个函数的功能是将一个数组或集合按照一定规则进行筛选,最后将筛选后的新数组返回

注意,我们说的是按照一定规则筛选,但具体是什么规则不知道,需要函数调用者来指定规则

这里就可以将这个规则封装成一个函数对象

List<Integer> filter1(List<Integer> list, Predicate<Integer> predicate) {
	List<Integer> result = new ArrayList<>();
		for (Integer data : list) {
            //这里将筛选规则进行了抽象,调用者传递过来的规则不同,筛选结果也不同
			if(predicate.test(data)) {
				result.add(data);
			}
		}
	return result;
}

调用者

//准备一个集合
List<Integer> list = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
//筛选出所有偶数
List<Integer> newList = filter1(list, item -> item % 2 == 0)
//筛选出所有奇数
System.out.println(filter1(list, item -> item % 2 == 1));

1.4 延迟执行

函数对象除了可以实现行为参数化,还可以实现延迟执行

看看下面代码

public void printLog() {
    log.debug("{}", print())
}
String print() {
    System.out.println("日志");
    return "日志"
}

在debug日志级别下运行printLog函数,发现打印了日志,同时控制台也打印了字符串

但是在更高级别下,发现日志是没有打印,但是控制台依然打印了字符串,这是Java的语言特性,从内到外执行函数

如果希望在更高的日志级别下,print不会被执行,那么可以如下改造

public void printLog() {
    if(log.isDebugEnabled()) {
        log.debug("{}", print())
    }
}
String print() {
    System.out.println("日志");
    return "日志"
}

这样改造可以实现我们的目的,但不是很优雅

可以使用函数对象来优雅地改造

public void printLog() {
    log.debug("{}", () -> print())
}
String print() {
    System.out.println("日志");
    return "日志"
}

将 log.debug的第二个参数变成一个函数对象,如果日志级别更高,那么print函数不会被执行

函数对象是怎么做到这一点的呢?

log.debug("{}", print())执行到这行代码时,会立刻去执行print函数

log.debug("{}", () -> print())执行到这行代码时,也会立刻去执行() -> print(),注意这个语句的执行结果是生成一个函数对象,并没有执行函数对象里面的print函数

那什么时候执行print函数呢,这得看log.debug函数内部是什么时候去调用这个函数对象的,log.debug只有在即将打印日志时才会调用函数对象,这就是为什么日志级别更高,print函数没有立即被执行的原因

总结:

函数在编程中是一个很重要的东西(面向对象语言中也是如此),但函数的位置是固定的,无法传播函数,于是就有了函数对象(其实现是Lambda表达式)

函数对象可以实现行为参数化,也就是将一个函数(规则)变成对象,传播到其他地方,其他地方就会按照指定的规则来执行任务(这也是设计模式中模板方法模式的思想)

函数对象还可以实现延迟执行,Java的语言特性,在一条表达式或语句中,总是会优先执行嵌套在里层的表达式或语句,如果里层存在函数,那么就会执行完这个函数的所有代码,但很多时候我们并不希望这样,此时就可以将里层的函数变成函数对象,那么顶多会创建这个函数对象,并不会执行这个函数,什么时候执行呢?得看这个函数对象的拥有者来决定

2.函数的语法

上面我们对函数的概念进行了了解,这么对函数对象的语法进行详细讲解

2.1 Lambda表达式

一个函数对象,最常用的表现形式就是Lambda表达式

Lambda表达式是一个函数对象,而对象都有各自的类型,Lambda表达式的类型是什么呢?

我们知道Lambda表达式必须依赖一个函数式接口(只有一个抽象函数方法的接口),那么编译时编译器就知道这个Lambda代表了哪个函数(接口里唯一的那个抽象函数)、参数列表信息,以及函数返回值类型,我们只需要在Lambda表达式里实现这个函数规则就行了

Lambda表达式的类型就是这个函数式接口,而Lambda表达式的代码体,就实现函数式接口里唯一的抽象函数

一条Lambda表达式分为两部分:(int a, int b) -> return a+ b

  1. (int a, int b):参数部分
  2. return a+ b:逻辑部分,就是代码体,接口抽象方法的实现

用一个箭头->将这两部分相连接

有如下函数式接口

//这个注解可以强制保证该接口抽象函数只有一个,否则编译不通过
@FunctionalInterface
public interface Func {
    //这个参数接收一个数值,返回一个新值
    int cal(int n);
}

Lambda表达式,下面Lambda表达式实现了计算并返回输入参数的平方数

(int n)参数列表、int返回值都要跟Func接口的cal函数一致,编译器就知道这个Lambda表达式代表的就是cal函数,我们调用lambda.cal(3),就可以得到正确int数组9

//square函数是一个伪代码,作用是输入一个参数,返回该参数的平方
Func lambda = (int n) -> {return square(n);}

Lambda表达式的一个重要特点就是简洁,在编译器已经知道信息的情况下,很多东西我们可以不用写

比如参数列表的类型,在cal函数里定义的明明白白,编译器可以通过顺序知道某个参数的类型

//参照Func接口的cal抽象函数,编译器知道n是int类型
//参数名字并不重要,重要的是参数顺序,编译器通过顺序来判断参数类型
Func lambda = (a) -> {return square(a);}

当参数列表只有一个参数的情况下(没有参数类型),括号也就没有意义,因为括号作用就是组织代码,一个参数还没有写类型,本身就是原子的,不需要组织

Func lambda = n -> {return square(n);}

如果Lambda表达式代码体的语句只有一条,那么花括号、return关键字、分号也可以去掉

Func lambda = n -> square(n)

n -> square(n)非常简洁,但通过Func接口的cal抽象函数,编译器可以推断出这个Lambda的参数列表、返回值类型

2.2 方法引用

除了上面介绍的Lambda表达式,函数对象的表现形式还可以是方法引用

方法引用是对Lambda表达式的终极简化,方法引用可以等价于一个Lambda表达式

其基本理念就是方法引用中未知的,必须作为对应的Lambda表达式参数

方法引用就是将现有方法的调用变成函数对象,而方法的调用又分为静态方法调用、实例方法调用、构造方法调用

静态方法

有如下的静态方法的方法引用

Math::min

咋一看有点费解,这是想干什么呢?

不难看出,这是想调用Math的静态方法min,也就是执行语句:Math.min(int a, int b)

而想要执行该语句,就需要得到a、b两个参数的值,于是就可以变成下面这样

(int a, int b) -> Math.min(a, b)

这不就是Lambda表达式吗?没错,方法引用等价于Lambda表达式

我们上面说过,对于不言自明的东西,一般都可以省略掉,编译器可以通过函数对象Math::min判断想要调用的方法的参数列表和返回值,还可以推断出函数对象代码体想要干的事情(执行语句Math.min(int a, int b)),所以参数列表、返回值、代码体都省略了,我们看到的不就是Math::min这个样子吗

为了方便理解,可以将方法引用Math::min跟Lambda表达(int a, int b) -> Math.min(a, b)划上等号

正确调用Math::min需要的两个实参是未知的,未知的参数有对应的Lambda表达式提供(int a, int b)

至于说Lambda表达式的这两个实参从哪来,谁调用谁提供实参呗

总结:

静态方法引用,引用的方法的参数列表,跟对应Lambda参数列表一致就行了

方法引用一定可以转化为Lambda表达式形式

Lambda表达式的代码体如果只有一个语句并且该语句是对其他方法的调用,那么Lambda也可以转化为方法引用形式

进而可以推断出,所有Lambda都可以转为方法引用形式(如果Lambda代码体有多行语句,可以将其所有代码抽取成一个方法,然后对这个方法进行方法引用)

所以,方法引用和Lambda可以相互转换

实例方法1

stu.setName(String name)

有如下的实例方法的方法引用,看上去跟静态方法引用一样,但实际这里的setName是实例方法

Student::setName

要调用实例方法,必须要一个实例对象,以及方法参数,这些都是未知的所以必须作为对应Lambda参数

对应的Lambda如下,发现Lambda参数(Student,String)比setName方法多一个参数(String)

(Student stu, String name) -> stu.setName(name)

实例对象作为对应Lambda形式的参数列表的第一个参数由调用者提供,然后后面的所有参数依次作为实例方法的参数

这种方法引用最特殊,后面我们会知道,其他的方法引用中所引用的方法参数列表都和对应Lambda参数列表一一对应,只有这种方法引用,Labmda的参数比实际引用的方法参数多一个(多一个实例对象)

实例方法2

实例方法的方法引用第二种形式,假设已有对象stu

此时实例方法的方法引用,就可以变成对象::方法名称形式

stu::println

由于实例对象已知,其他普通参数未知,所以必须作为Lambda参数

(String name) -> stu.setName(name)

这里的对象不一定是普通new出来的对象,还可以是thissuper

this::非静态方法

super::非静态方法

带对象的实例方法引用,引用的方法的参数列表,跟对应Lambda参数列表一致

构造方法

new Student(String name)

构造方法也可以用方法引用来简化

Student::new

使用构造方法,需要类和参数名称,类是已知的,参数是未知的,所以构造参数必须作为Lambda表达式参数

(String name) -> new Student(name)

构造方法引用,引用的方法的参数列表,跟对应Lambda参数列表一致

总结

凡是可以使用方法引用的地方,都可以使用Lambda表达式无缝替换

除了实例方法1所描述的方法引用,其他方法引用所引用的方法参数列表,和对应的Lambda参数列表一致

编号格式特点备注
1类名::静态方法与对应Lambda参数一致
2类名::非静态方法对应Lambda参数列表比该非静态方法多一个该类对象参数
3对象::非静态方法参数一致
4类名::new参数一致
5this::非静态方法参数一致3的特例
6super::非静态方法参数一致3的特例

2.3 函数对象类型

上面我们说过,函数对象既然是对象,肯定有自己的类型

确定一个函数对象的类型,需要看两部分,参数列表返回值类型

如果两个函数对象的参数列表个数类型相同,返回类型相同,那么这两个函数对象就可以认为是同一类,归类后其类型用一个接口来表示(函数式接口)

比如下面两个函数对象

(int a, String b) -> return "hello";
(int c, String d) -> return "world";

二者参数列表(跟参数名无关)、返回值类型完全一致,可以认为同一个类型,可以使用如下函数式接口来表示

@FunctionalInterface
interface Func {
    String fun(int a, String b)
}

所以上面的函数对象可以赋值给Func接口

Func a = (int a, String b) -> return "hello";
Func b = (int c, String d) -> return "world";

有一个类

class Person {
    static String hello(int age, String name) {
        return "my name is" + name + ", age is" + age;
    }
}

可以使用方法引用形式,这个函数对象的参数参数列表是(int, String),返回值是String,故而和上面两个函数对象是同一个类型

Person::hello
//下面三个函数对象同一个类型
Func a = (int a, String b) -> return "hello";
Func b = (int c, String d) -> return "world";
Func c = Person::hello;

函数式接口可以使用泛型

@FunctionalInterface
interface Func<T, K> {
    T fun(K k)
}

编译器可以根据上下文推断出函数对象的参数和返回值类型

比如下面Func的第二个泛型就是参数的类型,所以下面的a参数就是Integer类型,返回值是String类型

Func<String, Integer> f = a -> "hello";

JDK提供了一些函数式接口

  1. Runnable:()-> void

  2. Callable:()-> T

  3. Comparator:(T, K) -> int

    用来比较,接受两个参数,返回一个int值,如果返回负数表示T < K,返回正式表示T > K,返回0表示T = K

  4. Consumer、BiConsumer、IntConsumer、LongConsumer、DoubleConsumer

    (T)-> void,消费者接口,这里的消费指消费参数,没有产出

    Bi指参数有两个:(T, U) -> void,Int表示参数是int类型:(int)-> void,.....

  5. Function、BiFunction、IntFunction、LongFunction、DoubleFunction

    (T) -> R,接受一个参数T,返回一个值R,Bi、Int、Long、Double同上

  6. Predicate、BiPredicate、Int Long Double

    (T) -> boolean,断言接口,接受一个参数T,返回一个boolean类型变量,Bi、Int、Long、Double同上

  7. Supplier、Int Long Double

    () -> T,生产者接口,这里的生产指没有输入,却有产出,Bi、Int、Long、Double同上

  8. UnaryOperator、BinaryOperator、Int Long Double

    (T) -> T,参数和返回值类型必须是一样的,BinaryOperator接受两个参数,Int、Long、Double同上

特例:有返回的函数式对象可以赋值给抽象方法为void的函数式接口

比如有如下类

class Person{
    static int hello(String name) {
        log.info(name);
        return 233;
    }
}

下面代码编译是可以通过的

Consumer<String> consumer = Person::hello

Person::hello这个函数对象返回值为int,而Consumer函数式接口的抽象方法返回值是void,这两个不是一个类型,为什么可以赋值呢?

原则上讲是这样的,但是正如我们前面说的,对于不言自明的东西,不需要指明,编译器可以推断出,这是编译器的一个优化点

这里也是同理的,Person::hello这个函数对象的返回值是int,赋值给返回值是void的Consumer函数式接口时,编译器认为虽然你返回了值,但是我只执行你里面的代码,不接受你的返回值不就行了吗?

所以上面语法是正确的,这也是编译器的优化点

2.4 闭包

看下面代码

int x = 10;
Function<Integer, Integer> f = y -> x + y;
f.apply(1);

将函数对象y -> x + y赋值给函数式接口f,然后调用该函数式对象

函数对象中用到了一个外部的变量x,无论这个函数式对象被传递到了哪,将来这个函数式对象执行时,随时都能找到这个变量,就可以认为函数式对象与外部变量x形成了闭包

闭包:在函数对象中用到了外部变量,这个外部变量可以是局部变量、方法参数、静态变量、实例变量,函数对象与外部变量形成了一个整体,这个整体就是闭包

闭包条件:除了静态变量,外部变量应该是不可变的,但不必使用final修饰,只要保证外部变量在首次赋值后没有发生变化

下面的代码在编译时就会报错,因为外部变量x在第四行发生了变化,形成闭包的外部变量不强制要求final修饰,但为了代码准确性,建议使用final

int x = 10;
Function<Integer, Integer> f = y -> x + y;
f.apply(1);
x = 20;

但是如果外部变量是一个对象,修改这个对象里面的属性值,那么编译会通过

Student stu = new Student(10);
Function<Integer, Integer> f = y -> stu.age + y;
f.apply(1);
//这行代码编译会通过,因为stu的值没有被改变,改变的只是里面属性的值
stu.age = 20;
//这行代码编译不会通过,因为stu的值被改变了
stu = new Student(20);

如果闭包提供的是静态变量,则没有上述限制

闭包的作用:除了参数外,额外给函数对象提供数据的手段

2.5 柯里化

什么是柯里化(Currying)?

柯里化就是让一个接收多个参数的函数转换成多个接收一个参数的函数

比如下面这个函数

interface F {
    int add(int a, int b);
}
void test() {
    //这个函数接收两个参数,并返回两个参数之和
    F f = (a, b) -> a + b;
    //执行该函数
    int num = f.add(1, 2);
}

可以转化成两个单独的函数FA和FB,每个函数单独接收一个参数

  • FA函数:返回FB类型的函数对象(int a) -> {return (int b) -> a + b;}

  • FB函数:根据得到的两个数据a、b执行对应的操作,比如相加

再调用FB,就得到了两数相加的结果

interface FA {
    FB op(int a);
}
interface FB {
   int op(int b);
}

void test() {
    //咋一看有点复杂,但其实是上面写法的简写形式
    FA fa = a -> b -> a + b;
    //调用fa函数,得到一个fb函数
    FB fb = fa.op(1);
    //调用fb,两个参数之和,等价于上面的f.add(1, 2)
    int num = fb.op(2);
}

柯里化是基于闭包的,函数fb(b -> a + b)中,需要两个参数a和b,其中b是函数fb自己的参数,而a是由闭包提供的数据

柯里化的意义

最大的意义就是让函数分步执行,任务拆分

比如一个任务需要三个数据a、b、c才能正确执行,那么这三个数据可以交给不同的函数去收集

步骤如下:

  1. 函数1收集到数据a后,通过闭包提供给函数2并返回函数2

  2. 函数2收集到数据b后,通过闭包将a、b提供给函数3并返回函数3

  3. 函数3收集到数据c后,根据通过闭包得到是数据a、b,三个数据齐活了,就开始执行任务

关系如下:

函数1:a -> 函数2

函数2: b -> 函数3

函数3: c -> 根据上面函数通过闭包提供的数据,加上自己的数据进行计算

三个函数层层返回,最里面的函数就是真正执行任务的函数

函数1的格式如下,函数1返回函数2,函数2返回函数3

F1 f1 = (int a) -> {
            return ((int b) -> {
                return ((int c) -> {
                    return a + b + c;
                });
            });
        };

其简写形式有点抽象,但二者是一个东西

F1 f = a -> b -> c -> a + b + c;

可以这样调用

f.op(1).op(2).op(3);

可以将不同的函数封装在不同的方法里,每个方法就代表一个步骤

柯里化另外一个意义就是参数复用,比如调用函数对象F1并传入参数a会返回函数对象F2,那么这个F2函数对象无论再调用多少次,该函数持有的数据a(由闭包提供)都是固定的,除非重新调用F1得到新的F2

递归于柯里化可以完美结合,后面会介绍

柯里化的本质是闭包

2.6 高阶函数

所谓高阶函数,就是指它是其他函数对象的使用者

比如下面的函数fn,它返回了另一个函数对象i -> i * i,那么就可以认为函数fn是高阶函数,因为它用到了其他的函数对象

Function<Integer, Integer> fu() {
    return i -> i * i;
}

这里的其他函数对象可以是高阶函数的参数、局部变量、返回值、以及闭包提供的外部函数对象

高阶函数通过闭包给它用到的这个函数对象提供值

函数对象作为抽象的部分,高阶函数作为具体的部分

作用:

  • 将通用、复杂的逻辑隐含在高阶函数内
  • 将易变、未定的逻辑放在使用到的函数对象中

下面这种情况就是高阶函数的典型应用场景

高阶函数内部发生了循环,循环中用到了函数对象

//这个高阶函数的作用是遍历数组的每一个元素,然后又函数对象comsumer来决定如何处理这个元素
void highFun(List<T> list, Consumer<T> consumer) {
    //内部循环遍历集合
    for(T t : list) {
        consumer.accept(t);
    }
}
//使用这个高阶函数时,指定函数对象的具体行为
void test() {
    //对于遍历到的每一个元素,进行打印操作
    highFun(List.<Integer>of(1, 2, 3, 4, 5), System.out::println);
}

这里的遍历不一定是顺序遍历,也可能是树的遍历、图的遍历

高阶函数简单应用-流

stream流里大量使用了其他函数对象,所以stream流的方法本质就是是高阶函数

下面简单模拟下stream流

流的构建

//流本质就是个对象
public class MyStream<T> {
    //流是对集合进行的操作,该属性用来保存集合
    private Collection<T> data;

    //私有化构造器
    private MyStream(Collection<T> data) {
        this.data = data;
    }

    //外界只能根据该方法创建流对象
    public static <K> MyStream<K> of(Collection<K> data) {
        return new MyStream<>(data);
    }
}

stream流里包含了一系列方法,映射、过滤、收集方法,这些方法共同的特点就是需要接收外界传来的函数对象,所以这些方法都是高阶函数

函数的特性是不变性,所以下面的方法都会创建一个新集合,而不是直接修改原集合

过滤方法

//过滤后返回新的流对象,过滤前后集合类型是一样,所以这里直接使用类上定义的泛型T
public MyStream<T> filter(Predicate<T> predicate) {
    //创建一个新集合来保存过滤后的数据
    List<T> newData = new ArrayList<>();
    //遍历流对象中持有的集合数据
    for (T datum : data) {
        //将符合条件的元素添加到
        if (predicate.test(datum)) {
            newData.add(datum);
        }
    }
    return of(newData);
}

映射方法

//对于原集合的每一个T,将其按照规则变成新元素K,这里的泛型K需要在方法上定义
public <K> MyStream<K> map(Function<T, K> function) {
    //存放映射后元素的新集合
    List<K> newData = new ArrayList<>();
    for (T datum : data) {
        //function.app接收旧元素,返回新元素
        newData.add(function.apply(datum));
    }
    return of(newData);
}

上面的所有方法都会返回一个新的MyStream对象,所以上面方法称为流的中间操作

消费方法

该方法没有返回值,可以视为流的终结操作

public void foreach(Consumer<T> consumer) {
    for (T datum : data) {
        //遍历每一个元素,并进行消费
        consumer.accept(datum);
    }
}

测试上面三个方法

//测试
void test() {
    List data = List.of(1, 2, 3, 4, 5, 6, 7);
    //将data集合的每一个元素映射成其平方,并依次打印
    MyStream.<Integer>of(data).map(item -> item * item).foreach(System.out::println);
    //过滤出集合的所有偶数,并打印
    MyStream.<Integer>of(data).filter(item -> item * 2 == 0).foreach(System.out::println);
}

合并方法

将集合的前两个元素合并成一个新元素,然后将这个新元素于下一个集合元素再次合并得到一个新元素,对集合所有元素依次进行该操作,直到将集合遍历一遍,并最终得到一个元素

该方法返回是一个具体的值,可以视为流的终结操作

public T reduce(T initValue, BinaryOperator<T> operator) {
    //用来保存上一次合并的结果
    T p = initValue;
    for (T datum : data) {
        p = operator.apply(p, datum);
    }
    return p;
}

为了方便遍历,提供一个初始值initValue,让集合的第一个元素于该初始化进行合并

下面是两条最常用的合并规则:

  • 得到两个元素中最大(小)的那个元素,并以此作为合并后的新元素
  • 就算两个元素的和,并以此作为合并后的新元素

上面两个规则分别用于得到集合元素最大(小)的元素,和计算集合所有元素的和

测试合并方法

void test() {
	List data = List.of(1, 2, 3, 4, 5, 6, 7);
    //计算集合所有元素的和
    Integer sum = MyStream.<Integer>of(data).reduce(0, Integer::sum);
}

收集方法

对于经过过滤、映射这些中间操作的流对象,我们希望结束操作并返回进过以上中间操作后的集合,也就是收集流中的这些元素

收集流中的集合数据,本质就是先得到这些数据,并放进一个新容器,然后返回这个新容器,而这个新容器类型是由外界指定的;并且收集数据的逻辑,也是外界指定的

所有收集方法需要两个参数

  1. 一个参数用于得到一个新容器对象
  2. 另一个参数用于执行将集合元素添加进新容器的逻辑

这两个参数都是函数对象,如下所示

public <C> C collect(Supplier<C> supplier, BiConsumer<C, T> ctBiConsumer) {
    //得到容器
    C c = supplier.get();
    for (T datum : data) {
        //执行将集合元素添加进新容器的逻辑
        ctBiConsumer.accept(c, datum);
    }
    return c;
}

该操作也是终结操作

测试

void test() {
    	
		//将数据收集成set
        HashSet<Integer> set = MyStream.of(data).collect(HashSet::new, HashSet::add);

        //将数据收集成按指定字符分割的字符串
        StringJoiner sj = MyStream.of(data).map(String::valueOf)
            .collect(() -> new StringJoiner("-"), (s, d) -> s.add(d));

        //将数据分组收集成map,key是值,value是该值出现的次数
        HashMap<Integer, Integer> m = MyStream.of(data).collect(
                HashMap::new,
                (map, d) -> map.put(d, (map.getOrDefault(d, 0)) + 1)
        );
}

总之,只要某个流的操作不是返回了一个新的流对象,那么都是属于终结操作

JDK中的Stream流大致原理跟上面差不多,不过细节处处理的更好

比如上面的演示,经过操作1时会遍历一次全量元素,然后下一个操作2再遍历一次全量数据

JDK中的Stream流会尽量减少遍历次数,也就是会将所有操作尽量合并到一次遍历里面,也就是遍历每一个元素时,先执行一次中作1、再执行操作2.....

3.Stream 的常见api

JDK中提供了一系列高阶函数的集合,这些高阶函数配合函数对象,能够实现很复杂的数据处理操作,这些高阶函数集合就是Stream流

Stream流分类

基本类型流

  • IntStream:流中元素全是int类型
  • LongStream:流中元素全是long类型
  • DoubleStream:流中元素全是double类型

普通对象流

  • Stream:流中元素全是T类型的对象

流的特性

  • 一次使用

    一个流对象调用终结方法后,无法进行使用

  • 两类操作

    中间操作:某个方法的返回值是Stream流对象,并且是lazy惰性的,该方法是中间操作

    终结操作:某个方法的返回值不是Stream流对象,并且是eager迫切的,该方法是终结操作

    调用中间操作,不会执行传入的函数对象,只有当调用终结操作时,函数对象才开始真正执行

    数据就好像是水,调用中间操作就好比是接上了一节节水管,终结操作就好比是水龙头,调用终结操作就是打开水龙头,水会经过一节节水管并最终从水龙头处流出来

3.1 Stream流的创建

JDK提供了三种方式创建一个流对象

  1. 根据提供的对象数据,通过Stream类的静态方法of生成流

    //of方法参数接收一个数组,然后内部将其变成集合
    Stream<Integer> stream = Stream.<Integer>of(1, 2, 3, 4, 5);
    
  2. 基于集合对象(Collection)构建流,通过集合对象提供的stream方法

    List<Integer> list = List.of(1, 2, 3, 4, 5, 6);
    Stream stream1 = list.stream();
    
  3. 基于数组构建流,通过静态方法Arrays.stream

    IntStream stream = Arrays.stream(new int[]{1, 2, 3});
    
  4. 没有数据,根据规则直接生成流

    rangerangeClosed,根据数字范围生成流,IntStream独有

    //含头不含尾,也就是从1开始生成,生成到99,不含100
    IntStream stream = IntStream.range(1, 10);
    //含头含尾,从1开始生成,生成到100
    IntStream stream = IntStream.rangeClosed(1, 10);
    

    iterate,根据上一个元素值来生成当前元素值,有三个参数

    1. 初始元素

    2. 结束条件的函数对象(类型是Predicate及其同类型函数式接口,参数是上一个元素)

    3. 新元素的函数对象(类型是UnaryOperator及其同类型函数式接口,参数是上一个元素)

    //从1开始生成,每次生成的新值在上一个基础上+1,新值大于10则结束生成
    Stream.<Integer>iterate(1, item -> item <= 10, item -> item + 1)
         .forEach(System.out::println);
    

    参数也可以没有结束条件函数对象,此时则要配合limit(后面会讲)截取前n个生成的元素

    generate,直接根据指定的规则生成元素,会无限生成,需要使用limit截取

    //循环生成10次当前毫秒数
    Stream.<Long>generate(() -> System.currentTimeMillis())
     .limit(10)
    	.forEach(System.out::println);
    
    
    

3.2 消费

正如2.6高阶函数介绍的,forEach接收一个参数,没有返回值,是一个消费方法,Stream的这些方法都是终结操作

对于基于数组、Collection的Stream,其forEach方法使用的函数对象类型是Consumer,如果是基于数组得到的流,函数对象类型还可能是IntConsumer、DoubleConsumer这些同类型函数式接口

//打印流中的所有元素
Stream.<Integer>of(1, 2, 3, 4, 5)
        .forEach(System.out::println);

3.3 过滤

会筛选出所有断言为true的元素,并返回一个新流,新流中元素就是经过筛选后的元素,这是一个中间操作

接收一个元素,并返回boolean,所以filter方法使用的函数对象类型是Predicate及其同类型函数式接口

过滤出所有偶数,并打印

//下面会打印出 2,4
Stream.<Integer>of(1, 2, 3, 4, 5)
        .filter(item -> item % 2 == 0)
        .forEach(System.out::println);

3.4 一对一映射

将每个元素按照规则转化成另一个新元素,并将这些新元素对应的新流返回

接收一个元素,返回另一个元素,所以map方法使用的函数对象类型的Function及其同类型函数式接口

将每个元素乘以2并打印

//下面会打印出 2,4,6,8,10
Stream.<Integer>of(1, 2, 3, 4, 5)
        .map(item -> item * 2)
        .forEach(System.out::println);

3.5 一对多映射

将流中的每个元素按照规则转化成另一个Stream流,然后这些转化后的Stream流就合并成一个更大的新Stream流并返回

每个转化后的Stream流可以等价视为为多个元素,所以这里可以视为一对多映射

接收一个元素,返回一个Stream,所以flatMap方法使用的函数对象类型也是Function及其同类型函数式接口,严格来说是Function<? super T, ? extends Stream<? extends R>>

流中的每个元素是一个集合,将所有集合的元素取出来并打印

//打印出 1,2,3,4,5,6
Stream.<Integer>of(List.of(1, 2, 3), List.of(4, 5, 6))
        .flatMap(item -> item.stream())
        .forEach(System.out::println);

也可以视为降维操作,因为将原本的二维集合变成一维集合

3.6 流的合并

将多个流合并成一个新的大流(flatMap)

Stream newStream = Stream.concat(stream1, stream2);

3.7 截取

skip

跳过流的前n个元素,并将剩下的元素作为新流返回

跳过前三个元素

//下面会打印出 4,5
Stream.<Integer>of(1, 2, 3, 4, 5)
        .skip(3)
        .forEach(System.out::println);

limit

保留前n个元素,剩下的不要,并将保留的元素作为新流返回

保留前三个元素

//下面会打印出 1,2,3
Stream.<Integer>of(1, 2, 3, 4, 5)
        .limit(3)
        .forEach(System.out::println);

skip和limit可以配合使用,比如保留第三、第四个元素,就先skip前两个元素,在limit两个元素并返回

takeWhile

条件成立保留,一旦条件不成立,剩下的舍弃,并将保留的元素作为新流返回

比如下面代码,遍历前四个元素时条件成立,当遍历到第五个元素时条件不成立,那么第五个以及后面的所有元素都会舍弃

//打印 1,2,3,4
Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2)
    .takeWhile(item -> item < 5)
    .forEach(System.out::println);

dropWhile

条件成立舍弃,一旦条件不成立,剩下的保留,并将保留的元素作为新流返回

下面代码,遍历前四个元素时条件成立则舍弃,当遍历到第五个元素时条件不成立,那么第五个以及后面的所有元素都会保留

//打印 5,6,7,8,9,1,2
Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 1, 2)
    .dropWhile(item -> item < 5)
    .forEach(System.out::println);

3.8 查找

从集合中返回一个元素,一般配合filter返回指定的条件的那一个元素,是终结方法

  • findAny

    从流中随便找一个元素返回

  • findFirst

    返回流中的第一个元素

下面代码筛选出值为5的元素,然后再返回这个元素

Optional<Integer> op = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9)
    .filter(item -> item == 5)
    .finAny();
//如果没有找到指定元素,就返回默认值-1
op.orElse(-1);

最佳实践是经过过滤后,确保流中只有一个元素了,再使用findAny或findFirst

3.9 判断

判断流中的所有元素是否满足指定条件,返回boolean,是终结方法

  • anyMatch(Predicate p)

    只要流中有一个元素满足条件,就返回true,反正false

  • allMatch(Predicate p)

    只有流中全部元素满足条件,才返回true,否则false

  • noneMath(Predicate p)

    只有流中没有一个元素满足条件,才返回true,否则false

比如下面代码,判断流中元素有没有存在奇数,存在奇数就返回true,否则false

boolean existOdd = Stream.of(2, 4, 6).anyMatch(item -> item % 2 != 0);

3.10 去重

去除流中重复的元素,并讲去重后的元素作为新流返回

如果流中元素是对象,则根据equal方法判断两个对象是否相同

Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2 ,4 ,7)
    .distinct()
    .forEach(System.out::println);

3.11 排序

指定一个排序规则来对流中元素排序

排序规则的函数对象是Comparator类型的

逆序打印流中元素

Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2 ,4 ,7)
        .sorted((a, b) -> b.compareTo(a))
        .forEach(System.out::println);

对于元素是对象的流

下面按照年龄降序排列person对象

//comparingInt接收一个函数对象,(p)->int,讲对象映射成一个数字,然后排序
Stream.<Person>of(new Person("张三", 33), new Person("李二", 22), new Person("王五", 55))
        .sorted(Comparator.comparingInt(Person::getAge).reversed())
        .forEach(System.out::println);

comparingInt返回一个Comparator类型函数对象,并接收一个ToIntFunction类型的函数对象,该函数对象作用是将对象按照规则映射成数字,方便排序

reversed同样返回一个Comparator类型函数对象,作用是降序

如何进一步排序呢?如果年龄相同,则按照姓名排序

Stream.<Person>of(new Person("张三", 33), new Person("李二", 22), new Person("王五狗", 22))
    	//先按照年龄降序
        .sorted(Comparator.comparingInt(Person::getAge).reversed()
                //如果年龄相同,再按照姓名长度降序排列
                .thenComparingInt(p->p.getName().length()).reversed())
        .forEach(System.out::println);

thenComparingInt方法同样返回Comparator类型函数对象,也接收一个ToIntFunction类型的函数对象

上面代码体现了柯里化,也体现了链式编程

Stream的很多需要用到排序的方法对对象类型的流进行操作时(比如求最大元素max),都可以使用comparingInt

3.12 数据合并

reduce,合并方法,依次两两合并,并按照规则变成一个新元素,让新元素参与下一次合并

根据规则不同,reduce可以是求最大(小)值、求和、计算流的元素个数

三个重载

  • reduce(BinaryOperator):p是上次合并的结果,x是当前遍历的元素,r是合并后的结果

    没有初始值,所以返回值是Optional,防止流中元素个数为0

    //得到流中最大的元素
    Optional<Integer> max = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 4, 7)
            .reduce((total, current) -> Integer.max(total, current))
    
  • reduce(init, BinaryOperator)

    准备一个初始值,流中没有元素,就返回这个初始值

    //对流中元素进行求和操作
    Integer total = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 4, 7)
            .reduce(0, (total, current) -> total + current)
    
  • reduce(init, BiFunction, BinaryOperator)

    并行流有关,并行流后面会介绍

计算流中元素的个数,可以先使用map将每个元素映射为1,再使用reduce进行累加操作

也可以直接使用流的count方法

同理,流也提供了maxmin方法用于计算最大最小值

求和操作sum,以及求平均值操作average

这些方法都是基于recude进行的封装

3.13 收集

收集,将元素收集进容器并返回这个容器,经典终结操作

collect方法

有两个重载

  • collect(Supplier, BiConsumer, BiConsumer)

    三个参数都是函数对象,表示的规则是:如何创建容器、如何将元素装进容器、如何合并容器(并行流)

  • collect(Collector)

    参数是一个收集器,收集器封装了如何创建容器、如何将元素装进容器的逻辑、如何合并容器(并行流)

这里的如何合并容器是并行流中需要用到的,因为并行流将数据分成若干份,每份数据交给不同线程处理,各线程创建自己的容器来收集数据,最后各线程汇总自己的结果得到最终的结果时,就需要合并容器

第一个重载代码如下所示,其原理在2.6那一节介绍过

//将流中元素收集进List容器
List<Integer> list = Stream.<Integer>of(1, 2, 3)
        .collect(ArrayList::new, ArrayList::add, (a, c) -> {});

不过我们一般使用第二个重载,也就是使用收集器,有一个工具类Collectors,提供了很多创建不同收集器对象的方法

下面是Collectors的常用方法

toList表示将元素收集进List集合,同理还有toSet收集进set集合,joining收集进StringBuilder对象

//Collectors.toList()方法返回一个收集器对象,该收集器对象用于将元素收集到List集合里
List<Integer> list = Stream.<Integer>of(1, 2, 3).collect(Collectors.toList());

toMap表示收集进Map集合,至少需要两个参数(Function),一个根据当前元素收集key,一个收集value

//得到的map长这样 {1:1, 2:1, 3:1}
Map<Integer, Integer> map = Stream.<Integer>of(1, 2, 3)
    	//"item -> item"将当前遍历到的元素作为key,"item -> 1"将1作为value
        .collect(Collectors.toMap(item -> item, item -> 1));

但是如果遇到了key相同的情况,就可能需要做自定义处理了

toMap有三个参数的重载,表示如果收集时遇到key相同的情况,那么就会进行合并,第三个参数就是合并函数

两个相同key对应的value作为合并函数的参数,返回值作为合并后的value

//得到的map长这样 {1:1, 2:1, 3:2}
Map<Integer, Integer> map = Stream.<Integer>of(1, 2, 3, 3)
    	//"item -> item"将当前遍历到的元素作为key,"item -> 1"将1作为value
        .collect(Collectors.toMap(item -> item, item -> 1, (v1, v2) -> v1 + v2));

groupingBy用于分组,按照某种规则,将元素分为若干组,这里需要用到两个收集器,作为groupingBy方法的两个参数

  1. 第一个收集器用于遍历每个元素,并返回一个映射值(map的key),映射值相同的分为同一组
  2. 第二个收集器收集每个分组下的元素

也就是说第一个收集器是产生map的key,第二个收集器是产生value的

比如按照数字元素的奇偶性分组,也就是奇数分为一组,偶数分为一组,比如1,2,3,4,5,两个收集器如下

  1. 划分每个元素的奇偶性

    item -> item % 2 == 0,此时元素的分组情况是:2、4是偶数,映射值为true;1、3、5是奇数,映射值是false

  2. 收集每个分组的数据

    Collectors.toList(),这里将每个分组下的元素收集成List集合,true=[2,4],false=[1,3,5]

最后得到的map就长这样:{false=[1, 3, 5, 7, 9], true=[2, 4, 6, 8, 10]}

代码如下所示

Map<Boolean, List<Integer>> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .collect(Collectors.groupingBy(item -> item % 2 == 0, Collectors.toList()))

也可以省略groupingBy的第二个参数,此时默认会使用Collectors.toList()收集每组的元素

//跟上面代码等价
Map<Boolean, List<Integer>> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .collect(Collectors.groupingBy(item -> item % 2 == 0))

groupingBy的第二个参数是一个收集器,也就是groupingBy的第二个参数也可以是一个groupingBy,也就是将组内的元素收集成一个map,既对组内元素再次进行分组,可以无限套娃

以上groupingBy方法默认创建的是HashMap,也可以创建其他类型的map,需要使用其另一个重载

下游收集器

上面演示的分组用到了两个收集器,Collectors.groupingByCollectors.toList(),第二个收集器包含在第一个收集器里面,所以第二个收集器是下游收集器

以下是几个常见的下游收集器

  • mapping(Function, Collector)

    下游收集器,第一个参数用于将组内的每个元素进行映射,一对一映射

    映射的元素也需要进行收集,所以第二个参数也是一个下游收集器用于收集隐射后的元素

    下面代码根据person的名字长度进行分组,并将组内的person映射成其名字,然后收集成List

    //得到的map长这样 -> {2=[张三, 里斯], 3=[哈哈哈, 嘻嘻嘻]}
    List<Person> persons = List.of(
        new Person("张三", 23), new Person("里斯", 43), 
        new Person("哈哈哈", 22), new Person("嘻嘻嘻", 55)
    );
    Map<Integer, List<String>> map = persons.stream()
        .collect(Collectors.groupingBy(p ->  p.getName().length(), 
                 	Collectors.mapping(p -> p.getName(), Collectors.toList())));
    
  • filtering(Predicate, Collector)

    下游收集器,第一个参数将组内元素进行过滤,第二个参数用于收集过滤后的数据

    下面代码根据person名字长度进行分组后,将组内person年龄大于30岁的过滤调,并收集剩下的person

    //{2=[Person{name='张三', age=23}], 3=[Person{name='哈哈哈', age=22}]}
    Map<Integer, List<Person>> map = persons.stream()
            .collect(Collectors.groupingBy(p -> p.getName().length(), 
                     	Collectors.filtering(p -> p.getAge() <= 30, Collectors.toList())))
    
  • flatMapping(Function, Collector)

    第一个参数将组内每个元素映射成一个Stream流,一对多映射

    第二个参数用于收集每个Stream流内的元素

    下面代码根据数字奇偶性进行分组,将组内每个元素复制一份后,收集成List

    //{false=[1, 1, 3, 3, 5, 5, 7, 7, 9, 9], true=[2, 2, 4, 4, 6, 6, 8, 8]}
    Map<Boolean, List<Integer>> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 4, 7)
            .collect(Collectors.groupingBy(item -> item % 2 == 0, 
                     	Collectors.flatMapping(i -> Stream.of(i, i), Collectors.toList())));
    
  • counting()

    收集组内元素的个数

    下面代码根据数字奇偶性进行分组,并得到每组的元素个数

    //{false=5, true=4}
    Map<Boolean, List<Integer>> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 4, 7)
            .collect(Collectors.groupingBy(item -> item % 2 == 0, Collectors.counting()));
    
  • minBy(Comparator)

    将组内元素按照比较器进行分组后,收集组内最小的元素

    //可能组内没有元素,所以用Optional -> {false=Optional[1], true=Optional[2]}
    Map<Boolean, List<Integer>> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 4, 7)
            .collect(Collectors.groupingBy(item -> item % 2 == 0, Collectors.counting()));
    

    同理还有收集组内最大的元素,maxBy

  • summingInt、summingLong、summingDouble

    将组内元素转换成int、long、double后,再计算总和并收集

    {false=25, true=20}
    Map<Boolean, Double> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9)
        .collect(Collectors.groupingBy(item -> item % 2 == 0,Collectors.summingInt(i -> i)))
    
  • averagingInt、averagingLong、averagingDouble

    将组内元素转换成int、long、double后,再计算平均值并收集

    //{false=5.0, true=5.0}
    Map<Boolean, Double> map = Stream.<Integer>of(1, 2, 3, 4, 5, 6, 7, 8, 9)
    .collect(Collectors.groupingBy(item -> item % 2 == 0, Collectors.averagingDouble(i -> i)))
    
  • reducing

    将每组元素进行合并,就算上面收集最大(小)值、平均值、总和的通用版本

这些下游收集器,可以参考上面的Streamapi

3.14 基本类型流特有的api

一开始我们就介绍了什么是基本类型流,除了上面演示的api,基本类型流有一些特有的api

以IntStream流为例,有如下api

  • mapToObject(int -> Object)

    对每个int元素映射成对象,并转为普通对象流

  • boxed()

    映射成其包装类型Integer对象,并转为普通对象流

  • sum()

    求和,int类型无需比较器

  • min()、max()

    求最小、最大值

  • average()

    求平均值

  • summaryStatistics()

    返回一个IntSummaryStatistics对象,对象里包含了count、sum、min、max、average

那么普通对象流怎么转为基本类型流呢?

  • mapToInt(x -> int)

    转为int类型的基本类型流

  • mapToLong(x -> long)

    转为long类型的基本类型流

  • mapToDouble(x -> double)

    转为double类型的基本类型流

基本类型流的收集方法collect只有一种重载collect(Supplier, ObjIntConsumer, BiConsumer),也就是如何创建容器、如何向容器添加数据、如何合并容器(并行流)需要指定

4 并行Stream

上面介绍的流是使用的单线程来处理的流,是串行流

并行流指使用多线程来处理流,采用了分而治之的思想,将全部数据分成若干份由不同的线程处理,然后各线程再汇总自己的处理结果得到最终的结果

并行流和串行流的api都是一样的,可以无缝切换使用

普通流调用parallel()方法将其变成并行流

//parallel就是并行流
Stream<Integer> parallel = Stream.of(1, 2, 3, 4, 5, 6).parallel();

下面演示了如何使用并行流来收集数据,为了打印线程的信息,下面使用了自定义的收集器

自定义收集器

//参数:如何创建容器、如何向容器添加数据、如何合并两个容器的数据、收尾操作、一些配置选项(可选参数)
Collector<Integer, ArrayList<Integer>, ArrayList<Integer>> collector = Collector.of(
        //如何创建容器
        () -> new ArrayList<>(),
        //如何向容器添加数据
        (list, item) -> list.add(item),
        //如何合并多个线程的各自的收集结果
        (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        },
        //收尾,所有线程收集的数据合并到一个大容器中,在最终返回收集结果前,需不需要对这个大容器做额外处理
        list -> list
        //特性
);

parallel.collect(collector);

并行流一开始会计算数据应该分成几分,需要使用几个线程(与cpu核心有关)

然后执行下面四个步骤

  1. supplier创建容器

    然后这些线程按照规则创建各自容器

  2. accumulator累加

    各线程收集数据,按照规则向容器添加数据

  3. combiner合并

    线程数据收集完毕后,两两合并各自收集的数据到新容器,直到将所有数据合并都一个容器

  4. 收尾

    在最终返回收集结果前,如何处理最终合并的大容器

可以设置并行流的一些特性,作为Collector.of的最后一个参数

特性分为下面三个:

  1. 是否需要收尾(默认收尾)

  2. 是否需要保证顺序(默认保证)

  3. 容器是否支持并发(默认不支持)

    比如上面例子中使用的容器ArrayList就不支持并发,不支持并发时并行流会使用锁来保证线程安全

下面例子中,此时容器只会创建一次,各个线程收集的数据都往这一个容器中添加,所以需要保证创建的容器是线程安全的

此时就不需要合并容器了,因为只有一个容器,步骤3的函数对象不会执行

Collector<Integer, Vector<Integer>, Vector<Integer>> collector = Collector.of(
        //如何创建线程安全容器,由主线程创建一次,其他线程并不会创建
        () -> new Vector<>(),
        //各个线程如何向这个容器存放数据
        (list, item) -> list.add(item),
        //由于此时只有一个容器,所以就不需要执行容器合并函数了
        (list1, list2) -> {
            list1.addAll(list2);
            return list1;
        },
        //收尾,所有线程收集的数据合并到一个大容器中,在最终返回收集结果前,需不需要对这个大容器做额外处理
        list -> list,
        //特性,不需要收尾,此时不会执行第四步的收尾逻辑
        Collector.Characteristics.IDENTITY_FINISH,
        //特性,不需要保证顺序
        Collector.Characteristics.UNORDERED,
        //特性,容器支持并发,需要保证创建的容器是线程安全的
        Collector.Characteristics.CONCURRENT
);

针对这种需要支持并发的情况,Collectors提供了两组方法,这两组方法都是

  • toConcurrentMap
  • groupingByConcurrent

在数据量小的时候,使用并行Stream就是一种浪费,因为大量的时间用在了线程交互上

百万级的数据才能体现并行流的优势

5 原理

5.1 Lambda原理

生成的方法和类

Lambda是一种语法糖,从逻辑上可以理解为一个函数对象,但是由于Java语言的特性,Lambda最终还是会被翻译成类、对象、方法

当某个类中的方法使用了Lambda表达式,运行时就会在当前类中生成一个静态私有方法,同时会生成一个实现对应的函数式接口的静态类,并在实现的抽象方法里调用生成的静态私有方法

比如下面代码

public class TestLambda {
    public static void main(String[] args) {
        //这里用到了Lambda表达式
        BinaryOperator<Integer> operator = (a, b) -> a + b;
        System.out.println(operator.apply(1, 2));
    }
}

运行后,就会生成一个静态内部类和静态私有方法

public class TestLambda {
    //这是运行时生成的方法,方法参数就是Lambda表达式的参数部分
    private static Integer labmda$main$0(Integer a, Integer b) {
        //方法体就是Lambda表达式的逻辑部分,
        return a + b;
    }
    
    //这是运行时生成的类,实现了BinaryOperator接口
    static final class TestLambda$$Lambda$14 implements BinaryOperator<Integer>{
        //在实现的方法里调用了上面生成的静态方法
        @Override
        public Integer apply(Integer a, Integer b) {
            return TestLambda.labmda$main$0(a, b);
        }
    }
    
    public static void main(String[] args) {
        //Lambda表达式本质就是创建了上面生成的类的对象,new MyLambda();
        BinaryOperator<Integer> operator = (a, b) -> a + b;
        System.out.println(operator.apply(1, 2));
    }
}

可以通过添加虚拟机参数-Djdk.internal.lambda.dumpProxyClasses,将生成的类和方法体现在编译文件中

MethodHandle

MethodHandle是一套api,用于操作各自实例方法、静态方法、构造器方法

MethodType.methodType用于描述方法返回值类型、参数类型

比如MethodType.methodType(int.class, int.class)表示一个返回一个int值,参数只有一个int的方法

public class MethodHandlerTest {
    
    public static void main(String[] args) throws Throwable {
        /**
         * findStatic:根据所在类、方法名、方法返回值和参数列表,寻找指定的静态方法
         * MethodType.methodType方法分别指定方法返回值类型和方法参数类型列表
         */
        MethodHandle staticMethod = MethodHandles.lookup()
            .findStatic(MethodHandlerTest.class, "test", 
                        MethodType.methodType(int.class, int.class));
        //调用
        staticMethod.invoke(12);

        /**
         * findVirtual,寻找实例方法
         */
        MethodHandle virtualMethod = MethodHandles.lookup()
            .findVirtual(MethodHandlerTest.class, "test1", 
                         MethodType.methodType(int.class, int.class));
        virtualMethod.invoke(new MethodHandlerTest(), 22);
        
        /**
         * findConstructor,寻找构造方法,否则方法不需要方法名称
         * 构造方法没有返回值,所以MethodType.methodType的第一个参数是void
         */
        MethodHandle constructorMethod = MethodHandles.lookup()
            .findConstructor(MethodHandlerTest.class,
                         MethodType.methodType(void.class));
        constructorMethod.invoke();
    }
    public MethodHandlerTest() {
        System.out.println("构造方法被调用啦");
    }

    public static int test(int a) {
        System.out.println("静态方法被调用啦 -> " + a);
        return a + a;
    }

    public int test1(int a) {
        System.out.println("实例被调用啦 -> " + a);
        return a + a;
    }
}

只能调用有权限访问的方法

生成类和对象

运行期间是怎么生成类和方法的呢?

生成函数对象所需要的类,使用了LambdaMetafactory.metafactory

该方法内部就是封装了MethodHandle

5.2 方法引用原理

方法引用同样是种语法糖

5.3 闭包原理

底层也是由最基本的类和对象、方法组成,

闭包提供的局部变量,在函数对象所生成的类中会变成私有final属性存在

闭包提供的静态变量,在函数对象所生成的类中可以通过类名直接使用该变量

闭包提供的实例变量,...

5.4 流的构建和切分原理