Java基础—函数式编程详解


Java函数式编程

1. 道之伊始

宇宙初开之际,混沌之气笼罩着整个宇宙,一切模糊不清。

然后,盘古开天,女娲造人:日月乃出、星辰乃现,山川蜿蜒、江河奔流、生灵万物,欣欣向荣。此日月、星辰、山川、江河、生灵万物,谓之【对象】,皆随时间而化。

然而:日月之行、星汉灿烂、山川起伏、湖海汇聚,冥冥中有至理藏其中。名曰【道】,乃万物遵循之规律,亦谓之【函数】,它无问东西,亘古不变

作为设计宇宙洪荒的程序员

  • 造日月、筑山川、划江河、开湖海、演化生灵万物、令其生生不息,则必用面向【对象】之手段
  • 若定规则、求本源、追纯粹,论不变,则当选【函数】编程之思想

下面就让我们从【函数】开始。

1.1 什么是函数

什么是函数呢?简单来说就是函数即规则

在数学上:

例如:

INPUT f(x) OUTPUT
1 ? 1
2 ? 4
3 ? 9
4 ? 16
5 ? 25
  • $f(x) = x^2$ 是一种规律, input 按照此规律变化为 output
  • 很多规律已经由人揭示,例如 $e = m \cdot c^2$​
  • 程序设计中可以自己去制定规律,一旦成为规则的制定者,你就是神

[!tip]

这里的规则定义其实就是函数的定义

1.2 大道无情

1.2.1 无情

何为无情:只要输入相同,无论多少次调用,无论什么时间调用,输出相同。

1.2.2 佛祖成道

例如如下代码:

public class TestMutable {

    public static void main(String[] args) {
        System.out.println(pray("张三"));
        System.out.println(pray("张三"));
        System.out.println(pray("张三"));
    }

    static class Buddha {
        String name;

        public Buddha(String name) {
            this.name = name;
        }
    }

    static Buddha buddha = new Buddha("佛祖");

    static String pray(String person) {
        return (person + "向[" + buddha.name + "]虔诚祈祷");
    }
}

以上 pray 的执行结果,除了参数变化外,希望函数的执行规则永远不变

张三向[佛祖]虔诚祈祷
张三向[佛祖]虔诚祈祷
张三向[佛祖]虔诚祈祷

然而,由于设计上的缺陷,函数引用了外界可变的数据,例如我在main中加上这样的代码:

buddha.name = "魔王";
System.out.println(pray("张三"));

结果就会是

张三向[魔王]虔诚祈祷

问题出在哪儿呢?函数的目的是除了参数能变化,其它部分都要不变,这样才能成为规则的一部分。佛祖要成为规则的一部分,也要保持不变,即你将静态内部类Buddha中的name属性调整为不可变即可。

改正方法:

static class Buddha {
    final String name;

    public Buddha(String name) {
        this.name = name;
    }
}

不是说函数不能引用外界的数据,而是它引用的数据必须也能作为规则的一部分

1.2.3 函数与方法

方法本质上也是函数。不过方法绑定在对象之上,它是对象个人法则

函数格式是:函数(对象数据,其它参数)

而方法的格式是:对象数据.方法(其它参数)

1.2.4 不变的好处

只有不变,才能在滚滚时间洪流中屹立不倒,成为规则的一部分。

多线程编程中,不变意味着线程安全

1.3 大道无形

1.3.1 函数化对象

函数本无形,也就是它代表的规则:位置固定、不能传播

若要有形,让函数的规则能够传播,需要将函数化为对象

[!note]

函数就相当于规则,而函数式对象就相当于行走的规则。

我现在有两处代码:

public class MyClass {
    static int add(int a, int b) {
        return a + b;
    }
} 

interface Lambda {
    int calculate(int a, int b);
}

Lambda add = (a, b) -> a + b; // 它已经变成了一个 lambda 对象

他们区别:

  • 前者是纯粹的一条两数加法规则,它的位置是固定的,要使用它,需要通过 MyClass.add 找到它,然后执行
  • 而后者(add 对象)就像长了腿,它的位置是可以变化的,想去哪里就去哪里,哪里要用到这条加法规则,把它传递过去

[!note]

接口的目的是为了将来用它来执行函数对象,此接口中只能有一个方法定义

例如:

public class AddClass {
    static int add(int a, int b) {
        return a + b;
    }

    interface AddFunction {
        int calculate(int a, int b);
    }

    public static void main(String[] args) {
        AddFunction addFunction = (a, b) -> a + b;

        System.out.println(AddClass.add(1,2));
        System.out.println(addFunction.calculate(1,2));

    }
}

从上面这个例子就可以看出,add方法他只是一个静态方法,以后是不可以作为参数传递给其他函数的,并且只能做某一种运算,但是我的接口AddFunction,里面只是定义了一个方法,具体的方法实现由使用者来决定,可以做多种运算,然后,你可以将方法的具体实现作为参数传递给其他函数(上述代码中,你传递addFunction即可),这里就要讲一下行为参数化了。

1.3.2 行为参数化

我有如下基本代码:

@Data
@AllArgsConstructor
public class Student {
    private String name;
    private int age;
    private String sex;

    public static void main(String[] args) {
        List<Student> students = ListUtil.of(
                new Student("张无忌", 18, "男"),
                new Student("杨不悔", 16, "女"),
                new Student("周芷若", 19, "女"),
                new Student("宋青书", 20, "男"));

    }
}

现在我有两个需求:

  1. 筛选出男性的学生
  2. 筛选出年龄小于18的学生

按照以前的方法,你可能会这样写:

@Data
@AllArgsConstructor
public class Student {
    //属性以及main省略...
    static List<Student> filter1(List<Student> students) {
        List<Student> result = new ArrayList<>();
        for (Student student : students) {
            if (student.sex.equals("男")) { //过滤性别为男性的学生
                result.add(student);
            }
        }
        return result;
    }

    static List<Student> filter2(List<Student> students) {
        List<Student> result = new ArrayList<>();
        for (Student student : students) {
            if (student.age <= 18) {//过滤出年龄小于18的学生
                result.add(student);
            }
        }
        return result;
    }

}

从上述代码就能够看出,代码的冗余十分高,只是判断的逻辑变了而已,那么我们是否可以提供一个函数化的对象,然后判断的逻辑由使用者提供呢?那当然是可以的。

  1. 定义接口:

    interface StudentFilter{
        /**
         * 因为你是写学生的过滤逻辑,所以你的返回值要为布尔类型,并且形参要为单个学生对象
         * @param student
         * @return
         */
        boolean studentFilter(Student student);
    }
    

    [!tip]

    这其实就是一个函数式接口。

  2. 提供公共的判断方法(函数):

    /**
     * 过滤出符合条件的学生
     * @param students 学生集合
     * @param studentFilter 接口对象
     * @return 符合条件的学生集合
     */
    static List<Student> filter(List<Student> students,StudentFilter studentFilter) {
        List<Student> result = new ArrayList<>();
        for (Student student : students) {
            if (studentFilter.studentFilter(student)) { //这里用使用者想要的判断逻辑
                result.add(student);
            }
        }
        return result;
    }
    
  3. 方法使用:

    System.out.println(filter(students,student -> student.getSex().equals("男")));
    System.out.println(filter(students,student -> student.getAge() <= 18));
    

完整代码:

@Data
@AllArgsConstructor
public class Student {
    private String name;
    private int age;
    private String sex;

    public static void main(String[] args) {
        List<Student> students = ListUtil.of(
                new Student("张无忌", 18, "男"),
                new Student("杨不悔", 16, "女"),
                new Student("周芷若", 19, "女"),
                new Student("宋青书", 20, "男"));

        System.out.println(filter(students,student -> student.getSex().equals("男")));
        System.out.println(filter(students,student -> student.getAge() <= 18));
    }

    /**
     * 过滤出符合条件的学生
     * @param students 学生集合
     * @param studentFilter 接口对象
     * @return 符合条件的学生集合
     */
    static List<Student> filter(List<Student> students,StudentFilter studentFilter) {
        List<Student> result = new ArrayList<>();
        for (Student student : students) {
            if (studentFilter.studentFilter(student)) { //这里用使用者想要的判断逻辑
                result.add(student);
            }
        }
        return result;
    }

}

interface StudentFilter{
    /**
     * 因为你是写学生的过滤逻辑,所以你的返回值要为布尔类型,并且形参要为单个学生对象
     * @param student
     * @return
     */
    boolean studentFilter(Student student);
}

执行结果:

[Student(name=张无忌, age=18, sex=), Student(name=宋青书, age=20, sex=)]
[Student(name=张无忌, age=18, sex=), Student(name=杨不悔, age=16, sex=)]

1.3.3 延迟执行

lambda表达式的一个重要特性是延迟执行(lazy execution),这意味着lambda表达式在创建时并不会立即执行,而是直到实际调用时才执行。

我们先来看一个简单的lambda表达式示例:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 使用lambda表达式
names.forEach(name -> System.out.println(name));

在这个例子中,forEach方法会立即执行System.out.println(name),因为它是一个终止操作(terminal operation)。但是,如果我们将lambda表达式与流(Stream)结合使用,情况会有所不同:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 使用Stream和lambda表达式
Stream<String> nameStream = names.stream().map(name -> {
    System.out.println("Mapping: " + name);
    return name.toUpperCase();
});

// 在这里没有任何输出,因为没有终止操作

在这个例子中,map方法是一个中间操作(intermediate operation),它会返回一个新的Stream,但不会立即执行lambda表达式。只有当我们添加一个终止操作时,整个流的计算才会被执行:

nameStream.forEach(name -> System.out.println("Final name: " + name));

此时,我们会看到所有的输出,包括映射(mapping)和最终的打印(final name),表明lambda表达式是在终止操作时才被执行的。

流的延迟执行依赖于两种操作类型:

  1. 中间操作(Intermediate Operations):这些操作是延迟执行的,它们会返回一个新的流,并且只有在流上执行终止操作时才会执行。常见的中间操作有mapfiltersorted等。
  2. 终止操作(Terminal Operations):这些操作会触发流的执行,并产生一个结果或副作用。常见的终止操作有forEachcollectreduce等。

为什么使用延迟执行?

  1. 性能优化:延迟执行允许Java流在需要的时候才计算,这可以减少不必要的计算,尤其是在处理大数据集时。比如,只有在需要最终结果时才执行中间操作,从而避免了中间结果的多次计算。
  2. 资源管理:通过延迟执行,可以更高效地管理资源,如内存和CPU。流可以通过惰性评估(lazy evaluation)来避免不必要的对象创建和内存占用。
  3. 流畅的API设计:延迟执行允许开发者以更自然和流畅的方式构建数据处理管道,这使得代码更加简洁和可读。

假设我们有一个大型数据集,需要对其进行多步处理,并且希望尽可能地提高效率:

List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "Dave", "Edward");

// 过滤掉长度小于5的名字,并转换为大写
List<String> result = names.stream()
    .filter(name -> {
        System.out.println("Filtering: " + name);
        return name.length() >= 5;
    })
    .map(name -> {
        System.out.println("Mapping: " + name);
        return name.toUpperCase();
    })
    .collect(Collectors.toList());

// 在这里才会输出过滤和映射的过程

在这个例子中,filtermap操作都是延迟执行的。只有在调用collect终止操作时,整个处理过程才会实际执行,并输出过滤和映射的过程。

2. 函数编程语法

2.1 表现形式

在 Java 语言中,Lambda对象有两种形式:Lambda表达式与方法引用

2.1.1 lambda表达式

语法:(parameters) -> expression或者(parameters) -> { statements; }

一个Lambda表达式由3部分组成:

  1. 参数列表:

    • 包含在圆括号 () 中,可以包含多个参数,参数之间用逗号 , 分隔。

    • 如果没有参数,写成 () -> expression

    • 如果只有一个参数且没有指定类型,可以省略圆括号,写成 parameter -> expression

    [!note]

    参数可以有类型声明,也可以没有类型声明。Java编译器可以通过上下文推断参数类型。

  2. 箭头操作符 ->:用于分隔参数列表和Lambda主体。

  3. Lambda主体:可以是一个表达式一段代码块

    • 如果是一个表达式,结果会自动返回,无需写return语句

    • 如果是一个代码块,需要用 {} 包裹,并且可以包含多条语句,必须显式地使用 return 语句返回值(如果需要返回值的话)。

例如:

  1. 指明参数类型,主体为表达式:

    (int a, int b) -> a + b;
    
  2. 指明参数类型,主体为代码块:

    (int a, int b) -> {
        int c = a + b;
        return c;
    }
    
  3. 不指明类型,由Java编译器自行推断:

    (a, b) -> a + b;
    

    [!caution]

    注意:这里的类型不可以随意省略,必须是能够通过上下文推导出的其类型的时候才可以省略,并且如果你省略,要不都省略,要不都不省略,不能是有的参数省略了类型,而有的参数没有省略类型。


基本概念讲完了,但是从这里开始,我们要树立起一个新的观念,要习惯将Lambda表达式看成是一个函数对象

那么既然是一个对象,那么在Java中,对象都是有类的,那么如何将Lambda抽象出一个类呢?这个我们后面的章节会继续讲到,这里先基于(a, b) -> a + b给出一个抽象的类:

interface Add{
    int opt(int a,int b);
}

使用时:

Add add = (a,b)->a+b;

[!note]

首先,我们**抽象一个Lambda表达式基本上都是抽象为一个接口类型(一般称为函数式接口)**,其次,接口中方法的返回值和参数类型要和Lambda表达式呼应上,这样,Java才能够在你省略参数类型的时候从上下文中推断出它的类型。

这样其实你就能够理解为什么lambda表达式是一个可移动的,位置不固定的对象了,其实它的本质就是一个接口,也符合了Java中面向接口编程的一个设计。

2.1.2 方法引用

语法:ClassName::methodName

方法引用可以分为四种主要类型:

  1. 静态方法引用:静态方法引用使用类名和静态方法名来引用

    语法:ClassName::staticMethodName

    规则:用于引用类的静态方法。Lambda方法的参数列表,返回值类型需要与函数式接口的抽象方法的参数列表以及返回值类型匹配

    Math::max
    

    查看max静态方法的参数列表:

    实际上等价于:

    (int a, int b) -> Math.max(a,b)
    
  2. 实例方法引用:实例方法引用是引用某个特定对象的实例方法。

    语法:instance::instanceMethodName

    规则:用于引用某个特定对象的实例方法。Lambda方法的参数列表,返回值类型需要与函数式接口的抽象方法的参数列表以及返回值类型匹配

    String str = "Hello, World!";
    str::toUpperCase;
    

    查看toUpperCase方法参数:

    实际上等价于:

    () -> str.toUpperCase();
    
  3. 对象的实例方法引用:这种方法引用是通过类名来引用实例方法,这些方法会应用于传递到Lambda表达式的第一个参数。

    语法:ClassName::instanceMethodName

    规则:用于引用类的实例方法,Lambda方法的第一个参数将作为调用该实例方法的对象。剩下的参数列表需要与函数式接口的抽象方法的参数列表匹配。并且二者的返回值类型要相同

    String::equals;
    

    查看equals方法参数:

    实际上等价于:

    (String str1, Object str2) -> str1.equals(str2);
    
  4. 构造方法引用:构造方法引用是引用构造函数,以便创建新的对象。

    语法:ClassName::new

    规则:用于引用类的构造方法。构造方法的参数列表需要与函数式接口的抽象方法的参数列表匹配

    Student::new
    

    如果Student类的构造方法是无参的,那么等价于下面的表达式:

    () -> new Student();
    

    如果Student类的构造方法是有参的,那么等价于下面的表达式:

    (参数1,参数2,参数3) -> new Student(参数1,参数2,参数3);
    

2.2 函数类型

Lambda表达式、方法引用多种多样,那么如何将他们进行分类呢?只有先进行了分类,才能将其抽象(接口)出来,这样它才能进行传递。

抽象需考虑

  1. 参数个数
  2. 以及类型
  3. 返回值类型

[!warning]

上面3个点都不是必须的。

当你多个Lambda表达式、方法引用同时满足上述规则的时候,你就可以将其看作同一类对象,然后抽象为一个接口。

同时,如果这个接口只有一个抽象方法,那么它就是一个函数式接口

[!note]

可能有些人之前了解过函数式接口,也知道有这么一个注解:@FunctionalInterface

如果某个接口加上了这个注解,那么它一定是一个函数式接口;

如果某个接口没有加这个注解,那么它可能是函数式接口;

因为@FunctionalInterface这个注解的作用只是在编译期间检查你这个接口是否符合函数式接口的要求,如果不符合,编译期间就会报错。最终决定它是否是函数式接口的,还是看这个接口中是否有且仅有一个抽象方法。

下面我有这样一些Lambda表达式,请你将其归类,并且给出对应的函数式接口:

(int a) -> (a & 1) == 0;
(int a, int b) -> a - b;
(int a, int b, int c) -> a + b + c;
() -> new Student;
(Student s) -> s.getName();
(int a, int b) -> a * b;
(Student s) -> s.getAge();
() -> new ArrayList<Student>();
(int a) -> BigInteger.valueOf(a).isProbablePrime(100);

按照上面提出的三点抽象规则,分类如下:

分类1:

(int a) -> (a & 1) == 0;
(int a) -> BigInteger.valueOf(a).isProbablePrime(100);

自己定义的函数式接口:

interface Type1{
    boolean opt1(int number);
}

JDK自己提供的函数式接口:

JDK自己提供的函数式接口


分类2:

(int a, int b) -> a - b;
(int a, int b) -> a * b;

自己定义的函数式接口:

interface Type2 {
    int opt2(int number1, int number2);
}

JDK自己提供的函数式接口:

JDK自己提供的函数式接口

[!note]

参数类型以及个数相同,返回值类型也相同


对于其他的:要不就是参数个数或类型不相同,要不就是返回值不相同。

就好比这两个:

(Student s) -> s.getName();
(Student s) -> s.getAge();

他们两个参数个数以及类型是一样的,但是他们的返回值类型不相同,这时候,你可以使用泛型来抽象

interface Type3<T> {
    T opt3(Student student);
}

JDK自己提供的函数式接口:

JDK自己提供的函数式接口


同样的,对于参数类型不同的,你也可以使用泛型进行抽象:

(Student s) -> s.getName();
(int a) -> (a & 1) == 0;

自己定义的函数式接口:

interface Type4<T,O> {
    T opt4(O object);
}

JDK自己提供的函数式接口:

JDK自己提供的函数式接口

2.3 常见的函数式接口

  1. Runnable

    对应的Lambda表达式:

    () -> void
    

    表示无参数,也没有返回值。

  2. Callable

    对应的Lambda表达式:

    () -> T
    

    表示无参数,有返回值

  3. Comparator

    对应的Lambda表达式:

    (T,T) -> int
    

    表示需要比较的对象有两个,返回值为int类型;

    如果前一个大于后一个,返回值为正数;

    如果前一个小于后一个,返回值为负数;

    如果前一个等于后一个,返回值为零。

  4. Consumer

    这其实是一类接口,统称为消费者接口,顾名思义,就是只消费(有参数)不生产(无返回值)

    Consumer源码

    对应的Lambda表达式:

    (T) -> void 
    

    其同类还有:

    函数式接口 Lambda表达式 参数 返回值 助记
    BiConsumer<T, U> (T t, U u) -> void 两个参数 无返回值 Bi你可以联想为Binary,表示有两部分的
    IntConsumer (int value) -> void 一个int类型的参数 无返回值 前缀识别
    LongConsumer (long value) -> void 一个long类型的参数 无返回值 前缀识别
    DoubleConsumer (double value) -> void 一个double类型的参数 无返回值 前缀识别
  5. Supplier

    这其实是一类接口,统称为生产者接口,顾名思义,就是只生产(有返回值)不消费(无参数)

    Supplier源码

    对应的Lambda表达式:

    () -> T
    

    其同类还有:

    函数式接口 Lambda表达式 参数 返回值 助记
    IntSupplier () -> int 无参数 int类型的返回值 前缀识别
    LongSupplier () -> long 无参数 long类型的返回值 前缀识别
    DoubleSupplier () -> double 无参数 double类型的返回值 前缀识别
  6. Function

    这其实是一类接口,统称为功能性接口,顾名思义,就是需要写功能的,有参数以及返回值

    Function部分源码

    对应的Lambda表达式:

    (T t) -> R
    

    其同类还有:

    函数式接口 Lambda表达式 参数 返回值 助记
    BiFunction<T, U, R> (T t, U u) -> R 两个参数 有返回值 Bi你可以联想为Binary,表示有两部分的
    IntFunction (int value) -> R 一个int类型参数 有返回值 前缀识别
    DoubleFunction (double value) -> R 一个double类型参数 有返回值 前缀识别
  7. Predicate

    这其实是一类接口,统称为断言接口,顾名思义,就是做判断的,有参数以及有布尔类型的返回值

    Predicate部分源码

    对应的Lambda表达式:

    (T t) -> boolean
    

    其同类还有:

    函数式接口 Lambda表达式 参数 返回值 助记
    BiPredicate<T, U> (T t, U u) -> boolean 两个参数 布尔值 Bi你可以联想为Binary,表示有两部分的
    IntPredicate (int value) -> boolean 一个int类型参数 布尔值 前缀识别
    DoublePredicate (double value) -> boolean 一个double类型参数 布尔值 前缀识别
  8. **其他 **:

    函数式接口 Lambda表达式 参数 返回值 助记
    UnaryOperator (T t) -> T 一个参数 参数类型与返回值类型相同 Unary表示有一元
    BinaryOperator (T a, T b) -> T 两个参数 参数类型与返回值类型相同 Binary表示两部分
    IntUnaryOperator (int value) -> int 一个int类型参数 参数类型与返回值类型相同 前缀识别

2.4 练习

把下面方法中,可能存在变化的部分,抽象为函数对象,并且从外界传递进来。

2.4.1 练习1

我有如下代码:

static List<Integer> filter(List<Integer> list) {
    List<Integer> result = new ArrayList<>();
    for (Integer number : list) {
        // 筛选:判断是否是偶数,但以后可能改变判断规则
        if((number & 1) == 0) {
            result.add(number);
        }
    }
    return result;
}

改造之后:

static List<Integer> filter(List<Integer> list, IntPredicate predicate) {
    List<Integer> result = new ArrayList<>();
    for (Integer number : list) {
        // 筛选:判断是否是偶数,但以后可能改变判断规则
        if(predicate.test(number)) {
            result.add(number);
        }
    }
    return result;
}

调用处:

System.out.println(filter(Arrays.asList(1,2,3,4,5,6,7,8),number->(number & 1) == 0));

执行结果:

[2, 4, 6, 8]

2.4.2 练习2

static List<String> map(List<Integer> list) {
    List<String> result = new ArrayList<>();
    for (Integer number : list) {
        // 转换:将数字转为字符串,但以后可能改变转换规则
        result.add(String.valueOf(number));
    }
    return result;
}

改造过后:

static List<String> map(List<Integer> list, Function<Object,String> function) {
    List<String> result = new ArrayList<>();
    for (Integer number : list) {
        // 转换:将数字转为字符串,但以后可能改变转换规则
        result.add(function.apply(number));
    }
    return result;
}

调用处:

System.out.println(map(Arrays.asList(1,2,3,4,5,6,7,8), String::valueOf));

执行结果:

[1, 2, 3, 4, 5, 6, 7, 8]

2.4.3 练习3

static void consume(List<Integer> list) {
    for (Integer number : list) {
        // 消费:打印,但以后可能改变消费规则
        System.out.println(number);
    }
}

改造之后:

static void consume(List<Integer> list, IntConsumer consumer) {
    for (Integer number : list) {
        // 消费:打印,但以后可能改变消费规则
        consumer.accept(number);
    }
}

调用处:

consume(Arrays.asList(1,2,3,4,5,6,7,8), System.out::println);

执行结果:

1
2
3
4
5
6
7
8

2.4.4 练习4

static List<Integer> supply(int count) {
    List<Integer> result = new ArrayList<>();
    for (int i = 0; i < count; i++) {
        // 生成:随机数,但以后可能改变生成规则
        result.add(ThreadLocalRandom.current().nextInt());
    }
    return result;
}

改造之后:

static List<Integer> supply(int count, IntSupplier supplier) {
    List<Integer> result = new ArrayList<>();
    for (int i = 0; i < count; i++) {
        // 生成:随机数,但以后可能改变生成规则
        result.add(supplier.getAsInt());
    }
    return result;
}

调用处:

System.out.println(supply(5,()->ThreadLocalRandom.current().nextInt()));

执行结果:

[-567412419, -2077876663, -1583467706, 2117964999, -1146305546]

2.4.5 练习5

写出等价的方法引用:

Function<String, Integer> lambda = (String s) -> Integer.parseInt(s);

等价于:

Function<String, Integer> lambda = Integer::parseInt;

BiPredicate<List<String>, String> lambda = (list, element) -> list.contains(element);

等价于:

BiPredicate<List<String>, String> lambda = List::contains;

BiPredicate<Student, Object> lambda = (stu, obj) -> stu.equals(obj);

等价于:

BiPredicate<Student, Object> lambda = Student::equals;

Predicate<File> lambda = (file) -> file.exists();

等价于:

Predicate<File> lambda = File::exists;

Runtime runtime = Runtime.getRuntime();

Supplier<Long> lambda = () -> runtime.freeMemory();

等价于:

Runtime runtime = Runtime.getRuntime();

Supplier<Long> lambda = runtime::freeMemory;

2.4.6 练习6

我有如下代码:

public Color(Integer red, Integer green, Integer blue) { }

这是一个3个参数的构造方法,如果想用 Color::new 来构造 Color 对象,还应当补充哪些代码?

补充代码:

class Color {
    public Color(Integer red, Integer green, Integer blue) { }
}

interface ColorInterface {
    Color create(Integer red, Integer green, Integer blue);
}

实际调用处:

ColorInterface colorInterface = Color::new; //这一步并没有立即创建,会延迟执行
System.out.println(colorInterface.create(255,255,255)); //这里才是真正的创建

[!note]

为这是一个3参的构造函数,在已有的JDK的函数式接口中,是没有可用的接口,所以这里你必须自己创建函数式接口,然后才能够使用Color::new 的方法来构造Color对象

