Skip to content

Java | 函数式编程

更新: 2/9/2025 字数: 0 字 时长: 0 分钟

Java 不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带 this 参数的函数。

函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。

函数式编程将函数作为基本运算单元,函数可作为变量传递,也可作为返回值。

函数式接口

  • 基本概念

    • 函数式接口是指仅有一个抽象方法的接口,除了抽象方法外,允许包含多个默认方法或静态方法。
    • 可以通过 @FunctionalInterface 注解明确标识接口为函数式接口。
    • 若接口满足单一抽象方法的条件,即便不加注解,编译器也会自动识别为函数式接口。
    • 函数式接口是 Java 8 引入的概念,用于支持 Lambda 表达式和方法引用。
  • 常见函数式接口

    接口抽象方法典型应用场景
    Runnablevoid run()执行线程任务,无返回值
    Comparator<T>int compare(T o1, T o2)对象排序
    Callable<V>V call()带返回值的异步任务
    Consumer<T>void accept(T t)数据消费(如遍历、处理)
    Function<T, R>R apply(T t)数据转换,类型映射

Lambda 表达式

  • 基本概念

    • Lambda 表达式是对匿名内部类的一种简化,用于实现函数式接口。
    • 在 Lambda 表达式出现之前,为了实现一个接口,通常需要定义一个匿名内部类,这会导致生成额外的 .class 文件。
    • 使用 Lambda 表达式,我们可以避免显式编写类定义,简化代码,并且不会生成额外的 .class 文件。
    • Lambda 表达式在 Java 中并不需要显式地通过 class 关键字定义一个类,就能实现函数式接口的功能。
    • Lambda 表达式允许我们不必为接口编写实现类,直接将实现传递给方法,减少代码冗余。
  • 使用方式

    • 语法:(参数) ‐> { 方法体 }
    • 和匿名内部类不同,Lambda 表达式仅支持接口,不支持抽象类。
    • 如果一个方法的参数是函数式接口类型,那么可以直接使用 Lambda 表达式来实现该接口。
    点击查看案例演示
    java
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println("传统方式");
        }
    }).start();
    java
    new Thread(() -> System.out.println("Lambda 方式")).start();
  • 使用细节

    • 如果参数只有一个,那么可以省去小括号。
    • 如果方法体中只有一个返回语句,可以直接省去花括号和 return 关键字。
    • 编译器会自动推断参数类型和返回值类型,避免显式声明。
    点击查看案例演示
    java
    String[] array = {"Apple", "Orange", "Banana", "Lemon"};
    
    // 传统匿名内部类写法
    Arrays.sort(array, new Comparator<String>() {
        @Override
        public int compare(String s1, String s2) {
            return s1.compareTo(s2);
        }
    });
    java
    String[] array = {"Apple", "Orange", "Banana", "Lemon"};
    
    // 基础 Lambda 表达式(显式参数类型)
    Arrays.sort(array, (String s1, String s2) -> {
        return s1.compareTo(s2);
    });
    java
    String[] array = {"Apple", "Orange", "Banana", "Lemon"};
    
    // 省略参数类型(编译器自动推断)
    Arrays.sort(array, (s1, s2) -> {
        return s1.compareTo(s2);
    });
    java
    String[] array = {"Apple", "Orange", "Banana", "Lemon"};
    
    // 省略大括号和 return(仅当方法体为单行返回语句)
    Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
    java
    String[] array = {"Apple", "Orange", "Banana", "Lemon"};
    
    // 方法引用替代 Lambda(参数与调用方法完全匹配)
    Arrays.sort(array, String::compareTo);

