Java函数式编程+Lambda表达式

文章目录

  • 函数式编程介绍
  • 纯函数
  • Lambda表达式基础
  • [函数式接口(Functional Interface)](#函数式接口(Functional Interface))
        • [1. **函数式接口的定义**](#1. 函数式接口的定义)
        • [2. **函数式接口与Lambda表达式的关系**](#2. 函数式接口与Lambda表达式的关系)
        • [3. **JDK内置的函数式接口**](#3. JDK内置的函数式接口)
        • [4. **Lambda表达式使用函数式接口的典型例子**](#4. Lambda表达式使用函数式接口的典型例子)
  • [**方法引用(Method Reference)详解**](#方法引用(Method Reference)详解)
    • [**1. 方法引用的基本概念**](#1. 方法引用的基本概念)
    • [**2. 方法引用的四种形式**](#2. 方法引用的四种形式)
      • [**2.1 引用静态方法**](#2.1 引用静态方法)
      • [**2.2 引用特定对象的实例方法**](#2.2 引用特定对象的实例方法)
      • [**2.3 引用构造方法**](#2.3 引用构造方法)
    • [**3. 方法引用与函数式接口的关系**](#3. 方法引用与函数式接口的关系)
    • [**4. 方法引用的实际应用**](#4. 方法引用的实际应用)
  • 常用的函数式接口

函数式编程介绍

Java8新引入函数式编程方式,大大的提高了编码效率。

从面向对象编程视角来看,程序中使用的变量,其实只是一个值的容器,这个容器中,可以放置不同的值。

从函数式编程视角,变量其实只是值的一个"别名",值本身是不能改的

采用函数式编程思想设计类,强调"类"所封装的数据,应该只被初始化一次,之后就不再更改。

JDK中的String类型,就是用"函数式编程"风格设计出来的一个例子。

Java 8引入了Lambda表达式特性,提供了Stream API,其内置的函数支持动态组合和级联调用,能够方便地实现"声明式"的编程方式。

函数式与面向对象编程的基本构造元素:

面向对象编程,编程的基本单元是"类",函数必须放在类中,从属于"类"。

函数式编程,以"函数"作为编程的基本构造块,函数之间可以相互协作和动态组合。

函数式编程能很容易地实现"行为的参数化"

函数封装了"行为",在函数式编程中,是"把函数作为值"来看待的。

既然"函数是值",那么它就可以作为另一个函数的参数或返回值,这个就是"行为的参数化"。

Java使用Lambda表达式来代表那些需要反复传递的行为,将其作为函数参数或返回值,从而实现了"行为参数化"。

这样讲有点抽象ヽ(´¬`)ノ,下面进行详细介绍。

纯函数

函数式编程中的"函数",强调要消除"副作用"。

所谓"副作用",就是指:

(1)函数的执行,受到其"运行上下文"的影响,在不同的运行环境中执行,会得到不同的结果。

(2)函数执行之后,会修改外界的数据,从而对外界的状态有所影响。

没有"副作用"的函数,可以放心地让多个线程调用。

抛出异常的函数,不满足"函数式编程"的要求

"纯函数"------没有副作用的函数:

一个方法是不是"Pure Function(纯函数)",关键就是它的运行,是不是有"副作用(Side Effect)"。纯函数是没有副作用的,只要输入参数值一定,它的结果总是一致的,从而可以安全地被跨线程调用而无需考虑线程同步问题。

java 复制代码
public class SideEffectIllustration {

    // 没有副作用的方法
    public int f1(int x) {
        return x * 2;
    }

    private int state = 0;
    // 有副作用的方法
    public int f2(int x) {
        state++;
        return x * 2 + state;
    }

    public static void main(String[] args) {
        SideEffectIllustration obj = new SideEffectIllustration();
        //创建10个线程,每个线程都调用f1或f2方法,观察多线程环境下
        //Pure Function的输出与有副作用的方法的输出有何区别
        Thread[] theads = new Thread[10];

        for (int i = 0; i < theads.length; i++) {
            final int index = i;
            theads[i] = new Thread(() -> {
                // Note:切换以下两句的注释,观察输出的结果
                System.out.println(String.format("第%d次,结果为:%d", index + 1, obj.f1(5)));
                //System.out.println(String.format("第%d次,结果为:%d", index + 1, obj.f2(5)));
            });
            theads[i].start();
        }
    }


}

在实际开发中,推荐尽量编写"纯函数"。

Lambda表达式基础

Lambda的引入

我们把只定义有一个抽象方法的接口,称为"单一抽象方法(SAM:Single Abstract Method)"的接口,在开发中可以有三种方式实现它:

传统方法

1. 顶层类
java 复制代码
interface MyInterface {
    void func();
}

class MyClass implements MyInterface {
    @Override
    public void func() {
        System.out.println("MyClass's func()");
    }
}

上述代码定义了一个MyInterface接口(它只定义了一个抽象方法,所以是SAM接口),接着,写了一个MyClass类实现这个接口,在这里,MyClass是一个顶层类。

然后,写了一个静态方法调用接口定义的方法:

java 复制代码
    public static void doWithMyInterface(MyInterface obj) {
        obj.func();
    }

使用传统编程方式,上面的代码是这样被调用的:

java 复制代码
        //传统方法,定义一个类实现接口,创建这个类的对象,
        //再把它传给doWithMyInterface()方法
        MyClass obj = new MyClass();
        doWithMyInterface(obj);
2. 内部类

除了使用顶层类,也可以使用内部类实现接口:

内部类的适用场景,主要是"仅在本类内部使用",不需要被外界调用,并且代码比较简短。

java 复制代码
        class MyInnerClass implements MyInterface{
            @Override
            public void func() {
                System.out.println("本地内部类,实现接口");
            }
        }
        //实例化本地内部类对象,传给示例方法
        doWithMyInterface(new MyInnerClass());
3. 匿名类

如果代码仅在特定方法内部调用,并且代码量也不大,可以直接使用匿名内部类实现接口:

java 复制代码
        doWithMyInterface(new MyInterface() {
            @Override
            public void func() {
                System.out.println("使用匿名内部类,实现接口");
            }
        });

Lambda

前面的代码是传统的经典的Java面向对象编程代码, 在开发中使用接口,所有代码可以很明确地分为"第一步、第二步、第三步......",很易于理解,也很规范,但每次都需要手工做这么多的事,似乎有点过于麻烦了。

为了便捷性考虑,Java设计者借鉴其他编程语言,把Lambda特性引入到了Java中。

就可以把上述的代码写成这样的形式。

java 复制代码
        //定义一个Lambda表达式,将其引用保存到变量中,
        //再把它传给doWithMyInterface()方法
        //从而可以节省下新定义一个类的工作任务
        MyInterface lambdaObj = () -> {
            System.out.println("Explicit Define Lambda object's func()");
        };
        doWithMyInterface(lambdaObj);

可以更进一步地简化为:

java 复制代码
        //直接把一个Lambda表达式作为doWithMyInterface()方法的参数
        //不仅不需要定义一个单独的类,甚至不再需要定义一个变量
        doWithMyInterface(() -> {
            System.out.println("inline lambda object's func()");
        });

** 直接执行Lambda表达式:**

要"执行"一个Lambda表达式所封装的代码,需要使用关联接口所定义的方法:

java 复制代码
        MyInterface lambdaObj2 = () -> {
            System.out.println("另一个Lambda表达式");
        };
        //Lambda表达式,也可以直接执行
        lambdaObj2.func();

在函数式编程代码中,函数与其它数据类型一样,也可以进行"赋值"和"传送",具体来说,就是可以定义"函数类型"的变量,函数可以成为另一个函数的"参数",函数也可以返回"另一个函数"。

可以把Lambda表达式理解为一种简洁的可传递匿名函数:它没有名称,但有参数列表,函数主体,返回类型,可能还有一个可以抛出的异常列表。

Lambda表达式格式:(如果方法体只有单行的话,可以把大括号去掉)

(参数列表) -> { 方法体 }

这是一些常见的Lambda使用例子:

再举一个完整的例子:

java 复制代码
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

public class UseComparator {

    public static void main(String args[]) {

        List<String> strings = new ArrayList<String>();

        strings.add("CCC");
        strings.add("ddd");
        strings.add("EEE");
        strings.add("AAA");
        strings.add("bbb");
        //使用Lambda表达式重写上述代码段
        Comparator<String> comparator = (str1, str2) -> {
            return str1.compareToIgnoreCase(str2);
        };
        strings.sort(comparator);
        System.out.println("Sort with comparator");

        //输出排序结果
        for (String str : strings) {
            System.out.println(str);
        }
    }
}

我们可以用Lambda表达式来写comparator方法,并在sort中利用这个方法进行排序。

现在知道了如何编写Lambda表达式,但在哪里使用呢?可以通过函数式接口来使用Lambda表达式。

Java通过"函数式接口"+ Lambda表达式,实现函数式编程。

函数式接口(Functional Interface)

能接收一个Lambda表达式的变量,必须是接口类型,并且这种接口,还必须是一种"函数式接口(functional interface)"。

所谓"函数式接口",就是"只定义有一个抽象方法的接口",在Java 8之前,这种接口被称为"SAM:Single Abstract Method"接口。

Java 8中,使用"@FunctionalInterface"标识一个"函数式接口"。

函数式接口 是 Java 8 引入的一个概念,指 只有一个抽象方法 的接口。

它是 Lambda 表达式的基础,Lambda 表达式可以直接替代函数式接口的实现。


1. 函数式接口的定义

函数式接口在语法上与普通接口无异,但必须确保只有一个抽象方法。

示例:
java 复制代码
@FunctionalInterface
public interface MyFunction {
    int apply(int x, int y); // 唯一的抽象方法
}

注意:

  • Java 8 提供了 @FunctionalInterface 注解,用于显式声明一个接口为函数式接口。
  • 如果有多个抽象方法,编译器会报错。

即使不加 @FunctionalInterface,接口只有一个抽象方法时,依然可以作为函数式接口使用。


2. 函数式接口与Lambda表达式的关系

Lambda表达式的本质 是一种简化语法,用来表示函数式接口的实例。

当一个 Lambda 表达式被传递时,JVM 自动将其映射为对应函数式接口的实现。

关联逻辑:
  1. 函数式接口提供了唯一的抽象方法
  2. Lambda 表达式的代码实现对应函数式接口的唯一抽象方法。
示例:
java 复制代码
@FunctionalInterface
interface MyFunction {
    int apply(int x, int y);
}

// 使用Lambda表达式实现MyFunction接口
MyFunction add = (x, y) -> x + y;

System.out.println(add.apply(3, 5)); // 输出: 8

解释:

  • MyFunction 是一个函数式接口。
  • Lambda表达式 (x, y) -> x + y 实现了 apply 方法。

3. JDK内置的函数式接口

Java 提供了许多内置的函数式接口,位于 java.util.function 包中。这些接口可以直接配合 Lambda 表达式使用。

常见接口:
  1. Predicate<T>:接收一个参数,返回布尔值。

    java 复制代码
    Predicate<Integer> isEven = n -> n % 2 == 0;
    System.out.println(isEven.test(4)); // 输出: true
  2. Consumer<T>:接收一个参数,无返回值。

    java 复制代码
    Consumer<String> print = s -> System.out.println(s);
    print.accept("Hello, Lambda!"); // 输出: Hello, Lambda!
  3. Function<T, R>:接收一个参数,返回一个结果。

    java 复制代码
    Function<Integer, String> intToString = n -> "Number: " + n;
    System.out.println(intToString.apply(10)); // 输出: Number: 10
  4. Supplier<T>:无参数,返回一个结果。

    java 复制代码
    Supplier<Double> random = () -> Math.random();
    System.out.println(random.get()); // 输出: 随机数
  5. BiFunction<T, U, R>:接收两个参数,返回一个结果。

    java 复制代码
    BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
    System.out.println(multiply.apply(3, 4)); // 输出: 12

4. Lambda表达式使用函数式接口的典型例子
  1. 线程启动:使用 Runnable 接口

    java 复制代码
    // 传统写法
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("Thread running...");
        }
    }).start();
    
    // 使用Lambda
    new Thread(() -> System.out.println("Thread running...")).start();
  2. 集合操作:Comparator接口

    java 复制代码
    List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
    
    // 使用Lambda表达式简化排序
    names.sort((a, b) -> a.compareTo(b));
    System.out.println(names);
  3. 事件处理:ActionListener接口

    java 复制代码
    JButton button = new JButton("Click Me");
    button.addActionListener(e -> System.out.println("Button clicked!"));

方法引用(Method Reference)详解

方法引用 是 Java 8 引入的一种简化 Lambda 表达式的语法,允许开发者通过直接引用已有方法来实现函数式接口的抽象方法,从而使代码更简洁、更可读。

Lambda表达式可以进一步简化为方法引用,直接使用现有方法实现接口的抽象方法。

每个方法引用都存在一个等效的 lambda 表达式

1. 方法引用的基本概念

  • 方法引用是 Lambda 表达式的简化形式。
  • 它使用双冒号 :: 操作符来引用方法。
  • 适用于 Lambda 表达式仅调用一个已有方法的场景。
方法引用的语法结构
java 复制代码
ClassName::methodName

例如:

java 复制代码
// Lambda 表达式
Function<String, Integer> lambda = s -> Integer.parseInt(s);

// 方法引用
Function<String, Integer> methodRef = Integer::parseInt;

在这两个例子中,Integer::parseInts -> Integer.parseInt(s) 的简化形式。


2. 方法引用的四种形式

2.1 引用静态方法

适用于 Lambda 表达式调用某个类的静态方法。

语法:

java 复制代码
ClassName::staticMethod

示例:

java 复制代码
// Lambda 表达式
Function<String, Integer> lambda = s -> Integer.parseInt(s);

// 方法引用
Function<String, Integer> methodRef = Integer::parseInt;

System.out.println(methodRef.apply("123")); // 输出: 123

分析:

  • Lambda 表达式 s -> Integer.parseInt(s) 调用的是 Integer 类的静态方法 parseInt
  • 通过 Integer::parseInt 简化了代码。

2.2 引用特定对象的实例方法

适用于 Lambda 表达式调用特定对象的实例方法。

语法:

java 复制代码
instance::instanceMethod

示例1:

java 复制代码
// 特定对象
String str = "Hello";

// Lambda 表达式
Supplier<Integer> lambda = () -> str.length();

// 方法引用
Supplier<Integer> methodRef = str::length;

System.out.println(methodRef.get()); // 输出: 5

分析:

  • Lambda 表达式 () -> str.length() 调用的是 str 对象的 length 方法。
  • 通过 str::length 简化代码。

示例2:

java 复制代码
// Lambda 表达式
BiFunction<String, String, Boolean> lambda = (s1, s2) -> s1.equals(s2);

// 方法引用
BiFunction<String, String, Boolean> methodRef = String::equals;

System.out.println(methodRef.apply("abc", "abc")); // 输出: true
System.out.println(methodRef.apply("abc", "def")); // 输出: false

分析:

  • Lambda 表达式 (s1, s2) -> s1.equals(s2) 调用的是 String 类实例的 equals 方法。
  • 通过 String::equals 进一步简化。

2.3 引用构造方法

适用于 Lambda 表达式用于创建对象的场景。

语法:

java 复制代码
ClassName::new

示例:

java 复制代码
// Lambda 表达式
Supplier<List<String>> lambda = () -> new ArrayList<>();

// 方法引用
Supplier<List<String>> methodRef = ArrayList::new;

List<String> list = methodRef.get();
System.out.println(list); // 输出: []

带参数的构造方法引用:

java 复制代码
// Lambda 表达式
Function<String, Integer> lambda = s -> new Integer(s);

// 方法引用
Function<String, Integer> methodRef = Integer::new;

System.out.println(methodRef.apply("123")); // 输出: 123

3. 方法引用与函数式接口的关系

方法引用本质上是对函数式接口的实现。

  • 函数式接口 要求实现唯一的抽象方法。
  • 方法引用 的方法与该抽象方法的签名必须一致。

示例:

java 复制代码
@FunctionalInterface
interface MyFunction {
    void print(String s);
}

// 使用 Lambda 表达式
MyFunction lambda = s -> System.out.println(s);

// 使用方法引用
MyFunction methodRef = System.out::println;

lambda.print("Hello, Lambda!");   // 输出: Hello, Lambda!
methodRef.print("Hello, Method Reference!"); // 输出: Hello, Method Reference!

4. 方法引用的实际应用

  1. 集合排序
java 复制代码
List<String> names = Arrays.asList("Bob", "Alice", "Charlie");

// 使用 Lambda 表达式
names.sort((a, b) -> a.compareTo(b));

// 使用方法引用
names.sort(String::compareTo);

System.out.println(names); // 输出: [Alice, Bob, Charlie]
  1. 流处理(Stream API)
java 复制代码
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");

// 使用 Lambda 表达式
names.stream().map(name -> name.toUpperCase()).forEach(name -> System.out.println(name));

// 使用方法引用
names.stream().map(String::toUpperCase).forEach(System.out::println);

常用的函数式接口

1. Consumer接口(级联多个Consumer)

Consumer的基本用法

Consumer<T> 接口用于接收一个输入参数并对其执行某些操作,但不会返回结果。常见用法是打印日志、更新状态、操作集合元素等。

  • 核心方法:

    java 复制代码
    void accept(T t);
    • 接收一个参数 t,对其执行操作。
    • 没有返回值。
级联Consumer - andThen方法

Consumer 提供了默认方法 andThen,可以将多个 Consumer 串联起来,按照顺序依次执行每个 Consumer 的逻辑。

  • 方法定义:

    java 复制代码
    default Consumer<T> andThen(Consumer<? super T> after)
    • 参数 after: 另一个 Consumer,会在当前 Consumer 执行完之后被调用。
    • 返回一个新的 Consumer,依次执行两个 Consumer 的操作。
  • 示例代码:

    java 复制代码
    public static void main(String[] args) {
        Consumer<String> consumer1 = str -> System.out.println("Consumer 1: " + str);
        Consumer<String> consumer2 = str -> System.out.println("Consumer 2: " + str.toUpperCase());
    
        // 将两个Consumer级联
        Consumer<String> combinedConsumer = consumer1.andThen(consumer2);
    
        combinedConsumer.accept("hello");
    }
    • 输出:

      Consumer 1: hello
      Consumer 2: HELLO
      

更复杂的级联示例

可以串联多个 Consumer 来实现更复杂的操作。

java 复制代码
public static void main(String[] args) {
    Consumer<String> consumer1 = str -> System.out.println("Step 1: " + str.trim());
    Consumer<String> consumer2 = str -> System.out.println("Step 2: " + str.toLowerCase());
    Consumer<String> consumer3 = str -> System.out.println("Step 3: " + str.toUpperCase());

    // 级联三个Consumer
    Consumer<String> combinedConsumer = consumer1.andThen(consumer2).andThen(consumer3);

    combinedConsumer.accept("  HeLLo WoRLd  ");
}
  • 输出:

    Step 1: HeLLo WoRLd
    Step 2: hello world
    Step 3: HELLO WORLD
    

2. Predicate接口与对象查找

Predicate的基本概念

Predicate<T> 接口主要用于定义一个"判断规则",用于表示一个"布尔值"函数。它接收一个输入参数,并返回一个布尔值,用于条件判断。

  • 核心方法:

    java 复制代码
    boolean test(T t);
    • 参数 t: 输入值。
    • 返回值:布尔值,用于判断输入是否满足条件。
  • 默认方法:

    • and : 将多个 Predicate 串联,所有条件均为 true 时返回 true
    • or : 至少一个条件为 true 时返回 true
    • negate : 对当前 Predicate 结果取反。
    • isEqual: 检查对象是否相等。

对象查找中的应用

Predicate 通常用于过滤集合、查找符合条件的对象。

  • 示例代码(查找满足条件的对象):

    java 复制代码
    public static void main(String[] args) {
        List<String> names = List.of("Alice", "Bob", "Charlie", "David");
    
        // 定义Predicate,查找长度大于3的名字
        Predicate<String> lengthPredicate = name -> name.length() > 3;
    
        // 查找符合条件的名字
        names.stream()
             .filter(lengthPredicate)
             .forEach(System.out::println);
    }
    • 输出:

      Alice
      Charlie
      David
      
  • 级联使用多个Predicate:

  • java 复制代码
    Predicate<String> startsWithA = name -> name.startsWith("A");
    Predicate<String> lengthPredicate = name -> name.length() > 3;
    
    // 组合条件:以A开头且长度大于3
    Predicate<String> combinedPredicate = startsWithA.and(lengthPredicate);
    
    names.stream()
         .filter(combinedPredicate)
         .forEach(System.out::println);
    • 输出:

      Alice
      

3. Function接口及相关接口

Function的基本概念

Function<T, R> 是 Java 8 中的函数式接口,此接口定义了一个apply方法,它接收一个T类型的对象,返回一个R类型的对象:

  • 核心方法:

    java 复制代码
    R apply(T t);
    • 参数 t: 输入值。
    • 返回值:类型为 R 的结果。
  • 默认方法:

    • andThen : 先执行当前 Function,再将结果传给另一个 Function
    • compose : 先执行参数指定的 Function,再将结果传递给当前 Function

示例代码
  • 基本用法:

    java 复制代码
    public static void main(String[] args) {
        Function<String, Integer> stringLength = str -> str.length();
    
        System.out.println(stringLength.apply("Hello")); // 输出 5
    }
  • 使用 andThencompose:

    java 复制代码
    public static void main(String[] args) {
        Function<String, Integer> stringLength = str -> str.length();
        Function<Integer, Integer> square = num -> num * num;
    
        // andThen: 先计算长度,再平方
        System.out.println(stringLength.andThen(square).apply("Hello")); // 输出 25
    
        // compose: 先平方长度,再计算平方
        System.out.println(square.compose(stringLength).apply("Hello")); // 输出 25
    }

相关接口
  1. BiFunction<T, U, R>

    作为Function接口的特例,有一个BiFunction<T, U, R>接口,它所定义的apply方法接收两个参数:T和U类型的,然后返回一个R类型的对象。

    • 表示一个接收两个参数的函数,返回一个结果。

    • 核心方法:

      java 复制代码
      R apply(T t, U u);
    • 示例:

      java 复制代码
      BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
      System.out.println(add.apply(5, 10)); // 输出 15
  2. UnaryOperator

    • Function<T, T> 的子接口,表示输入和输出类型相同的函数。其实就是Function<T,T>的简写。

    • 示例:

      java 复制代码
      UnaryOperator<Integer> square = x -> x * x;
      System.out.println(square.apply(5)); // 输出 25
  3. BinaryOperator

    • BiFunction<T, T, T> 的子接口,等价于BiFunction<T,T,T>,表示两个相同类型参数的函数,并返回相同类型的结果。

    • 示例:

      java 复制代码
      BinaryOperator<Integer> multiply = (a, b) -> a * b;
      System.out.println(multiply.apply(2, 3)); // 输出 6
  4. IntToDoubleFunction

    • 表示接收一个 int 类型参数并返回一个 double 类型结果的函数。

    • 示例:

      java 复制代码
      IntToDoubleFunction half = x -> x / 2.0;
      System.out.println(half.applyAsDouble(10)); // 输出 5.0
相关推荐
余华余华37 分钟前
题目 3209: 蓝桥杯2024年第十五届省赛真题-好数
java·开发语言
小咖拉眯1 小时前
快速高效求素数|质数的方法—Java(模板)
java·开发语言·数据结构·算法
FLLdsj2 小时前
Scala身份证上的秘密以及Map的遍历
java·开发语言·scala
mo47763 小时前
JS中的类与对象
java·开发语言·javascript
我的钱啊djlfakjsdlfj3 小时前
远程调用 rpc 、 open feign
网络·网络协议·rpc
T.O.P113 小时前
数据结构和算法
java·开发语言·数据结构
重生之绝世牛码3 小时前
Java设计模式 —— 【创建型模式】工厂模式(简单工厂、工厂方法模式、抽象工厂)详解
java·大数据·开发语言·设计模式·工厂方法模式·设计原则·工厂模式
北漂编程小王子4 小时前
maven 工具 clean、compile、package、install、deploy 常用命令使用区别
java·maven·maven常用命令·clean package
binqian4 小时前
【maven】配置下载私有仓库的快照版本
java·maven