Java | 函数式编程
更新: 2/9/2025 字数: 0 字 时长: 0 分钟
Java 不支持单独定义函数,但可以把静态方法视为独立的函数,把实例方法视为自带
this
参数的函数。函数式编程的一个特点就是,允许把函数本身作为参数传入另一个函数,还允许返回一个函数。
函数式编程将函数作为基本运算单元,函数可作为变量传递,也可作为返回值。
函数式接口
基本概念
- 函数式接口是指仅有一个抽象方法的接口,除了抽象方法外,允许包含多个默认方法或静态方法。
- 可以通过
@FunctionalInterface
注解明确标识接口为函数式接口。 - 若接口满足单一抽象方法的条件,即便不加注解,编译器也会自动识别为函数式接口。
- 函数式接口是 Java 8 引入的概念,用于支持 Lambda 表达式和方法引用。
常见函数式接口
接口 抽象方法 典型应用场景 Runnable
void 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 表达式来实现该接口。
点击查看案例演示
javanew Thread(new Runnable() { @Override public void run() { System.out.println("传统方式"); } }).start();
javanew Thread(() -> System.out.println("Lambda 方式")).start();
- 语法:
使用细节
- 如果参数只有一个,那么可以省去小括号。
- 如果方法体中只有一个返回语句,可以直接省去花括号和
return
关键字。 - 编译器会自动推断参数类型和返回值类型,避免显式声明。
点击查看案例演示
javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 传统匿名内部类写法 Arrays.sort(array, new Comparator<String>() { @Override public int compare(String s1, String s2) { return s1.compareTo(s2); } });
javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 基础 Lambda 表达式(显式参数类型) Arrays.sort(array, (String s1, String s2) -> { return s1.compareTo(s2); });
javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 省略参数类型(编译器自动推断) Arrays.sort(array, (s1, s2) -> { return s1.compareTo(s2); });
javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 省略大括号和 return(仅当方法体为单行返回语句) Arrays.sort(array, (s1, s2) -> s1.compareTo(s2));
javaString[] array = {"Apple", "Orange", "Banana", "Lemon"}; // 方法引用替代 Lambda(参数与调用方法完全匹配) Arrays.sort(array, String::compareTo);
方法引用
基本概念
- 方法引用就是将一个已实现的方法,直接作为函数式接口中抽象方法的实现。当然前提是方法定义得一样才行。
- 方法引用是一种简化 Lambda 表达式的语法糖,允许你通过名称直接引用已有的方法。
- 当 Lambda 表达式仅仅调用一个已存在的方法时,可以直接用方法名替代完整的 Lambda 写法,使代码更简洁、更具可读性。
使用方式
- 如果某个方法的签名与函数式接口的抽象方法签名一致,就可以使用该方法进行引用。
- 方法签名只关心参数类型和返回类型,不考虑方法名或类的继承关系。
方法引用的优势
- 简洁性:比 Lambda 表达式更简洁,减少了冗余代码。
- 可读性:直接通过方法名表达了意图,使代码更易理解。
- 复用性:能够复用已有的方法实现,避免重复编写逻辑。
方法引用的类型
类型 语法 示例 Lambda 等效形式 参数映射规则 静态方法引用 类名::静态方法名
Integer::sum
(a, b) -> Integer.sum(a, b)
参数直接传递 构造方法引用 类名::new
String::new
() -> new String()
参数与构造方法参数匹配 未绑定实例方法引用 类名::实例方法名
String::compareTo
(s1, s2) -> s1.compareTo(s2)
第一个参数为方法调用者 已绑定实例方法引用 对象::实例方法名
System.out::println
s -> System.out.println(s)
参数作为实例方法的参数 各类型的区别
- 静态方法引用:不需要对象,直接通过类名引用静态方法。
- 构造方法引用:构造方法的参数与 Lambda 表达式的参数匹配。
- 未绑定实例方法引用:实例方法需要对象调用,但这里没有明确指定的对象,则编译器会将第一个参数作为方法调用的目标对象。
- 已绑定实例方法引用:直接使用预先存在的对象作为方法调用者,函数式接口的参数直接作为方法参数。
静态方法引用
点击查看案例演示
函数式接口定义:
java@FunctionalInterface public interface Tools { int sum(int a, int b); // 待实现的求和方法 }
使用匿名内部类实现:
javapublic 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 表达式:
javapublic 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; }
此时,我们可以直接将已有方法的实现作为函数式接口的实现:
javapublic class Main { public static void main(String[] args) { Tools tools = (a, b) -> Integer.sum(a, b); // 直接使用 Integer 为我们写好的求和方法 System.out.println(tools.sum(1, 2)); } }
对应的匿名内部类的方式:
javapublic 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
接口中定义的完全一样,所以说我们可以直接使用方法引用:javapublic class Main { public static void main(String[] args) { Tools tools = Integer::sum; System.out.println(tools.sum(1, 2)); } }
构造方法引用
点击查看案例演示
如果要把一个
List<String>
转换为List<Person>
,应该怎么办?javaclass 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
:javaList<String> names = List.of("Bob", "Alice", "Tim"); List<Person> persons = new ArrayList<>(); for (String name : names) { persons.add(new Person(name)); }
要更简单地实现
String
到Person
的转换,我们可以引用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; } }
后面我们会讲到
Stream
的map()
方法。现在我们看到,这里的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
。未绑定实例方法引用
点击查看案例演示
javaimport 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()
的方法定义:javapublic final class String { public int compareTo(String anotherString) { ... } }
这个方法的签名只有一个参数,为什么和
int Comparator<String>.compare(String, String)
能匹配呢?因为实例方法有一个隐含的
this
参数,String
类的compareTo()
方法在实际调用的时候,第一个隐含参数总是传入this
,相当于静态方法:javapublic 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>
接口。javaimport java.util.function.Consumer; public class Main { public static void main(String[] args) { Consumer<String> consumer = System.out::println; consumer.accept("Hello, World!"); // 直接通过方法引用调用实例方法 } }