2.5 闭包(Closure)

定义:闭包是指在函数内部定义的函数可以访问外部函数中的变量。换句话说,Lambda表达式(或者匿名类)可以捕获并使用其所在环境中的变量。

Lambda表达式和匿名类都可以访问其外部作用域中的变量,这些变量被称为“自由变量”。闭包的主要特性包括:

  1. 访问外部变量:Lambda表达式可以访问其定义处的外部局部变量。
  2. 变量的不可变性:被捕获的外部局部变量必须是“有效最终变量”(effectively final)。也就是说,这些变量在Lambda表达式中不能被修改,要么是用final修饰的,要么就是没用final修饰,但是从来没有改过的变量。

例如:

import java.util.function.Function;

public class ClosureExample {
    public static void main(String[] args) {
        // 定义一个外部变量
        int num = 10;

        // Lambda表达式捕获外部变量(num)
        Function<Integer, Integer> adder = (x) -> x + num;

        // 调用Lambda表达式
        int result = adder.apply(5);
        System.out.println(result);  // 输出 15

        // 尝试修改外部变量
        // num = 15;  // 这行代码会导致编译错误,因为 num 必须是有效最终变量
    }
}

在这个例子中:

  • num 是一个外部变量,被Lambda表达式捕获。
  • adder 是一个Lambda表达式,它捕获了外部变量 num 并将其与参数 x 相加。
  • 捕获的变量 num 在Lambda表达式中是不可变的,因此尝试修改 num 会导致编译错误,要不就直接给num使用final修饰。

总结:

  1. 什么是闭包:函数对象和它外界变量绑定在一起
  2. 闭包的限制:闭包的变量必须是final或者effective final
  3. 闭包的作用:给函数执行提供数据的手段

2.6 柯里化(Carrying)

柯里化的基本概念:柯里化的主要目的是将一个多参数函数转换为一系列的单参数函数。换句话说,如果有一个函数 f(x, y, z),通过柯里化可以将其转换为 f(x)(y)(z)。这样,每次调用函数时只传递一个参数,返回一个新的函数。

[!note]

Java中柯里化的作用是让函数对象分步执行(本质上是利用多个函数对象和闭包)

例如:

@FunctionalInterface
interface F1 {
    int op(int a, int b);
}

调用处:

F1 f1 = (a, b) -> a + b;
System.out.println(f1.op(1,2));

现在我要将他进行柯里化,让其分步执行:

@FunctionalInterface
interface One {
    Two op(int a);
}

@FunctionalInterface
interface Two {
    int op(int b);
}

调用处:

One one = a -> b -> a+b;
System.out.println(one.op(1).op(2));

总结:

  1. 什么是柯里化:让接收多个参数的函数转换成一系列接收一个参数的函数
  2. 如何实现柯里化:结合闭包实现
  3. 柯里化的作用:让函数分步执行

2.7 高阶函数

在Java的Lambda表达式中,高阶函数是指可以接收其他函数作为参数,或返回一个函数作为结果的函数

下面我给出具体的案例来体会一下。

我现在有如下代码:

public class SimpleStream<T> {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        SimpleStream.of(list)
                .filter(x -> x % 2 == 0)
                .map(x -> x * x)
                .foreach(System.out::println);
    }
}

现在我自己创建了一个简单流(SimpleStream);模仿Stream,请你完善我的of、filter、map、foreach方法,从而来体会一下高阶函数:

public class SimpleStream<T> {
    private Collection<T> collection;

    public SimpleStream(Collection<T> collection) {
        this.collection = collection;
    }

    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        SimpleStream.of(list)
                .filter(x -> x % 2 == 0)
                .map(x -> x * x)
                .foreach(System.out::println);
    }

    /**
     * 因为是类.of 所以这里是静态,同时,这里要返回一个SimpleStream实例形成链式调用
     *
     * @param collection
     * @param <T>
     * @return
     */
    public static <T> SimpleStream<T> of(Collection<T> collection) {
        return new SimpleStream<>(collection);
    }

    /**
     * 这里是过滤,由于链式调用,你要返回SimpleStream对象,同时,你的lambda
     * 表达式是:提供一个元素,返回一个boolean,所以使用Predicate接口
     *
     * @param predicate
     * @return
     */
    public SimpleStream<T> filter(Predicate<T> predicate) {
        List<T> list = new ArrayList<>();
        for (T t : collection) {
            if (predicate.test(t)) {
                list.add(t);
            }
        }
        return new SimpleStream<>(list);
    }


    /**
     * 这里是处理元素,由于链式调用,你要返回SimpleStream对象,同时,你的lambda
     * 表达式是提供一个元素,返回一个元素,所以,你要使用Function接口,同时,
     * SimpleStream对象的泛型应该为返回元素的类型
     *
     * @param function
     * @param <R>
     * @return
     */
    public <R> SimpleStream<R> map(Function<T, R> function) {
        List<R> list = new ArrayList<>();
        for (T t : collection) {
            list.add(function.apply(t));
        }
        return new SimpleStream<>(list);
    }

    /**
     * 这里是打印元素,算是中止操作,所以无需返回值
     * 其次,lambda表达式是一个方法引用,简单来说就是有参无返回值
     * 所以这里的接口使用消费者接口
     * @param consumer
     */
    public void foreach(Consumer<T> consumer){
        for (T t : collection) {
            consumer.accept(t);
        }
    }
}

当你全部实现上诉代码之后,相信你对高阶函数已经有了一些理解了。

3. StreamAPI

3.1 什么是Stream

Stream 是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。

Stream 和 Collection 集合的区别:Collection 是一种静态的内存数据结构,讲的是数据,而 Stream 是有关计算的,讲的是计算。前者是主要面向内存,存储在内存中,后者主要是面向 CPU,通过 CPU 实现计算。

[!caution]

  1. Stream 自己不会存储元素。
  2. Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream
  3. Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。即一旦执行终止操作,就执行中间操作链,并产生结果。
  4. Stream一旦执行了终止操作,就不能再调用其它中间操作或终止操作了

对于第4点注意项,使用一个例子就很好的证明了:

public static void main(String[] args) {
    IntStream intStream = IntStream.of(1, 2, 3, 4);
    intStream.forEach(System.out::println); //执行一次终止操作
    intStream.forEach(System.out::println); //再次执行终止操作报错
}

执行结果:

执行结果

3.2 Stream的操作三个步骤

  1. 创建 Stream
    一个数据源(如:集合、数组),获取一个流

  2. 中间操作
    每次处理都会返回一个持有结果的新Stream,即中间操作的方法返回值仍然是Stream类型的对象。因此中间操作可以是个操作链,可对数据源的数据进行n次处理,但是在终结操作前,并不会真正执行。

    方法名称 描述 示例
    filter(Predicate<T>) 筛选符合条件的元素 stream.filter(x -> x > 10)
    map(Function<T, R>) 对每个元素应用函数并返回新的流 stream.map(String::length)
    flatMap(Function<T, Stream<R>>) 将每个元素转换为流并合并所有流的元素为一个流 stream.flatMap(Collection::stream)
    distinct() 去除流中的重复元素 stream.distinct()
    sorted() 对流中的元素进行自然排序 stream.sorted()
    sorted(Comparator<T>) 根据比较器对流中的元素进行排序 stream.sorted(Comparator.reverseOrder())
    peek(Consumer<T>) 对每个元素执行操作,但不改变流 stream.peek(System.out::println)
    limit(long n) 截取前n个元素 stream.limit(5)
    skip(long n) 跳过前n个元素 stream.skip(3)
    mapToInt(ToIntFunction<T>) 将元素映射为IntStream stream.mapToInt(String::length)
    mapToLong(ToLongFunction<T>) 将元素映射为LongStream stream.mapToLong(Long::valueOf)
    mapToDouble(ToDoubleFunction<T>) 将元素映射为DoubleStream stream.mapToDouble(Double::valueOf)
    flatMapToInt(Function<T, IntStream>) 将元素映射为IntStream并合并流 stream.flatMapToInt(arr -> Arrays.stream(arr))
    flatMapToLong(Function<T, LongStream>) 将元素映射为LongStream并合并流 stream.flatMapToLong(arr -> Arrays.stream(arr))
    flatMapToDouble(Function<T, DoubleStream>) 将元素映射为DoubleStream并合并流 stream.flatMapToDouble(arr -> Arrays.stream(arr))
  3. 终止操作(终端操作)
    终止操作的方法返回值类型就不再是Stream了,因此一旦执行终止操作,就结束整个Stream操作了。一旦执行终止操作,就执行中间操作链,最终产生结果并结束Stream。

    方法名称 描述 示例
    forEach(Consumer<T>) 对流中的每个元素执行操作 stream.forEach(System.out::println)
    forEachOrdered(Consumer<T>) 按顺序对流中的每个元素执行操作 stream.forEachOrdered(System.out::println)
    toArray() 将流中的元素收集到数组 Object[] arr = stream.toArray()
    toArray(IntFunction<A[]>) 将流中的元素收集到指定类型的数组 String[] arr = stream.toArray(String[]::new)
    reduce(BinaryOperator<T>) 对流中的元素进行归约操作 stream.reduce((a, b) -> a + b)
    reduce(T identity, BinaryOperator<T>) 带初始值的归约操作 stream.reduce(0, (a, b) -> a + b)
    reduce(U identity, BiFunction<U, ? super T, U>, BinaryOperator<U>) 带初始值的归约操作 stream.reduce(0, (sum, x) -> sum + x, Integer::sum)
    collect(Collector<? super T, A, R>) 将流中的元素收集到集合、列表、映射等 stream.collect(Collectors.toList())
    collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner) 使用自定义方法进行收集 stream.collect(ArrayList::new, List::add, List::addAll)
    min(Comparator<? super T>) 找出流中的最小值 stream.min(Comparator.naturalOrder())
    max(Comparator<? super T>) 找出流中的最大值 stream.max(Comparator.naturalOrder())
    count() 计算流中的元素个数 long count = stream.count()
    anyMatch(Predicate<T>) 判断是否有任意一个元素符合条件 boolean exists = stream.anyMatch(x -> x > 10)
    allMatch(Predicate<T>) 判断是否所有元素都符合条件 boolean allMatch = stream.allMatch(x -> x > 10)
    noneMatch(Predicate<T>) 判断是否所有元素都不符合条件 boolean noneMatch = stream.noneMatch(x -> x > 10)
    findFirst() 返回第一个元素(可能为空) Optional<T> first = stream.findFirst()
    findAny() 返回任意一个元素(可能为空) Optional<T> any = stream.findAny()

