从匿名内部类到 Lambda,再到方法引用:结合项目代码梳理 Java 中的行为传递

17 阅读6分钟

前言

  1. 之前在学习匿名内部类,lambda 和方法引用时,对其的理解停留在表面,没有思考它们之间的联系。一周后在回看代码有了更深层次的理解,现在从“行为传递”的角度总结知识点
  2. 初接触 Java,部分表达不当望谅解

1. 匿名内部类是什么

顾名思义,匿名内部类即为没有显示定义取名的类,常用于:

  • 继承父类并且立即创建一个子类对象
  • 实现接口并且立即创建一个实现类对象

下面我以更常见的第二种情况来讲一下匿名内部类的定义方法

public interface Sleep {
    void sleep();
}
Sleep s1 = new Sleep() {
    @Override
    public void sleep() {
        System.out.println("Minji sleeps tight");
    }
};

接口是不能直接实例化的,这时候就需要引入匿名内部类。

👉 更准确地说,这里做了两件事:

  1. 临时定义一个接口的实现类
  2. 创建这个实现类的对象

匿名内部类本质上是一个没有显式命名的类,同时完成了“定义 + 创建”。


2. 匿名内部类与回调

匿名内部类之所以常见,不是因为开发者喜欢定义“临时类”,而是因为许多 API 设计本身就需要调用者传一个行为对象

public class Test {
    public static void main(String[] args) {
        // 注意 startsleep 在执行中的回调
        Sleep s1 = new Sleep() {
            @Override
            public void sleep() {
                System.out.println("Minji sleeps tight");
            }
        };
        startSleep(s1);

        // 可以直接作为参数传入方法
        startSleep(new Sleep() {
            @Override
            public void sleep() {
                System.out.println("Hanni sleeps tight");
            }
        });
    }

    public static void startSleep(Sleep sleep){
        System.out.println("夜深了");
        sleep.sleep();
    }
}

在执行到 sleep.sleep() 时,会调用匿名内部类中定义的具体方法。

这就是典型的回调思想

方法的执行流程先写好,但是某一步具体怎么执行,由调用方传入

👉 匿名内部类的核心价值是:
不是为了少写一个类文件,而是为了临时传入一段“行为”


3. 匿名内部类的典型使用场景

典型使用场景如下:

  • 事件回调
  • 事件监听
  • 线程 Runnable
  • 排序规则

下面主要讲后三种:

1. 事件监听

JButton btn = new JButton("Bell");
panel.add(btn);
btn.addActionListener(e -> System.out.println("knock knock"));

本质上需要传入一个 ActionListener 接口的实现类(这里已经用 lambda 简化)。

传统写法:

static class BellImpl implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("knock knock");
    }
}

线程 Runnable 和排序规则也是同理,本质都是:传入一个行为


4. 从匿名内部类到 Lambda 表达式

虽然匿名内部类已经能解决“临时传递行为”的问题,但语法比较啰嗦。

下面通过排序来说明:

Arrays.sort(members, new Comparator<Member>() {
    @Override
    public int compare(Member o1, Member o2) {
        return o1.getAge() - o2.getAge();
    }
});

实际上真正有意义的只有:

return o1.getAge() - o2.getAge();

前面的 new Comparator@Override 等只是语法“壳”。

👉 为了简化,引入 Lambda:

Arrays.sort(members, (o1, o2) -> o1.getAge() - o2.getAge());

Lambda 让代码更聚焦在“参数 + 行为”。


Lambda 的前提

并不是所有匿名内部类都可以用 lambda 简化,只有函数式接口可以:

@FunctionalInterface
interface Sleep {
    void sleep();
}

👉 即:有且只有一个抽象方法
并且通过 @FunctionalInterface 注解来表示和验证是否是函数式接口


Lambda 的简化规则

  1. 参数类型可以省略
(o1, o2) -> ...
  1. 方法体只有一行时,可以省略 {}
(o1, o2) -> o1.getAge() - o2.getAge()

