JAVA

Java——函数式接口(java.util.function)

函数式接口是什么


定义:

有且仅有一个抽象方法的接口(不包括默认方法、静态方法以及对Object方法的重写)

大家对函数式接口的认识应该都来自于Java8的Stream API,比如Predicate、Function,借助这些函数式接口,Stream才能写出一个个骚操作:

对于Stream流大家常用的方法有哪些?

看着这些常用方法,入参都是Function相关接口函数

public class StreamTest {

    public static void main(String[] args) {

        List<User> userList = Lists.newArrayList();
        userList.add(new User(1L, "彼得", 18));
        userList.add(new User(2L, "鲍勃", 19));
        userList.add(new User(3L, "威廉

        userList.stream()
                .filter(user -> user.getAge() > 18)
                .map(User::getName)
                .forEach(System.out::println);
    }
}

点进filter方法,你会发现它的参数就是一个函数式接口Predicate:

我们可以从中得到启发:

函数式接口不同于以往的普通接口,它最大的作用其实是为了支持行为参数传递,比如传递Lambda、方法引用、函数式接口对应的实例对象等。

总结一句话,Function函数作为方法入参,核心原因就是把具体业务逻辑交由外层处理。使方法只处理核心逻辑,从而减少业务逻辑对方法的侵入性,使代码更加简洁优雅,易于维护。
例如map(Function<? super T, ? extends R> mapper)方法,就是把集合中单个item对象的处理逻辑交由外部逻辑处理。 map方法内只需获取结果进行核心逻辑处理。

四大基本函数式接口


是 java.util.function 包下最基本的四个函数式接口。

  • Function

Function<T, R> 是 Java 8 中的一个函数式接口,用于表示接受一个输入参数 T,并返回一个结果 R 的函数。Function接口中有一个抽象方法apply,用于定义函数的逻辑。Function接口通常用于将数据进行转换、映射或者执行某种转换操作。

Function 接口的 apply 方法,就是让你传入一个参数,返回一个值。

并且在泛型中体现了 传入 和 返回 的参数类型。

实例1:

import java.util.function.Function;

public class Function_Demo {
    public static void main(String[] args) {
        Function<String,String> function = new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s;
            }
        };
        System.out.println(function.apply("Sky"));

        //lambda 表达式写法:
        function = ((str)->{ return str;});
        System.out.println(function.apply("Song"));
    }
}

打印结果:
Sky
Song

实例2:

public static void main(String[] args) {
    List<Plan> planList  = new ArrayList<>(Collections.emptyList());
    planList.add(new Plan(1L,"SUCCEED"));
    planList.add(new Plan(2L,"FAIL"));

    // map方法入参,需要传入一个Function函数。item -> item.getPlanNo())写法属于
    // Function函数规范。item表示入参,item.getPlanNo()表示返参
    Set<Long> planNoList1 = planList.stream().map(item -> item.getPlanNo()).collect(Collectors.toSet());
    // 简写
    Set<Long> planNoList2 = planList.stream().map(Plan::getPlanNo).collect(Collectors.toSet());
}

static class Plan{
    private Long planNo;
    private String planStatus;

    public Plan(Long planNo, String planStatus){
        this.planNo = planNo;
        this.planStatus = planStatus;
    }

    public Long getPlanNo() {
        return planNo;
    }
    public Plan setPlanNo(Long planNo) {
        this.planNo = planNo;
        return this;
    }
    public String getPlanStatus() {
        return planStatus;
    }
    public Plan setPlanStatus(String planStatus) {
        this.planStatus = planStatus;
        return this;
    }
}
  • Predicate

Predicate 接口的 test 方法就是传入一个参数,返回一个 boolean 值。

实例:

import java.util.function.Predicate;

public class Predicate_demo {
    public static void main(String[] args) {
        Predicate<Integer> predicate =new Predicate<Integer>() {
            @Override
            public boolean test(Integer integer) {
                return 0 != integer;
            }
        };
        System.out.println(predicate.test(0));

        //lambda 表达式写法:
        predicate = (integer -> {return 0 != integer;});
        System.out.println(predicate.test(1));
    }
}

打印结果:
false
true
  • Consumer

Consumer 接口的 accept 方法就是 传入一个参数,但是不返回值。