总结图

[!note]

中间操作是惰性执行的(lazy),它们会返回一个新的流,并且只有在终止操作执行时才会被真正执行。终止操作会触发流的执行,并返回一个结果或者产生副作用。通过结合中间操作和终止操作,可以对数据进行复杂的处理和转换。

3.3 Stream具体API

3.3.1 构建流(Stream)

构建流是需要用现有的数据来生成流(Stream)。

方式一:通过集合

Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:

  • default Stream<E> stream() : 返回一个顺序流

  • default Stream<E> parallelStream() : 返回一个并行流

例如:

@Test
public void test01(){
    List<Integer> list = Arrays.asList(1,2,3,4,5);

    //JDK1.8中,Collection系列集合增加了方法
    Stream<Integer> stream = list.stream();
}

方式二:通过数组

Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:

  • static <T> Stream<T> stream(T[] array): 返回一个流
  • public static IntStream stream(int[] array)
  • public static LongStream stream(long[] array)
  • public static DoubleStream stream(double[] array)

例如:

@Test
public void test02(){
    String[] arr = {"hello","world"};
    Stream<String> stream = Arrays.stream(arr); 
}

@Test
public void test03(){
    int[] arr = {1,2,3,4,5};
    IntStream stream = Arrays.stream(arr);
}

方式三:通过Stream的of()

可以调用Stream类静态方法 of(), 通过显示值创建一个流。它可以接收任意数量以及类型的参数。

  • public static<T> Stream<T> of(T... values) : 返回一个流

例如:

@Test
public void test04(){
    Stream<Integer> stream = Stream.of(1,2,3,4,5);
    stream.forEach(System.out::println);
}

[!note]

由于可以接收任意数量以及类型的参数,所以,一般用它来创建对象的Stream。

3.3.2 过滤(Filter)

在Stream API中,filter是一个用于过滤元素的中间操作。它接受一个Predicate(谓词)作为参数,该谓词用于筛选流中的元素。只有谓词返回true的元素才会被保留在流中,而返回false的元素则会被过滤掉。对应源码:

Stream<T> filter(Predicate<? super T> predicate);

使用示例:

假设有一个List包含一些整数:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

我们想要过滤出所有的偶数。可以使用filter操作来实现:

List<Integer> evenNumbers = numbers.stream()
                                  .filter(num -> num % 2 == 0)
                                  .collect(Collectors.toList());

System.out.println(evenNumbers); // 输出结果为 [2, 4, 6, 8, 10]

在这个例子中:

  • numbers.stream()List转换为一个流。
  • .filter(num -> num % 2 == 0)对流中的每个元素进行判断,保留满足条件(即为偶数)的元素。
  • .collect(Collectors.toList())将过滤后的元素收集到一个新的List中。

3.3.3 映射(Map)

在Stream API中,map是一个用于映射每个元素到对应结果的中间操作。它接受一个Function作为参数,该Function会被应用到流中的每个元素上,并返回一个新的元素。结果是一个包含映射后元素的流。

使用示例:

假设有一个List包含一些整数,我们想要将每个整数乘以2。可以使用map操作来实现:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

List<Integer> doubledNumbers = numbers.stream()
                                      .map(num -> num * 2)
                                      .collect(Collectors.toList());

System.out.println(doubledNumbers); // 输出结果为 [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

在这个例子中:

  • numbers.stream()List转换为一个流。
  • .map(num -> num * 2)对流中的每个元素应用num -> num * 2这个函数,将每个整数乘以2。
  • .collect(Collectors.toList())将映射后的元素收集到一个新的List中。

3.3.4 降维(FlatMap)

在Stream API中,flatMap是一个非常强大的中间操作,用于将一个流中的每个元素转换为另一个流,然后将这些流合并成一个单一的流。它接受一个返回流的Function作为参数。

[!note]

简单来说就是将两个流或者多个流合并为一个流。

使用场景flatMap通常用于将包含多个嵌套结构的流展平成一个流,例如处理包含列表的列表,或者处理包含字符串的字符串。

使用示例:

假设有一个包含若干List对象的List,我们想要将所有内部的List合并成一个单一的List。可以使用flatMap操作来实现:

List<List<String>> listOfLists = Arrays.asList(
    Arrays.asList("a", "b", "c"),
    Arrays.asList("d", "e", "f"),
    Arrays.asList("g", "h", "i")
);

List<String> flatList = listOfLists.stream()
                                   .flatMap(List::stream)
                                   .collect(Collectors.toList());

System.out.println(flatList); // 输出结果为 [a, b, c, d, e, f, g, h, i]

在这个例子中:

  • listOfLists.stream()List<List<String>>转换为一个流。
  • .flatMap(List::stream)对流中的每个List应用List::stream这个方法引用,将每个内部List转换为流,并将所有这些流合并成一个单一的流。
  • .collect(Collectors.toList())将合并后的流收集到一个新的List中。

假设有一个包含若干句子的List,我们想要将每个句子拆分成单词,并将所有单词合并成一个单一的List

List<String> sentences = Arrays.asList(
    "hello world",
    "java stream",
    "flat map example"
);

List<String> words = sentences.stream()
                              .flatMap(sentence -> Arrays.stream(sentence.split(" ")))
                              .collect(Collectors.toList());

System.out.println(words); // 输出结果为 [hello, world, java, stream, flat, map, example]

在这个例子中:

  • sentences.stream()List<String>转换为一个流。
  • .flatMap(sentence -> Arrays.stream(sentence.split(" ")))对流中的每个句子应用sentence.split(" ")来将其拆分成单词数组,然后将单词数组转换为流,并将所有这些流合并成一个单一的流。
  • .collect(Collectors.toList())将合并后的流收集到一个新的List中。

3.3.5 拼接(Concat)

在Stream API中,Stream.concat是一个用于连接两个流的静态方法,是一个中间操作。它接受两个流作为参数,并返回一个新的流,该流是由这两个流按顺序连接而成的。

对应源码:

public static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
  • a:第一个流
  • b:第二个流

使用示例:

假设有两个包含整数的流,我们想要将它们连接成一个单一的流:

Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(4, 5, 6);

Stream<Integer> concatenatedStream = Stream.concat(stream1, stream2);

concatenatedStream.forEach(System.out::println); // 输出结果为 1, 2, 3, 4, 5, 6

在这个例子中:

  • Stream.of(1, 2, 3)创建了一个包含1, 2, 3的流。
  • Stream.of(4, 5, 6)创建了一个包含4, 5, 6的流。
  • Stream.concat(stream1, stream2)将这两个流连接成一个新的流。
  • concatenatedStream.forEach(System.out::println)遍历并打印连接后的流的元素。

假设有两个包含字符串的流,我们想要将它们连接成一个单一的流:

Stream<String> stream1 = Stream.of("a", "b", "c");
Stream<String> stream2 = Stream.of("d", "e", "f");

Stream<String> concatenatedStream = Stream.concat(stream1, stream2);

concatenatedStream.forEach(System.out::println); // 输出结果为 a, b, c, d, e, f

如果需要连接多个流,可以多次使用Stream.concat

