CodeGym /课程 /JAVA 25 SELF /将函数作为参数传递:示例

将函数作为参数传递:示例

JAVA 25 SELF
第 49 级 , 课程 2
可用

1. 传递行为而不是数据

为什么要把函数作为参数传递?

设想:你有一个用来排序列表的方法。但它怎么知道要按什么规则排序呢?按字母?按长度?按生日?你可以为每种情况都写一个单独的方法,但这很快就会变成疯狂的复制粘贴。

相反,Java 允许你传递行为(也就是函数或 lambda),由它来决定如何排序、过滤、转换等等。这样代码就更灵活、可复用。

示例:使用比较器排序

List<String> names = List.of("Anna", "Boris", "Vika");

// 传递行为:如何比较元素(按字符串长度)
names.stream()
     .sorted((a, b) -> a.length() - b.length())
     .forEach(System.out::println);

示例:使用谓词过滤

List<String> names = List.of("Anna", "Boris", "Vika");

// 传递行为:保留谁(名字长度大于 4)
names.stream()
     .filter(name -> name.length() > 4)
     .forEach(System.out::println);

在这两种情况下,你都没有把逻辑“硬编码”到方法里,而是给了它一小段行为,它会应用到每个元素上。

好处:更少重复,更高灵活性

不必写十几个代码相近但逻辑不同的方法,你只需写一个通用方法,把“该做什么”作为函数传进去。这能节省时间、减少错误,也让代码更易于测试。

2. 传递函数的语法

将 lambda 表达式作为参数

最常见的方式——在调用方法的地方直接写一个带箭头 -> 的 lambda:

list.forEach(item -> System.out.println(item));

甚至还能更短,如果已经有合适的方法——使用方法引用(::):

list.forEach(System.out::println);

方法引用(method references)

如果已经有合适的方法,可以用 :: 传递它的引用:

// 普通方法
public static boolean isLongName(String name) {
    return name.length() > 4;
}

// 传递方法引用
names.stream()
     .filter(MyClass::isLongName)
     .forEach(System.out::println);

这在方法签名与目标函数式接口期望的签名一致时成立(如 Predicate<T>Function<T, R>Comparator<T> 等)。

3. 来自标准库的示例

Collections.sort 与 Comparator

List<String> names = new ArrayList<>(List.of("Anna", "Boris", "Vika"));

// 按名字长度排序
names.sort((a, b) -> a.length() - b.length());
System.out.println(names); // [Vika, Anna, Boris]

Stream.filter 与 Predicate

List<String> names = List.of("Anna", "Boris", "Vika");

// 只保留以 'V' 开头的名字
names.stream()
     .filter(name -> name.startsWith("V"))
     .forEach(System.out::println); // Vika

Stream.map 与 Function

List<String> names = List.of("Anna", "Boris", "Vika");

// 将名字转换为大写
names.stream()
     .map(String::toUpperCase)
     .forEach(System.out::println);

Optional.ifPresent 与 Consumer

Optional<String> opt = Optional.of("你好!");

// 如果有值,就打印出来
opt.ifPresent(s -> System.out.println("在字符串中:" + s));

4. 实战:编写以函数为参数的方法

是时候把知识用起来了!我们来写几个方法,它们接收一个函数作为参数并在内部使用它。

示例 1:处理列表元素的方法(自制 forEach

假设我们有一个用户列表(User)。我们想对每个用户执行某个动作——例如打印名字、发送邮件、发放积分等。不把动作“硬编码”,而是把它作为参数传入——Consumer<User>

import java.util.List;
import java.util.function.Consumer;

class User {
    String name;
    int age;
    User(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

public class UserProcessor {
    // 接收 Consumer<User> 的方法
    public static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) {
            action.accept(user);
        }
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 30),
            new User("Vika", 22)
        );

        // 打印所有用户的姓名
        processUsers(users, user -> System.out.println("姓名:" + user.name));

        // 发放奖励(示例中仅打印)
        processUsers(users, user -> System.out.println(user.name + " 获得了奖励!"));
    }
}

示例 2:过滤元素的方法(Predicate

编写一个方法,只返回满足条件的用户(例如,只要成年人):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Predicate;

public class UserFilter {
    public static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) {
            if (condition.test(user)) {
                result.add(user);
            }
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 仅过滤成年人
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        adults.forEach(user -> System.out.println(user.name)); // Anna, Vika
    }
}