❗ 注意:此时不能写 return

  1. 多行时必须写 {} 和 return
(o1, o2) -> {
    return o1.getAge() - o2.getAge();
}
  1. 只有一个参数时,括号可省略

5. lambda 的进一步简化:方法引用

当 Lambda 只是调用一个已有方法时:

(o1, o2) -> Member.compareByAge(o1, o2)

可以写成:

Member::compareByAge

1. 静态方法引用

如果某个 Lambda 表达式里只是调用一个静态方法,并且 "→" 前后参数的形式一致,就可以使用静态方法引用。

Arrays.sort(members, Member::compareByAge);

public class Member {
    private String name;
    private int age;
    private String gender;
    private double height;

    public static int compareByAge(Member m1, Member m2){
        return Integer.compare(m1.getAge(), m2.getAge());
    }
}

匿名内部类:

Collections.sort(list, new Comparator<Member>() {
    @Override
    public int compare(Member o1, Member o2) {
        return Member.compareByAge(o1, o2);
    }
});

lambda:

(o1, o2) -> Member.compareByAge(o1, o2)

方法引用:

Member::compareByAge

👉 本质条件不是简单“参数一致”,而是:
函数式接口的方法签名可以匹配该方法

在 Java 里,一个方法的“签名”核心包括:

👉 参数列表 + 返回值类型(在这里也必须匹配)


2. 实例方法引用

原写法:

public int compareByHeight(Member m1, Member member)

👉 这个写法不太合理,因为没有使用 this

更推荐写法:

public int compareByHeight(Member other) {
    return Double.compare(this.height, other.height);
}

使用:

Arrays.sort(members, Member::compareByHeight);

👉 这里的含义是:

  • 第一个参数作为调用者(this)
  • 第二个参数作为方法参数

6、构造器引用

如果 Lambda 只是创建对象:

Function<String, Member> f = name -> new Member(name);

可以简化为:

Function<String, Member> f = Member::new;

👉 本质:参数列表一致 + 返回值是新对象


⭐ 三种写法对比(从匿名内部类到方法引用)

同一个排序逻辑,可以用三种方式实现:

1️⃣ 匿名内部类

Arrays.sort(members, new Comparator<Member>() {
    @Override
    public int compare(Member o1, Member o2) {
        return o1.getAge() - o2.getAge();
    }
});

2️⃣ Lambda 表达式

Arrays.sort(members, (o1, o2) -> o1.getAge() - o2.getAge());

3️⃣ 方法引用(前提:已有比较方法)

Arrays.sort(members, Member::compareByAge);

👉 调用关系本质

这三种写法虽然形式不同,但底层调用是一样的:

Arrays.sort(...)
    ↓
Comparator.compare(o1, o2)
    ↓
具体比较逻辑(年龄比较)

⭐ 核心理解

  • 匿名内部类:手动创建一个 Comparator 对象
  • Lambda:简化 compare 方法的实现
  • 方法引用:直接复用已有方法

👉 本质上,它们都在实现同一件事:

为 Comparator 提供 compare() 的实现(也就是“传递一个行为”)


总结

一句话总结

匿名内部类 → Lambda → 方法引用,本质都是在做一件事:传递行为


对比总结

写法本质特点
匿名内部类创建一个行为对象最灵活但最啰嗦
Lambda简化实现方法可读性好
方法引用直接复用已有方法最简洁但限制最多

  • 匿名内部类:把行为“包成对象”
  • Lambda:把行为“直接写出来”
  • 方法引用:把行为“指向已有方法”

👉 三者只是写法不同,本质统一:

都是把一段行为作为参数传递给其他代码


补充

Lambda 在 JVM 层面是通过 invokedynamic 实现的,并不是简单的匿名内部类语法糖。


结语

如果从“行为传递”这个角度去理解:

  • 排序、线程、回调这些 API 会变得更清晰
  • Lambda 和方法引用也不再是单纯的语法技巧

👉 这也是 Java 8 一个非常重要的变化:

把行为当作数据进行传递