Stream<String> stream1 = Stream.of("a", "b");
Stream<String> stream2 = Stream.of("c", "d");
Stream<String> stream3 = Stream.of("e", "f");

Stream<String> concatenatedStream = Stream.concat(
    Stream.concat(stream1, stream2),
    stream3
);

concatenatedStream.forEach(System.out::println); // 输出结果为 a, b, c, d, e, f

通过使用Stream.concat,可以方便地将多个流连接在一起,形成一个新的流,从而简化流的处理流程。

[!caution]

  • Stream.concat返回的流是顺序流。即,第一个流的所有元素在第二个流的任何元素之前。
  • 如果任意一个流为null,调用Stream.concat时会抛出NullPointerException
  • 连接后的流可能是无限流(Infinite Stream),如果其中任意一个流是无限的,则需要注意使用终端操作时可能会导致无限循环。

从这里,你能够看出flatmapconcat功能上有些类似,那么他们直接的区别是什么呢?

  • flatMap将每个元素替换为一个流,然后将所有这些流中的元素合并为一个流。适用于将包含集合的集合展开为一个平坦的集合。

  • Stream.concat将两个流连接起来,形成一个包含所有元素的新流。适用于需要连接两个独立的流。

虽然两者在某些情况下看起来功能相似,但它们解决的问题和应用场景有所不同flatMap更灵活且强大,可以处理嵌套的数据结构,而Stream.concat则更简单直接,用于连接两个流。

3.3.6 截取(skip & limit )

在Stream API中,skiplimit是两个用于控制流中元素数量的中间操作。它们可以用于跳过和限制流中的元素数量,从而对流进行截断。

skip用于跳过流中的前N个元素。如果流中元素的数量少于N个,则返回一个空的流。

源码:

Stream<T> skip(long n)
  • n:要跳过的元素数量。

使用示例:

假设有一个包含若干整数的流,我们想要跳过前3个元素:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> skippedStream = stream.skip(3);

skippedStream.forEach(System.out::println); // 输出结果为 4, 5, 6

在这个例子中:

  • Stream.of(1, 2, 3, 4, 5, 6)创建了一个包含整数的流。
  • .skip(3)跳过流中的前3个元素。
  • .forEach(System.out::println)遍历并打印剩余的元素。

limit用于限制流中的元素数量。如果流中元素的数量多于N个,则只保留前N个元素,其余的元素将被丢弃。如果limit的参数大于或等于流的长度,结果将是一个与原流相同的流。

源码:

Stream<T> limit(long maxSize)
  • maxSize:要保留的最大元素数量。

使用示例:

假设有一个包含若干整数的流,我们想要限制只保留前3个元素:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> limitedStream = stream.limit(3);

limitedStream.forEach(System.out::println); // 输出结果为 1, 2, 3

在这个例子中:

  • Stream.of(1, 2, 3, 4, 5, 6)创建了一个包含整数的流。
  • .limit(3)只保留流中的前3个元素。
  • .forEach(System.out::println)遍历并打印保留的元素。

可以结合使用skiplimit来对流进行更复杂的操作,例如跳过前3个元素,并只保留接下来的2个元素:

Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);

Stream<Integer> resultStream = stream.skip(3).limit(2);

resultStream.forEach(System.out::println); // 输出结果为 4, 5

在这个例子中:

  • Stream.of(1, 2, 3, 4, 5, 6)创建了一个包含整数的流。
  • .skip(3)跳过流中的前3个元素。
  • .limit(2)只保留接下来2个元素。
  • .forEach(System.out::println)遍历并打印这些元素。

3.3.7 查找与匹配(Find & Match)

在Stream API中,FindMatch系列操作是用于在流中查找元素和匹配条件的终端操作。它们主要包括以下几种:

Find系列操作

  1. findFirstfindFirst返回流中的第一个元素。如果流为空,则返回一个空的Optional

    源码:

    Optional<T> findFirst()
    

    使用示例:

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    Optional<String> firstElement = stream.findFirst();
    
    firstElement.ifPresent(System.out::println); // 输出结果为 a
    

    在这个例子中,findFirst返回流中的第一个元素”a”。

  2. findAnyfindAny返回流中的任意一个元素。对于并行流,这个元素不一定是第一个元素。如果流为空,则返回一个空的Optional

    源码:

    Optional<T> findAny()
    

    使用示例:

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    Optional<String> anyElement = stream.findAny();
    
    anyElement.ifPresent(System.out::println); // 可能输出 a, b, c, 或 d 中的任意一个
    

    在这个例子中,findAny返回流中的任意一个元素。

[!note]

一般我们在使用Find系列的操作的时候,往往跟Filter操作结合起来使用,并且,Find系列操作返回值都是Optional对象

Match系列操作

  1. allMatchallMatch用于检查流中的所有元素是否都匹配给定的谓词(Predicate)。如果所有元素都匹配,则返回true;否则返回false

    源码:

    boolean allMatch(Predicate<? super T> predicate)
    

    使用示例:

    Stream<Integer> stream = Stream.of(2, 4, 6, 8);
    
    boolean allEven = stream.allMatch(num -> num % 2 == 0);
    
    System.out.println(allEven); // 输出结果为 true
    

    在这个例子中,allMatch检查流中的所有整数是否都是偶数。

  2. anyMatchanyMatch用于检查流中的任意一个元素是否匹配给定的谓词(Predicate)。如果有任意一个元素匹配,则返回true;否则返回false

    源码:

    boolean anyMatch(Predicate<? super T> predicate)
    

    使用示例:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    
    boolean anyEven = stream.anyMatch(num -> num % 2 == 0);
    
    System.out.println(anyEven); // 输出结果为 true
    

    在这个例子中,anyMatch检查流中是否有任意一个整数是偶数。

  3. noneMatchnoneMatch用于检查流中的所有元素是否都不匹配给定的谓词(Predicate)。如果所有元素都不匹配,则返回true;否则返回false

    源码:

    boolean noneMatch(Predicate<? super T> predicate)
    

    使用示例:

    Stream<Integer> stream = Stream.of(1, 3, 5, 7);
    
    boolean noneEven = stream.noneMatch(num -> num % 2 == 0);
    
    System.out.println(noneEven); // 输出结果为 true
    

    在这个例子中,noneMatch检查流中的所有整数是否都不是偶数。

[!caution]

  • findFirstfindAny返回的结果是一个Optional,需要使用Optional的方法(如isPresentifPresent等)来处理可能为空的情况。
  • allMatchanyMatchnoneMatch短路操作,即一旦确定结果,就不会再处理剩余的元素。例如,对于allMatch,一旦发现一个不匹配的元素,便会立即返回false,而不再检查剩余元素。
  • 对于并行流(Parallel Stream),findAny可能会比findFirst更高效,因为它不要求返回第一个元素。

3.3.8 去重与排序(Distinct & Sorted)

在Stream API中,distinctsorted是用于处理流中元素的中间操作。它们分别用于去重和排序流中的元素。

distinct用于过滤流中的重复元素,只保留唯一的元素。它根据元素的equals方法来判断是否重复

[!note]

由于是根据元素的equals方法来判断是否重复,所以,如果是对象之间的去重,最好给对象重写equals方法,当然,你用Object的equals也是可以的。

源码:

Stream<T> distinct()

使用示例:

假设有一个包含若干整数的流,其中有一些重复的元素,我们想要去除这些重复的元素:

Stream<Integer> stream = Stream.of(1, 2, 3, 2, 4, 3, 5);

Stream<Integer> distinctStream = stream.distinct();

distinctStream.forEach(System.out::println); // 输出结果为 1, 2, 3, 4, 5

在这个例子中:

  • Stream.of(1, 2, 3, 2, 4, 3, 5)创建了一个包含整数的流。
  • .distinct()过滤掉流中的重复元素。
  • .forEach(System.out::println)遍历并打印去重后的元素。

sorted用于对流中的元素进行排序。它有两种变体:

  1. 自然排序(Natural Order):
    • 适用于实现了Comparable接口的元素。
  2. 自定义排序(Custom Order):
    • 通过提供一个Comparator来定义排序规则。

源码:

  1. 自然排序:

    Stream<T> sorted()
    
  2. 自定义排序:

    Stream<T> sorted(Comparator<? super T> comparator)
    

使用示例:

  1. 自然排序

    假设有一个包含若干整数的流,我们想要对这些整数进行自然排序:

    Stream<Integer> stream = Stream.of(5, 3, 1, 4, 2);
    
    Stream<Integer> sortedStream = stream.sorted();
    
    sortedStream.forEach(System.out::println); // 输出结果为 1, 2, 3, 4, 5
    

    在这个例子中:

    • Stream.of(5, 3, 1, 4, 2)创建了一个包含整数的流。
    • .sorted()对流中的整数进行自然排序。
    • .forEach(System.out::println)遍历并打印排序后的元素。
  2. 自定义排序

    假设有一个包含若干字符串的流,我们想要按字符串长度进行排序:

    Stream<String> stream = Stream.of("apple", "banana", "cherry", "date");
    
    Stream<String> sortedStream = stream.sorted(Comparator.comparingInt(String::length));
    
    sortedStream.forEach(System.out::println); // 输出结果为 date, apple, cherry, banana
    

    在这个例子中:

    • Stream.of("apple", "banana", "cherry", "date")创建了一个包含字符串的流。
    • .sorted(Comparator.comparingInt(String::length))按字符串长度对流中的字符串进行排序。
    • .forEach(System.out::println)遍历并打印排序后的元素。