是的,传进去的值被消费了,顾名思义!!

实例1:

import java.util.function.Consumer;

public class Consumer_Demo {
    public static void main(String[] args) {
        Consumer<String> consumer = new Consumer<String>() {
            @Override
            public void accept(String s) {
                System.out.println("消费:"+s);
            }
        };
        consumer.accept("cake");

        //lambda 表达式写法:
        consumer = (str)->{ System.out.println("消费:"+str); };
        consumer.accept("money");
    }

}
打印结果:
消费:cake
消费:money

实例2:

public static void main(String[] args) {
    List<Plan> planList  = new ArrayList<>();
    planList.add(new Plan(1L,"SUCCEED"));
    planList.add(new Plan(2L,"FAIL"));
    planList.add(new Plan(3L,"SUCCEED"));

    List<Long> planNo = new ArrayList<>();
    planList.stream()
            .filter(plan -> "SUCCEED".equals(plan.getPlanStatus()))
            // forEarch方法入参,需要传入一个Consumer函数。plan -> planNo.add(plan.getPlanNo())符合函数规范。 plan表示入参。
            .forEach(plan -> planNo.add(plan.getPlanNo()));
    System.out.println("count:" + planNo.size());
}
  • Supplier

有了 消费者 自然要有 生产者!

Supplier 接口的 get 方法就是不用往里传参数,就能返回一个值。

实例:

import java.util.function.Supplier;

public class Supplier_Demo {
    public static void main(String[] args) {
        Supplier<String> supplier = new Supplier<String>() {
            @Override
            public String get() {
                return "提供:Sky";
            }
        };
        System.out.println(supplier.get());

        //lambda 表达式写法:
        supplier = ()->{return "提供:Song";};
        System.out.println(supplier.get());
    }
}
打印结果:
提供:Sky
提供:Song
  • BiFunction函数

与Function区别在于,BiFunction允许传入2个参数,返回一个值。

  • BiConsumer函数

与Consumer函数区别在于,BiConsumer函数允许传入2个参数,无返回值。

实际场景Function函数应用


当我了解Stream对Function的函数运用后,我佩服这些源码编写大佬的思路与才华。所以我认为核心是需要学习他们的编码设计思路,在日常编写代码中,真的碰到了类似场景,就可以想起并运用这套思想去设计代码,使得我们代码更加优雅,简洁高效。这也是我们学习源码的初衷。

例如有这样一个场景,我们对接了很多支付渠道,每家对接口参数的加密方式不同。我们可以针对支付渠道设计一个抽象类。在抽象类中定义一个签名方法。具体不同渠道的签名规则以Function函数传入。

抽象类中公共签名方法

/**
 * 排序并签名
 *
 * @param params   要签名参数
 * @param append   待签名字符串追加内容
 * @param signName 签名参数
 * @param signer   签名函数
 * @return 最终签名
 */
protected final String sign(Map<String, String> params,
                            String append,
                            String signName,
                            Function<String, String> signer) {
    String signStr = params.keySet().stream()
            .filter(key -> StringUtils.hasLength(key)
                    && !key.equals(signName)
                    && StringUtils.hasLength(params.get(key)))
            .sorted()
            .map(key -> key + "=" + params.get(key))
            .collect(Collectors.joining("&"));
    signStr += append;

    if (logger.isDebugEnabled()) logger.debug("待签名字符串:{}", signStr);
    return signer.apply(signStr);
}

/**
* md5 大写加密
*/
protected final String md5Upper(String signStr) {
    return md5Origin(signStr).toUpperCase();
}

/**
* md5 加密
*/
protected final String md5Origin(String signStr) {
    return DigestUtils.md5Hex(signStr);
}

具体支付渠道进行签名调用

// 调用抽象类中的sign签名方法
String sign = sign(params, "&key=" + "私钥字符串", "sign"
// 这里不同渠道类中也可自定义加密规则
, this::md5Origin);

// 另外一种写法
String sign2 = sign(params, "&key=" + "私钥字符串", "sign"
, (signStr) -> DigestUtils.md5Hex(signStr));

类似的场景其实有很多,在很多源码使用上,都会把业务逻辑通过函数式入参,交由你来处理。使得源码方法的公用性会更强,代码更加简洁易于维护。