Skip to content

深入探索-函数式接口的应用与个性化定制之道

在Java编程语言中,函数式接口是一个非常重要的概念,它简化了编程模型的复杂性,并促进了代码的可读性和重用性。函数式接口的核心特性是它们只有一个抽象方法,这使得它们可以被隐式地转换为lambda表达式或方法引用,从而轻松实现函数式编程范式。

本文将深入探讨Java中函数式接口的概念、特点以及它们在实际编程中的应用,并介绍在大麦项目如何自定义函数式接口并介绍以及和设计模式之间的关系,帮助小伙伴更好地理解和运用这一强大的编程工具

四种函数式接口

Function 接口(转换功能)

java
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

}

T类型作为入参类型,R类型作为出参类型

示例

java
public static void testFunction(){
    Function<Integer,String> function = str -> "转换后:"+str;
    System.out.println(function.apply(1));
}

执行结果

plain
转换后:1

Consumer 接口(消费功能)

java
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
}

只有T类型的入参,没有出参,也就是要消费这个参数

示例

java
public static void testConsumer(){
    Consumer<String> consumer = str -> System.out.println("输出:"+str);
    consumer.accept("这是我");
}

执行结果

java
输出:这是我

Supplier 接口(提供功能)

java
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

只有T类型的出参

示例

java
public static void testSupplier(){
    Supplier<String> supplier = () -> "提供数据";
    System.out.println(supplier.get());
}

执行结果

java
提供数据

Predicate 接口(断言功能)

java
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);
  
}

T类型作为入参类型,boolean类型作为出参类型

示例

java
public static void testPredicate(){
    Predicate<Integer> predicate = (number) -> 1 == number;
    System.out.println("是否等于1:"+predicate.test(2));
}

执行结果

plain
是否等于1:false

而stream流的所有操作都是围绕着这四种类型的函数式接口而展开的,这里举几个例子

java
public static void main(String[] args) {
    List<TestUser> testUserList = getTestUserList();
    List<String> testUserNameList = testUserList.stream().map(TestUser::getName).collect(Collectors.toList());
    testUserNameList.forEach((userName) -> System.out.println("用户名为:" + userName));
}

这里是将泛型为 TestUser 对象的集合先转换为stream流,然后通过map方法进一步转换为 TestUser中的name属性为元素的集合

这里看一下stream提供的map方法源码是什么样子

java
<R> Stream<R> map(Function<? super T, ? extends R> mapper);

可以看到map方法的入参就是 Function接口,实现了集合元素中类型的转换

除了stream流用到了转换,这里在集合的循环forEach方法中也是使用了函数式接口,看一下的源码

java
default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

很明显就是循环中利用了Consumer接口来进行执行消费类的动作

经过上述的介绍我们知道了函数式接口的作用,除了stream流的应用外,我们自己完全也可以来利用起来,接下来就是介绍大麦项目中是如何利用这些接口的

Supplier接口的使用-Redis封装组件中get方法

当使用redis时,最常用的流程是从redis查询数据,如果查询不到则从数据库或者服务调用中得到,再放入redis中,而为了减少代码量,使其更优雅,可以使用函数式接口让使用起来更加的简单

java
public <T> T get(RedisKeyBuild redisKeyBuild, Class<T> clazz, Supplier<T> supplier, long ttl, TimeUnit timeUnit) {
    T t = get(redisKeyBuild, clazz);
    if (CacheUtil.isEmpty(t)) {
        t = supplier.get();
        if (CacheUtil.isEmpty(t)) {
            return null;
        }
        set(redisKeyBuild,t,ttl,timeUnit);
    }
    return t;
}

这里使用了Supplier函数式接口,当redis查询不到的话,直接执行java.util.function.Supplier#get 方法,然后将返回的结果放入redis中

在节目详情流程中就使用到了此方法,我们来看下

java
public ProgramVo getById(Long programId) {
    return redisCache.get(RedisKeyBuild.createRedisKey(RedisKeyManage.PROGRAM,programId)
            ,ProgramVo.class,
            () -> createProgramVo(programId)
            ,EXPIRE_TIME, 
            TimeUnit.DAYS);
}
java
private ProgramVo createProgramVo(long programId){
    ProgramVo programVo = new ProgramVo();
    Program program = 
            Optional.ofNullable(programMapper.selectById(programId))
                    .orElseThrow(() -> new DaMaiFrameException(BaseCode.PROGRAM_NOT_EXIST));
    BeanUtil.copyProperties(program,programVo);
    AreaGetDto areaGetDto = new AreaGetDto();
    areaGetDto.setId(program.getAreaId());
    ApiResponse<AreaVo> areaResponse = baseDataClient.getById(areaGetDto);
    if (Objects.equals(areaResponse.getCode(), ApiResponse.ok().getCode())) {
        if (Objects.nonNull(areaResponse.getData())) {
            programVo.setAreaName(areaResponse.getData().getName());
        }
    }else {
        log.error("base-data rpc getById error areaResponse:{}", JSON.toJSONString(areaResponse));
    }
    return programVo;
}

可以看到代码看起来非常的间接,层次分明,一下子就能看到各自的部分都是做了什么事情

而有些情况下上述的4种函数类型的接口并不能满足业务要求,那么就需要自定义函数式接口

自定义函数式接口

比如说我在要方法里执行某个任务,接着后续再用到这个任务,而这个任务就可以用接口来承载,像线程池的方法就是这种形式

java
public void execute(Runnable command)

这里需要传入异步任务,而任务就需要在Runnable接口中实现

java
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

而在大麦项目中,也用到了这种方式来自定义函数式接口

分布式锁组件

在大麦项目中,分布式锁的组件提供了多种操作形式,有自定义注解+aop、方法级别加锁、方法命令式加锁,而方法命令是加锁就是自定义函数接口

java
/**
 * 没有返回值的加锁执行
 * @param taskRun 要执行的任务
 * @param name 锁的业务名
 * @param keys 锁的标识
 *
 * */
public void execute(TaskRun taskRun,String name,String [] keys)
java
@FunctionalInterface
public interface TaskRun {
    
    /**
     * 执行任务
     * */
    void run();
}
java
/**
 * 有返回值的加锁执行
 * @param taskCall 要执行的任务
 * @param name 锁的业务名
 * @param keys 锁的标识
 * @return 要执行的任务的返回值
 * */
public <T> T submit(TaskCall<T> taskCall,String name,String [] keys)
java
@FunctionalInterface
public interface TaskCall<V> {

    /**
     * 执行任务
     * @return 结果
     * */
    V call();
}

当使用时,直接将要加锁的业务放到TaskRun或者TaskCall接口中,借助lamba表达式,使用起来简单清晰

java
serviceLockTool.execute(() -> {
    //加锁逻辑
    orderMapper.update(id);
},"修改订单",orderId);

其实这种实现的形式就是设计模式中的命令模式

命令模式的概念:

命令模式是对命令的封装。命令模式把发出命令的责任和执行命令的责任分割开,委派给不同的对象

每一个命令都是一个操作,请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的

更新: 2025-10-13 12:02:17
原文: https://www.yuque.com/u22210564/ykdrdh/gz76d7mt1w6phgf2

Java 后端面试知识库