[!caution]

  • distinct操作会使用内部的哈希集(HashSet)来跟踪已经看到的元素,因此元素必须正确实现equalshashCode方法。
  • sorted操作是有状态的,即它需要知道所有的元素才能进行排序,因此在处理大流或无限流时需要小心
  • 自然排序(sorted())适用于实现了Comparable接口的元素,如整数、字符串等。
  • 自定义排序(sorted(Comparator))可以使用任何自定义的比较器(Comparator)来定义排序规则。

还有一点,在自定义排序中,用到了Comparator接口,你需要仔细的去了解一下,这样排序起来才会熟练。

3.3.9 简化(reduce)

在Stream API中,reduce是一个终端操作,用于将流中的元素组合起来。它提供了一种方式,将流中的元素通过某个累加器(Accumulator)函数组合成一个单一的结果。reduce操作可以用于实现求和、求积、求最大值、最小值等各种聚合操作

reduce有三个变体:

  1. Optional<T> reduce(BinaryOperator<T> accumulator)

    这个变体没有初始值,它返回一个包含结果的Optional,如果流为空,则返回一个空的Optional

    使用示例:假设有一个包含若干整数的流,我们想要计算这些整数的和

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    
    Optional<Integer> sum = stream.reduce((a, b) -> a + b);
    
    sum.ifPresent(System.out::println); // 输出结果为 15
    

    在这个例子中:

    • Stream.of(1, 2, 3, 4, 5)创建了一个包含整数的流。
    • .reduce((a, b) -> a + b)使用累加器函数(a, b) -> a + b计算流中所有整数的和。
    • sum.ifPresent(System.out::println)如果结果存在,则打印结果。
  2. T reduce(T identity, BinaryOperator<T> accumulator)

    这个变体有一个初始值identity,它返回一个非Optional的结果。如果流为空,则返回初始值identity

    使用示例:假设有一个包含若干整数的流,我们想要计算这些整数的和,并提供一个初始值

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    
    int sum = stream.reduce(0, (a, b) -> a + b);
    
    System.out.println(sum); // 输出结果为 15
    

    在这个例子中:

    • Stream.of(1, 2, 3, 4, 5)创建了一个包含整数的流。
    • .reduce(0, (a, b) -> a + b)使用初始值0和累加器函数(a, b) -> a + b计算流中所有整数的和。
    • System.out.println(sum)打印结果。
  3. <U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner)

    这个变体用于并行流(Parallel Stream),它有一个初始值identity,一个累加器函数accumulator,和一个合并器函数combiner

    使用示例:假设有一个包含若干字符串的流,我们想要将这些字符串连接起来,并提供一个初始值

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    String concatenatedString = stream.reduce("", (partialString, element) -> partialString + element, (partialString1, partialString2) -> partialString1 + partialString2);
    
    System.out.println(concatenatedString); // 输出结果为 abcd
    

    在这个例子中:

    • Stream.of("a", "b", "c", "d")创建了一个包含字符串的流。
    • .reduce("", (partialString, element) -> partialString + element, (partialString1, partialString2) -> partialString1 + partialString2)使用初始值"",累加器函数(partialString, element) -> partialString + element和合并器函数(partialString1, partialString2) -> partialString1 + partialString2连接所有字符串。
    • System.out.println(concatenatedString)打印结果。

上面的示例其实都是很简单的示例,其实,还有一些简化的API,底层就是用的reduce。

比如:

  1. max和min
  2. count
  3. sum
  4. average

3.3.10 收集(Collect)

在Stream API中,collect是一个终端操作,用于将流中的元素转换成另一种形式。通常用于将流的元素收集到集合(如列表、集合)或摘要结果中。

collect方法有两个不同的签名:

  1. <R> R collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)
  2. <R, A> R collect(Collector<? super T, A, R> collector)

**二者之间的区别 **:

  1. collect(Collector<? super T, A, R> collector)

    这是一个高级别的、抽象的收集方法,使用预定义或自定义的Collector接口实现来执行收集操作。Collectors类提供了许多现成的收集器,极大简化了常见的收集操作。

    特点:

    • 使用简单,适合常见的收集操作(如收集到List、Set、Map等)。
    • 可以复用现有的收集器。
    • 通过组合不同的收集器,可以实现复杂的收集逻辑(如分组、分区、连接字符串等)。
  2. collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)

    这是一个低级别的、灵活的收集方法,允许开发者自定义收集的各个步骤。它需要提供三个参数:一个结果容器的供应者(Supplier),一个累加器(BiConsumer),以及一个合并器(BiConsumer)。

    特点:

    • 更灵活,可以自定义复杂的收集过程。
    • 适用于特殊的、无法通过标准收集器实现的收集操作。
    • 需要更多的代码和细节管理。

下面详细讲解一下3参的这个收集

<R> R collect(Supplier<R> supplier,BiConsumer<R, ? super T> accumulator,BiConsumer<R, R> combiner);

这个方法是一个更灵活的收集方法,允许我们自定义收集过程。它需要我们提供三个参数:

  1. Supplier<R> supplier:结果容器的供应者,用于创建一个新的结果容器。例如可以是一个ArrayListHashSet等。它的类型参数R表示结果容器的类型。
  2. BiConsumer<R, ? super T> accumulator:累加器,用于将元素添加到结果容器中。定义如何将流中的每个元素添加到结果容器中。R是结果容器的类型,T是流中元素的类型。
  3. BiConsumer<R, R> combiner:合并器,用于在并行流中将多个部分结果合并成一个。定义如何将两个结果容器合并在一起。通常在并行流中使用,以合并不同线程处理的部分结果。如果你没用到并行流,不可以传递NULL,必须传递一个lambda表达式,只是这个lambda表达式不做任何处理,例如(a,b) -> {}即可

示例:

  • 将流中的元素收集到一个ArrayList

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    List<String> list = stream.collect(
        ArrayList::new,        // Supplier: 提供一个新的ArrayList
        ArrayList::add,        // Accumulator: 将元素添加到ArrayList
        (a,b)->{}
    );
    
    System.out.println(list); // 输出结果为 [a, b, c, d]
    
  • 将流中的元素收集到一个StringBuilder中,并在每个元素之间添加逗号分隔符

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    StringBuilder result = stream.collect(
        StringBuilder::new,               // Supplier: 提供新的StringBuilder实例
        (sb, s) -> {                      // Accumulator: 将元素累加到StringBuilder
            if (sb.length() > 0) {
                sb.append(", ");
            }
            sb.append(s);
        },
       (a,b)->{}
    );
    
    System.out.println(result.toString()); // 输出结果为 a, b, c, d
    

使用collect(Supplier<R> supplier, BiConsumer<R, ? super T> accumulator, BiConsumer<R, R> combiner)方法,您可以完全控制收集过程,包括如何创建结果容器,如何将元素添加到结果容器,以及如何合并部分结果。它提供了极大的灵活性,适用于各种复杂的收集操作。


单参的收集这里不做讲解,因为里面涉及到了收集器(Collector),我们下一小节讲解。

3.3.11 收集器(Collector)

