1. 引言
函数式接口是只包含一个抽象方法(即没有实现的方法)的接口。只有这样的接口才可以用更简洁的方式来表示方法实现——在 Java 中通过 lambda 表达式实现(我们稍后会学习)。
为什么只能有一个方法?
因为函数式接口只描述一个操作。如果方法有两个或更多,就不清楚到底该实现哪个方法了。所以规则很简单:一个接口——一个抽象方法。
标准库中的示例
- Runnable — 用于线程中的任务(void run())
- Callable<V> — 返回结果的任务(V call())
- Comparator<T> — 对象比较(int compare(T o1, T o2))
- Consumer<T> — “消费者”(void accept(T t))
- Supplier<T> — “供应者”(T get())
- Function<T, R> — 从 T 到 R 的函数(R apply(T t))
- Predicate<T> — 条件判断(boolean test(T t))
例如,接口 Runnable:
public interface Runnable {
void run();
}
再看接口 Comparator:
public interface Comparator<T> {
int compare(T o1, T o2);
// ... 还可以有 default 和 static 方法,但抽象方法只有一个!
}
要点:default 和 static 方法不算作抽象方法,因此它们可以有任意多个!
2. 注解 @FunctionalInterface
Java 很严格、很讲原则。为避免混淆,它允许用注解 @FunctionalInterface 将接口显式标记为函数式接口。这就像贴上一张“只带一个按钮!”的标签,防止随意添加多余内容。
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
现在,如果你一时忘了又添加了第二个抽象方法,编译器会立刻报错:
@FunctionalInterface
public interface Oops {
void doIt();
void doSomethingElse(); // 错误!有两个抽象方法
}
注解是必需的吗?
不是,非必需。如果接口恰好只包含一个抽象方法,它即使没有该注解也仍是函数式接口。但加上注解可以明确你的意图,并保护你免于不小心犯错。
能添加 default 和 static 方法吗?
可以!关键是必须只有一个抽象方法。其他方法都可以是 default 或 static,数量不限。
示例:
@FunctionalInterface
public interface FancyOperation {
int apply(int a, int b);
default void printInfo() {
System.out.println("我是一个 fancy 操作!");
}
static void description() {
System.out.println("用于算术的函数式接口。");
}
}
3. 声明与使用示例
假设你想描述一个对两个数字进行运算的操作,可以这样做:
@FunctionalInterface
public interface Operation {
int apply(int a, int b);
}
现在可以用不同的方式实现该接口。
通过普通类实现
public class SumOperation implements Operation {
@Override
public int apply(int a, int b) {
return a + b;
}
}
用法:
Operation sum = new SumOperation();
System.out.println(sum.apply(2, 3)); // 5
通过匿名类实现
Operation multiply = new Operation() {
@Override
public int apply(int a, int b) {
return a * b;
}
};
System.out.println(multiply.apply(2, 3)); // 6
关于 lambda 的说明
从 Java 8 开始,这类接口可以用更简洁的 lambda 表达式来实现——语法更短。我们会在后续几讲学习 lambda,目前只需知道:函数式接口正是为这种便捷用法而设计的。
4. 实践:编写你自己的函数式接口
任务 1:实现你的 Action!
创建接口 Action,接收一个字符串且不返回任何值。通过匿名类实现它,使其将字符串以大写形式打印。
@FunctionalInterface
interface Action {
void act(String s);
}
public class ActionDemo {
public static void main(String[] args) {
Action shout = new Action() {
@Override
public void act(String text) {
System.out.println(text.toUpperCase());
}
};
shout.act("我在学 java!"); // 我在学 JAVA!
}
}
(稍后我们会看到,使用 lambda 表达式可以写得更短。)
任务 2:数字过滤器
创建接口 NumberPredicate,其方法为 boolean test(int n)。使用匿名类实现偶数性检查。
@FunctionalInterface
interface NumberPredicate {
boolean test(int n);
}
public class PredicateDemo {
public static void main(String[] args) {
NumberPredicate isEven = new NumberPredicate() {
@Override
public boolean test(int n) {
return n % 2 == 0;
}
};
System.out.println(isEven.test(4)); // true
System.out.println(isEven.test(7)); // false
}
}
任务 3:使用标准接口
可以直接使用现成的 Predicate<Integer>:
import java.util.function.Predicate;
Predicate<Integer> isPositive = new Predicate<Integer>() {
@Override
public boolean test(Integer x) {
return x > 0;
}
};
System.out.println(isPositive.test(10)); // true
System.out.println(isPositive.test(-5)); // false
表格:标准库中的函数式接口
| 接口 | 方法 | 描述 | 使用示例 |
|---|---|---|---|
|
|
无参数、无返回值的任务 | 线程、计时器 |
|
|
带返回值的任务 | 线程、ExecutorService |
|
|
比较两个对象 | 集合排序 |
|
|
“消费者” | 遍历集合 |
|
|
“供应者” | 延迟初始化、数据生成 |
|
|
从 T 到 R 的函数 | 数据转换 |
|
|
条件判断 | 集合过滤 |
5. 使用函数式接口时的常见错误
错误 1:添加了第二个抽象方法。 如果接口中有多个抽象方法,它就不再是函数式接口。编译器(尤其是在使用 @FunctionalInterface 时)会立即报告错误。
错误 2:忘了 default 和 static 方法不算作抽象方法。 可以放心地将它们添加到函数式接口中——这不会违反“只允许一个抽象方法”的规则。
错误 3:方法签名实现不正确。 例如,接口要求两个参数,而你只实现了一个参数的方法。务必检查方法签名。
错误 4:未使用 @FunctionalInterface,结果把接口改坏了。 如果不加注解,可能会不小心添加多余的方法——之后还要费力排查为什么代码不工作。最好总是加上注解,以明确意图。
GO TO FULL VERSION