方法引用

  • 基本概念

    • 方法引用就是将一个已实现的方法,直接作为函数式接口中抽象方法的实现。当然前提是方法定义得一样才行。
    • 方法引用是一种简化 Lambda 表达式的语法糖,允许你通过名称直接引用已有的方法。
    • 当 Lambda 表达式仅仅调用一个已存在的方法时,可以直接用方法名替代完整的 Lambda 写法,使代码更简洁、更具可读性。
  • 使用方式

    • 如果某个方法的签名与函数式接口的抽象方法签名一致,就可以使用该方法进行引用。
    • 方法签名只关心参数类型和返回类型,不考虑方法名或类的继承关系。
  • 方法引用的优势

    • 简洁性:比 Lambda 表达式更简洁,减少了冗余代码。
    • 可读性:直接通过方法名表达了意图,使代码更易理解。
    • 复用性:能够复用已有的方法实现,避免重复编写逻辑。
  • 方法引用的类型

    类型语法示例Lambda 等效形式参数映射规则
    静态方法引用类名::静态方法名Integer::sum(a, b) -> Integer.sum(a, b)参数直接传递
    构造方法引用类名::newString::new() -> new String()参数与构造方法参数匹配
    未绑定实例方法引用类名::实例方法名String::compareTo(s1, s2) -> s1.compareTo(s2)第一个参数为方法调用者
    已绑定实例方法引用对象::实例方法名System.out::printlns -> System.out.println(s)参数作为实例方法的参数
  • 各类型的区别

    • 静态方法引用:不需要对象,直接通过类名引用静态方法。
    • 构造方法引用:构造方法的参数与 Lambda 表达式的参数匹配。
    • 未绑定实例方法引用:实例方法需要对象调用,但这里没有明确指定的对象,则编译器会将第一个参数作为方法调用的目标对象。
    • 已绑定实例方法引用:直接使用预先存在的对象作为方法调用者,函数式接口的参数直接作为方法参数。
  • 静态方法引用

    点击查看案例演示

    函数式接口定义:

    java
    @FunctionalInterface
    public interface Tools {
        int sum(int a, int b); // 待实现的求和方法
    }

    使用匿名内部类实现:

    java
    public class Main {
        public static void main(String[] args) {
            Tools tools = new Tools() {
                @Override
                public int sum(int a, int b) {
                    return a + b;
                }
            };
            System.out.println(tools.sum(1, 2));
        }
    }

    也可以使用 Lambda 表达式:

    java
    public class Main {
        public static void main(String[] args) {
            Tools tools = (a, b) -> a + b;
            System.out.println(tools.sum(1, 2));
        }
    }

    只不过还能更简单,因为 Integer 类中默认提供了求两个 int 值之和的静态方法:

    java
    // Integer 类中就已经有对应的实现了
    public static int sum(int a, int b) {
        return a + b;
    }

    此时,我们可以直接将已有方法的实现作为函数式接口的实现:

    java
    public class Main {
        public static void main(String[] args) {
            Tools tools = (a, b) -> Integer.sum(a, b); // 直接使用 Integer 为我们写好的求和方法
            System.out.println(tools.sum(1, 2));
        }
    }

    对应的匿名内部类的方式:

    java
    public class Main {
        public static void main(String[] args) {
            Tools tools = new Tools() {
                @Override
                public int sum(int a, int b) {
                    return Integer.sum(a, b); // 直接使用 Integer 为我们写好的求和方法
                }
            };
            System.out.println(tools.sum(1, 2));
        }
    }

    我们发现,Integer.sum 的参数和返回值,跟我们在 Tools 接口中定义的完全一样,所以说我们可以直接使用方法引用:

    java
    public class Main {
        public static void main(String[] args) {
            Tools tools = Integer::sum;
            System.out.println(tools.sum(1, 2));
        }
    }
  • 构造方法引用

    点击查看案例演示

    如果要把一个 List<String> 转换为 List<Person>,应该怎么办?

    java
    class Person {
        String name;
        public Person(String name) {
            this.name = name;
        }
    }
    
    List<String> names = List.of("Bob", "Alice", "Tim");
    List<Person> persons = ???

    传统的做法是先定义一个 ArrayList<Person>,然后用 for 循环填充这个 List

    java
    List<String> names = List.of("Bob", "Alice", "Tim");
    List<Person> persons = new ArrayList<>();
    for (String name : names) {
        persons.add(new Person(name));
    }

    要更简单地实现 StringPerson 的转换,我们可以引用 Person 的构造方法:

    java
    // 引用构造方法
    import java.util.*;
    import java.util.stream.*;
    
    public class Main {
        public static void main(String[] args) {
            List<String> names = List.of("Bob", "Alice", "Tim");
            List<Person> persons = names.stream().map(Person::new).collect(Collectors.toList());
            System.out.println(persons);
        }
    }
    
    class Person {
        String name;
        public Person(String name) {
            this.name = name;
        }
        public String toString() {
            return "Person:" + this.name;
        }
    }

    后面我们会讲到 Streammap() 方法。现在我们看到,这里的 map() 需要传入的 FunctionalInterface 的定义是:

    java
    @FunctionalInterface
    public interface Function<T, R> {
        R apply(T t);
    }

    把泛型对应上就是方法签名 Person apply(String),即传入参数 String,返回类型 Person

    Person 类的构造方法恰好满足这个条件,因为构造方法的参数是 String,而构造方法虽然没有 return 语句,

    但它会隐式地返回 this 实例,类型就是 Person,因此,此处可以引用构造方法。

    构造方法的引用写法是 类名::new,因此,此处传入 Person::new

  • 未绑定实例方法引用

    点击查看案例演示
    java
    import java.util.Arrays;
    
    public class Main {
        public static void main(String[] args) {
            String[] array = {"Apple", "Orange", "Banana", "Lemon"};
            Arrays.sort(array, String::compareTo);
            System.out.println(String.join(", ", array));
        }
    }

    上面的代码可以编译通过,这说明 String.compareTo() 方法符合 Lambda 定义。

    观察 String.compareTo() 的方法定义:

    java
    public final class String {
        public int compareTo(String anotherString) {
            ...
        }
    }

    这个方法的签名只有一个参数,为什么和 int Comparator<String>.compare(String, String) 能匹配呢?

    因为实例方法有一个隐含的 this 参数,String 类的 compareTo() 方法在实际调用的时候,第一个隐含参数总是传入 this,相当于静态方法:

    java
    public static int compareTo(String this, String anotherString);

    所以,String.compareTo() 方法也可作为方法引用传入。

  • 已绑定实例方法引用

    点击查看案例演示

    Consumer<T> 是 Java 内置的函数式接口,定义如下:

    java
    @FunctionalInterface
    public interface Consumer<T> {
        void accept(T t); // 抽象方法:接受一个参数 t,无返回值
    }

    Consumer<String> 的抽象方法 accept(String)System.out.println(String) 的方法签名完全一致,

    因此可以直接通过方法引用 System.out::println 来实现 Consumer<String> 接口。

    java
    import java.util.function.Consumer;
    
    public class Main {
        public static void main(String[] args) {
            Consumer<String> consumer = System.out::println;
            consumer.accept("Hello, World!"); // 直接通过方法引用调用实例方法
        }
    }

贡献者

The avatar of contributor named as wkwbk wkwbk

页面历史