Collectors是一个工具类,提供了许多常用的收集器(Collectors)实现。下面是一些常用的收集器:

  1. 收集到List列表

    部分源码:

    List<T> collect(Collectors.toList())
    

    示例:

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    List<String> list = stream.collect(Collectors.toList());
    
    System.out.println(list); // 输出结果为 [a, b, c, d]
    
  2. 收集到Set集合

    部分源码:

    Set<T> collect(Collectors.toSet())
    

    示例:

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    Set<String> set = stream.collect(Collectors.toSet());
    
    System.out.println(set); // 输出结果为 [a, b, c, d]
    
  3. 收集到特定类型的集合

    部分源码:

    <C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory)
    

    示例:

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    LinkedList<String> linkedList = stream.collect(Collectors.toCollection(LinkedList::new));
    
    System.out.println(linkedList); // 输出结果为 [a, b, c, d]
    
  4. 收集到映射

    部分源码:

    <T, K, U> Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper, 
                                               Function<? super T, ? extends U> valueMapper)
    

    示例:

    Stream<String> stream = Stream.of("a", "bc", "d", "fgh");
    
    Map<String, Integer> map = stream.collect(Collectors.toMap(x->x, String::length));
    
    System.out.println(map); // 输出结果为 {a=1, bc=2, d=1, fgh=3}
    
  5. 计算摘要信息

    部分源码:

    Collector<T, ?, IntSummaryStatistics> summarizingInt(ToIntFunction<? super T> mapper)
    Collector<T, ?, LongSummaryStatistics> summarizingLong(ToLongFunction<? super T> mapper)
    Collector<T, ?, DoubleSummaryStatistics> summarizingDouble(ToDoubleFunction<? super T> mapper)
    

    示例:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
    
    IntSummaryStatistics stats = stream.collect(Collectors.summarizingInt(Integer::intValue));
    
    System.out.println("Count: " + stats.getCount()); // 输出结果为 Count: 5
    System.out.println("Sum: " + stats.getSum());     // 输出结果为 Sum: 15
    System.out.println("Min: " + stats.getMin());     // 输出结果为 Min: 1
    System.out.println("Average: " + stats.getAverage()); // 输出结果为 Average: 3.0
    System.out.println("Max: " + stats.getMax());     // 输出结果为 Max: 5
    
  6. 拼接字符串

    部分源码:

    public static Collector<CharSequence, ?, String> joining()
    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter)
    public static Collector<CharSequence, ?, String> joining(CharSequence delimiter,CharSequence prefix,CharSequence suffix)
    

    示例:

    Stream<String> stream = Stream.of("a", "b", "c", "d");
    
    String joined = stream.collect(Collectors.joining(", "));
    
    System.out.println(joined); // 输出结果为 a, b, c, d
    
  7. 分组

    部分源码:

    public static <T, K> Collector<T, ?, Map<K, List<T>>> groupingBy(Function<? super T, ? extends K> classifier)
        
    public static <T, K, A, D>
    Collector<T, ?, Map<K, D>> groupingBy(Function<? super T, ? extends K> classifier,Collector<? super T, A, D> downstream)
        
    public static <T, K, D, A, M extends Map<K, D>>
    Collector<T, ?, M> groupingBy(Function<? super T, ? extends K> classifier,
                                  Supplier<M> mapFactory,
                                  Collector<? super T, A, D> downstream)
    

    示例:

    Stream<String> stream = Stream.of("apple", "banana", "cherry", "date");
    
    Map<Integer, List<String>> groupedByLength = stream.collect(Collectors.groupingBy(String::length));
    
    System.out.println(groupedByLength); // 输出结果为 {5=[apple, cherry], 6=[banana], 4=[date]}
    

    [!tip]

    其实groupingBy与toMap功能上基本是类似的

  8. 分区

    部分源码:

    public static <T>
    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)
        
    public static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate,
                                                    Collector<? super T, A, D> downstream)
    

    示例:

    Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
    
    Map<Boolean, List<Integer>> partitioned = stream.collect(Collectors.partitioningBy(n -> n % 2 == 0));
    
    System.out.println(partitioned); // 输出结果为 {false=[1, 3, 5], true=[2, 4, 6]}
    

3.3.12 下游收集器(DownCollector)

下游收集器(downstream collector)是用于在复杂的收集操作中进一步处理或收集已经部分处理过的数据的收集器。它们通常与组合收集器(composite collector)一起使用,以实现更复杂的收集逻辑。常见的组合收集器包括Collectors.groupingByCollectors.partitioningBy等,这些组合收集器可以接受另一个收集器作为参数,这个参数就是下游收集器。

[!note]

其实你观察各个收集器中的源码,如果它的参数中要接收一个Collector<? super T, A, D> downstream,那么这个downstream实则就是一个下游收集器

组合收集器中的下游收集器:

groupingBy是一个组合收集器,用于根据某个分类函数对元素进行分组,并且可以接受一个下游收集器来进一步处理分组后的数据。

示例1:简单分组

@Data
class Person {
    private String name;
    private int age;

}

public class Main {
    public static void main(String[] args) {
        Stream<Person> people = Stream.of(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 30),
            new Person("David", 25)
        );

        Map<Integer, List<Person>> groupedByAge = people.collect(groupingBy(Person::getAge));

        System.out.println(groupedByAge);
        // 输出结果为:{25=[Bob (25), David (25)], 30=[Alice (30), Charlie (30)]}
    }
}

在这个例子中,groupingBy(Person::getAge)是一个组合收集器,它根据Person的年龄对流中的元素进行分组。默认情况下,分组后的数据将被收集到一个列表中。

紧接着,我们使用下游收集器进一步处理上一步分组之后的数据:

@Data
class Person {
    private String name;
    private int age;
}

public class Main {
    public static void main(String[] args) {
        Stream<Person> people = Stream.of(
            new Person("Alice", 30),
            new Person("Bob", 25),
            new Person("Charlie", 30),
            new Person("David", 25)
        );

        Map<Integer, List<String>> namesByAge = people.collect(
            groupingBy(
                Person::getAge,                         // 分类函数:根据年龄分组
                mapping(Person::getName, toList())      // 下游收集器:将Person映射到名字并收集到列表中
            )
        );

        System.out.println(namesByAge);
        // 输出结果为:{25=[Bob, David], 30=[Alice, Charlie]}
    }
}

在这个例子中,groupingBy(Person::getAge, mapping(Person::getName, toList()))中使用了下游收集器mapping(Person::getName, toList()),它将每个Person对象映射为名字,并将这些名字收集到一个列表中。

[!caution]

下游收集器只是一个相对的概念,不一定Collectors.mappingCollectors.collectingAndThen等都是下游收集器,只要这个收集器在某个收集器中作为参数,这个收集器就都称为下游收集器,例如Collectors.toListCollectors.toSet等其实都可以称为下游收集器。

所以,这是一个相对的概念,Collectors中所有的收集器都有可能称为下游收集器。

3.3.13 三种基本流

基本流包括IntStreamLongStreamDoubleStream,用于处理基本类型的流。基本类型的流要比包装类型的流的性能要更好一点。

例如我有如下基本流和包装流:

IntStream intStream = IntStream.of(1, 2, 3, 4);//基本流
Stream<Integer> integerStream = Stream.of(1, 2, 3, 4);//包装流

其中intStream的性能更好。

基本流的类型

  1. IntStream:处理int类型的流。

    IntStream intStream = IntStream.of(1, 2, 3, 4, 5);
    
  2. LongStream:处理long类型的流。

    LongStream longStream = LongStream.of(1L, 2L, 3L, 4L, 5L);
    
  3. DoubleStream:处理double类型的流。

    DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0, 4.0, 5.0);
    

基本流的创建:基本流可以通过多种方式创建,包括数组、范围、生成器等。

  1. 通过of方法创建

    IntStream intStream = IntStream.of(1, 2, 3);
    LongStream longStream = LongStream.of(1L, 2L, 3L);
    DoubleStream doubleStream = DoubleStream.of(1.0, 2.0, 3.0);
    
  2. 通过rangerangeClosed方法创建

    IntStream rangeStream = IntStream.range(1, 5);       // [1, 2, 3, 4]
    IntStream rangeClosedStream = IntStream.rangeClosed(1, 5);  // [1, 2, 3, 4, 5]
    

    [!tip]

    可见reang方式创建的结果是含头不含尾,rangeClosed是包含两边端点值的

  3. 通过generate方法创建

    generate方法用于生成基于提供的生产者函数的无限流。它需要一个参数,即生成元素的函数。

    IntStream generatedStream = IntStream.generate(() -> 1).limit(5); // [1, 1, 1, 1, 1]
    

    [!caution]

    generate创建的流是无限流,这里必须使用limit来限制

  4. 通过iterate方法创建

    iterate方法用于生成基于某个种子值和迭代函数的无限流。它需要两个参数:初始种子值和一个更新函数。每次迭代会使用更新函数计算下一个元素。

    IntStream iteratedStream = IntStream.iterate(0, n -> n + 2).limit(5); // [0, 2, 4, 6, 8]
    

    [!caution]

    iterate创建的流是无限流,这里必须使用limit来限制

  5. 从数组创建

    int[] intArray = {1, 2, 3, 4, 5};
    IntStream arrayStream = Arrays.stream(intArray);
    

基本流相对于普通流特有的一些方法

方法 描述
sum 计算流中所有元素的总和
average 计算流中所有元素的平均值,返回一个 Optional 类型
min 获取流中元素的最小值,返回一个 Optional 类型
max 获取流中元素的最大值,返回一个 Optional 类型
summaryStatistics 返回一个统计信息的对象,包含流中的元素个数、总和、最小值、平均值和最大值
boxed 将基本类型流转换为对应的包装类型流(如 IntStream 转换为 Stream<Integer>
asDoubleStream IntStreamLongStream 转换为 DoubleStream
asLongStream IntStream 转换为 LongStream
range 生成一个从起始值到结束值(不包括结束值)的顺序数字流
rangeClosed 生成一个从起始值到结束值(包括结束值)的顺序数字流

文章作者: 念心卓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 念心卓 !
  目录