示例 3:转换方法(Function

现在编写一个方法,把用户列表转换为他们的姓名列表(或任何其它转换):

import java.util.List;
import java.util.ArrayList;
import java.util.function.Function;

public class UserMapper {
    public static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) {
            result.add(mapper.apply(user));
        }
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 获取姓名列表
        List<String> names = mapUsers(users, user -> user.name);
        System.out.println(names); // [Anna, Boris, Vika]

        // 获取年龄列表
        List<Integer> ages = mapUsers(users, user -> user.age);
        System.out.println(ages); // [25, 17, 22]
    }
}

示例 4:生成方法(Supplier

有时需要“按需”获取某个值——例如生成随机数、创建对象、获取当前时间。这时适合使用接口 Supplier<T>

import java.util.function.Supplier;

public class ValueGenerator {
    public static int getValue(Supplier<Integer> supplier) {
        return supplier.get();
    }

    public static void main(String[] args) {
        // 获取随机数
        int random = getValue(() -> (int)(Math.random() * 100));
        System.out.println("随机数:" + random);

        // 获取当前时间(毫秒)
        long time = getValue(System::currentTimeMillis);
        System.out.println("时间:" + time);
    }
}

5. 一个完整的小应用:把示例串起来

假设我们要做一个简单的“用户列表”应用。我们已经会:

  • 按条件过滤用户(例如只要成年人);
  • 把用户转换成其他东西(姓名、邮箱、年龄);
  • 对每个用户执行动作(打印、发放积分);
  • 按需生成值(例如创建新用户)。

现在使用这些方法,我们就可以构建灵活的数据处理方案,无需每次为新需求重写代码。

import java.util.*;
import java.util.function.*;

public class UserApp {
    static class User {
        String name;
        int age;
        User(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String toString() {
            return name + " (" + age + ")";
        }
    }

    // 通用的用户处理方法
    static void processUsers(List<User> users, Consumer<User> action) {
        for (User user : users) action.accept(user);
    }

    // 通用过滤器
    static List<User> filterUsers(List<User> users, Predicate<User> condition) {
        List<User> result = new ArrayList<>();
        for (User user : users) if (condition.test(user)) result.add(user);
        return result;
    }

    // 通用转换器
    static <R> List<R> mapUsers(List<User> users, Function<User, R> mapper) {
        List<R> result = new ArrayList<>();
        for (User user : users) result.add(mapper.apply(user));
        return result;
    }

    public static void main(String[] args) {
        List<User> users = List.of(
            new User("Anna", 25),
            new User("Boris", 17),
            new User("Vika", 22)
        );

        // 1. 打印所有用户
        processUsers(users, user -> System.out.println("用户:" + user));

        // 2. 只找出成年人
        List<User> adults = filterUsers(users, user -> user.age >= 18);
        System.out.println("成年人:" + adults);

        // 3. 获取成年人的姓名
        List<String> adultNames = mapUsers(adults, user -> user.name);
        System.out.println("成年人的姓名:" + adultNames);

        // 4. 给成年人发放奖励
        processUsers(adults, user -> System.out.println(user.name + " 获得了奖励!"));
    }
}

为什么这比“传统”做法更好?

这种方式更灵活:同一组方法可以用于完全不同的场景,只需传入不同的函数即可。代码不会膨胀成无穷无尽的“这样过滤”、“那样打印”——我们有一套通用的工具,处处可用。因此代码更紧凑、清晰,也更不容易出错。而且这种风格与现代的 Stream API 完美契合,迁移到流式处理时无需重新学习——思路完全一致。

6. 将函数作为参数传递时的常见错误

错误 1:签名与期望的接口不匹配。
如果方法接收 Predicate<User>,而你传入的 lambda 返回的不是 boolean,而是例如 String,编译器会立刻说“哎呀呀!”并拒绝构建项目。请检查返回值与参数是否与期望一致。

错误 2:lambda 使用了可能会变化的变量。
在 Java 中,lambda 表达式只能使用外部上下文中的 final 或“有效 final(effectively final)”变量。如果你试图在 lambda 中修改这样的变量——会产生编译错误。

错误 3:混淆了接口类型。
有时你想传递的是 Consumer<T>,却写成了有返回值的函数(例如 Function<T, R>)。请确认你的 lambda 返回的正是所需的内容(或什么都不返回——对于 Consumer)。

错误 4:把过于复杂的 lambda 直接写在参数里。
如果一个 lambda 超过一两行,最好把它提取成单独的变量或方法。否则代码会变得难以阅读,最后只有你能看懂(而且未必总能看懂)。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION