首页 / Java 入门教程 / 函数式接口与 Lambda 表达式

Java 入门教程

函数式接口与 Lambda 表达式

本教程共 100 篇 · 第 47 篇 · 更新于 2026-08-05 · 约 5 分钟阅读

JavaJava 入门教程Lambda函数式接口FunctionalInterfaceJava8

本节目标:搞懂什么是「函数式接口」,学会用 (参数) -> 表达式 的 Lambda 语法写匿名行为,摆脱又厚又啰嗦的匿名内部类。

一个让人头大的写法

假设我们要让一段代码「在另一个线程里运行」。以前得这么写:

Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("干活了");
    }
};

为了表达「运行时要执行什么」这一个动作,却套了 new Runnable() { ... }@Overridepublic void run 一大堆样板代码。真正有用的只有中间那一行打印。

Java 8 的 Lambda 表达式就是来消灭这种啰嗦的。

函数式接口:Lambda 的「类型」

Lambda 不是凭空出现的,它必须「贴」在一个特定接口上。这个接口要满足一个条件:只有一个抽象方法。满足这个条件的接口叫函数式接口

Runnable 就是典型的函数式接口——它只有一个 void run() 抽象方法。正因如此,上面那段代码用 Lambda 可以写成:

Runnable r = () -> System.out.println("干活了");

一个箭头 ->,左边是参数(这里 run() 没有参数,写空括号 ()),右边是要执行的代码。那一堆样板全没了。

Lambda 的基本语法

完整形态是:(参数列表) -> { 语句; },但很多情况下能再简化。

1. 没有参数:

() -> System.out.println("hi")

2. 一个参数(括号可省):

s -> System.out.println(s)
// 等价于 (s) -> System.out.println(s)

3. 多个参数:

(a, b) -> a + b

4. 多行语句(用大括号 + return):

(a, b) -> {
    int sum = a + b;
    return sum;
}
Note

如果箭头右边只有「一个表达式」,Java 会自动把它当作返回值,不用写 return 和大括号。一旦用了 { },就必须自己写 return

类型推断:参数为什么不用写类型

你可能疑惑:Lambda 里 ab 没写 int,编译器怎么知道类型?

答案是上下文推断。因为等号右边要赋给一个函数式接口(比如 Comparator<Integer>),编译器从接口的抽象方法签名里反推出参数该是什么类型,所以你不必重复声明。

Comparator<Integer> cmp = (a, b) -> a - b;

这里 ab 被推断为 Integer。如果脱离上下文、编译器无法推断,Lambda 就得补全类型,否则报错。

@FunctionalInterface 注解

你可以给「只有一个抽象方法」的接口加上 @FunctionalInterface 注解:

@FunctionalInterface
interface Calculator {
    int calc(int a, int b);
}

它的作用不是「让接口变成函数式接口」——只要只有一个抽象方法,它就是了。这个注解是给编译器看的:强制检查这个接口确实只含一个抽象方法,哪天你手滑加了第二个抽象方法,编译器立刻报错。

Tip

自己定义函数式接口时,建议都加上 @FunctionalInterface。它相当于一份「契约声明」,既防误操作,也告诉读代码的人:「这个接口就是给 Lambda 用的」。

用自定义函数式接口写 Lambda

@FunctionalInterface
interface Calculator {
    int calc(int a, int b);
}

public class Main {
    public static void main(String[] args) {
        Calculator add = (a, b) -> a + b;
        Calculator mul = (a, b) -> a * b;

        System.out.println(add.calc(3, 4));   // 7
        System.out.println(mul.calc(3, 4));   // 12
    }
}

这里 addmul 都是 Calculator 接口的实例,但我们没写 new,没写匿名类,只用 Lambda 把「具体算法」塞了进去。函数式接口把「算法」当成了可以传递的值——这就是函数式编程味道的来源。

完整可运行示例(Runnable + 自定义)

@FunctionalInterface
interface Greeter {
    void greet(String name);
}

public class Main {
    public static void main(String[] args) {
        Greeter g = name -> System.out.println("你好," + name);
        g.greet("小明");
        g.greet("小红");
    }
}

运行输出:

你好,小明
你好,小红

Lambda 能替掉的经典场景

除了 Runnable,Java 标准库里大量接口都是函数式接口,配合 Lambda 立刻变清爽:

  • Comparator<T>:排序比较器,(a, b) -> ... 直接写比较逻辑。
  • Runnable:线程要执行的任务。
  • Callable<T>:带返回值的任务。
  • FileFilterPredicate<T>:条件判断,x -> x > 0
Warning

Lambda 内部访问外部变量时,那个变量必须是**实质 final(effectively final)**的——也就是声明后没被重新赋值。你不能在一个会变的局部变量上让 Lambda 去读写它,否则编译报错。这点和 final 的规矩一致。

为什么非得是「函数式接口」

你可能会想:Lambda 明明就是一段代码,为什么一定得「贴」在某个接口上?因为 Java 是静态类型语言,任何表达式都得有类型。Lambda 自己没有独立类型,编译器需要靠「目标接口」来推断它长什么样(几个参数、什么返回)。函数式接口就提供了这个「类型锚点」。

标准库已经把常用的函数式接口都备齐了,全在 java.util.function 包里,拿来直接用就行。

四大内置函数式接口速查

这个包里有几十个接口,但天天要用的就四个。把这张表记牢,后面学 Stream 会顺很多:

接口抽象方法干什么Lambda 示例
Function<T,R>R apply(T t)收一个值,变成另一个值s -> s.length()
Consumer<T>void accept(T t)收一个值,只消费不返回s -> System.out.println(s)
Supplier<T>T get()不收参数,凭空产出一个值() -> "hello"
Predicate<T>boolean test(T t)收一个值,返回真假n -> n > 0

口诀好记:有进有出是 Function,只进不出是 Consumer,只出不进是 Supplier,出的是布尔值就是 Predicate

Function<String, Integer> len   = s -> s.length();
Consumer<String>          show  = s -> System.out.println(s);
Supplier<String>          gen   = () -> "凭空造一个字符串";
Predicate<String>         empty = s -> s.isEmpty();

System.out.println(len.apply("hello"));   // 5
show.accept(gen.get());                   // 凭空造一个字符串
System.out.println(empty.test(""));       // true
Tip

写工具方法时,优先用这些标准函数式接口当参数,而不是自己重复定义一堆 @FunctionalInterface,这样别人读起来更顺,也更通用。

方法引用:Lambda 的再简写

如果 Lambda 体里只是「原样调用一个已有方法」,可以用双冒号 :: 再简写一层。这叫方法引用

// Lambda 写法
Consumer<String> c1 = s -> System.out.println(s);
// 方法引用写法,两者完全等价
Consumer<String> c2 = System.out::println;

方法引用一共四种形式:

形式语法例子
静态方法类名::静态方法Integer::parseInt
特定对象的实例方法对象::实例方法System.out::println
任意对象的实例方法类名::实例方法String::length
构造方法类名::newArrayList::new

第三种最容易绕晕:String::length 等价于 s -> s.length(),Lambda 的第一个参数被当成了方法的调用者。

Tip

别为了炫技强行用方法引用。只有当 Lambda 体恰好是「一个方法调用,不多不少」时才换成 ::;一旦要加参数、加判断,老老实实写 Lambda 更好读。

小结

  • 函数式接口 = 只有一个抽象方法的接口;Lambda 表达式必须贴在函数式接口上使用。
  • Lambda 语法:(参数) -> 表达式/语句块,能通过上下文自动推断参数类型。
  • @FunctionalInterface 让编译器帮你守住「只有一个抽象方法」的约束。
  • Lambda 把「行为」变成可传递的值,消灭了大量匿名内部类样板代码。
  • Lambda 内只能访问实质 final 的外部局部变量。
  • 四大内置接口:Function(有进有出)、Consumer(只进不出)、Supplier(只出不进)、Predicate(出布尔值)。
  • Lambda 体只是一次方法调用时,可以用 :: 简写成方法引用,共四种形式。

Lambda 是后续集合 Stream 流式处理、以及很多现代 Java 写法的基石。下一章我们看密封类,它是另一种「约束继承关系」的现代特性。