0%

前言

该文章主要介绍JDK中各种常见的函数式接口,并会附上一些个人对函数式编程的一些扩展思考与实际用法。

常见的函数式接口介绍

jdk1.8的函数式接口都在rt.jar中java.util.function包下,以下以jdk集合类与个人常用的接口进行介绍:

  • Function<T,R>:传入类型为T的对象并执行含返回值(返回值为R-return类型)的指定方法,方法可临时实现。常见于类Optional{map();flatMap();}、Stream{map();flatMap();}、Comparator{thenComparing();}等,MybatisPlus 3.0版本之后的SFunction接口与该接口作用相同,区别在于添加了序列化,使开发者可通过传入getter Function匹配对应字段而无需再写字段名进行匹配,免除字段名写错的问题。

  • BiFunction<T,U,R>:传入类型为T、U类型(T、U可以相同)的两个对象并执行含返回值的指定方法,方法可临时实现。常见于类Stream{reduce();}、Map{replaceAll();computeIfPresent();compute();merge();}等。

  • Consumer<T>:传入单个对象并执行对象中无返回值的指定方法,方法可临时实现。常见于类List{foreach();}、Stream{foreach();}、Optional{ifPresent();}等。

  • BiConsumer<T, U>:传入两个对象并执行对象中无返回值的指定方法,方法可临时实现。常见于类Stream{collect();}、Map{foreach();}等。

  • Supplier<T>:供应商接口,可理解为对象的无参构造函数代理接口,每次调用其get()方法都会产生一个新的对象。常见于类Stream{generate();collect();}Objects{requireNonNull();}、ThreadLocal{withInitial();}

  • Predicate<T>:传入一个对象返回其指定行为方法执行结果布尔值,方法可临时实现。常见于类Optional{filter();}、Stream{filter();anyMatch();allMatch();noneMatch();}、ArrayList{removeIf();}

  • BiPredicate<T, U>:可根据前面的Bi接口与Predicate推断,不再多作阐述

常见的函数式接口用法

Stream中的函数式编程

以下先以一段代码简单的介绍jdk中的函数式用法:

List<String> list = Lists.newArrayList("a", "b", "c", "d", "e");
String result = list.stream()
                .filter(str -> !StringUtils.equals(str, "c"))   // ① 参数为Predicate<? super String>,返回值为Stream<String>
                .map(str -> str + ",")   // ② 参数为Function<? super String, ? extends String>,返回值为Stream<String>
                .reduce((current, next) -> current + next) // ③ 参数为BinaryOperator<String>,返回值为Optional<String>
                .orElse("");

List转为Stream后Stream中的泛型都会对应为List元素的类型,以下为上面几个stream对象方法的简单讲解:
①:实现了一个Predicate<String>接口,并让Stream对象调用该接口的实现操作去过滤获取列表中元素值不为"c"的元素
②: 实现了一个Function<String,String>接口,在每个元素末尾添加字符串”,”,并返回添加后的结果
③: 实现了一个BinaryOperator<String>接口,将stream的当前元素与下一个元素进行拼接并返回拼接结果。BinaryOperator<T>BiFunction<T,U,R>的子接口,在2个参数类型与返回类型都相同的情况下可使用BinaryOperator接口替代BiFunction接口,但两个接口实质上都需要实现apply()方法进行操作并返回结果,并无太大区别,可把BinaryOperator成BiFunction的一个子集,其定义如下:

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
    ......
}

单看以上代码可能还无法体现出为什么叫函数式编程的原因,现在把以上代码还原为为函数实现显示样式:

String result = list.stream()
        .filter(new Predicate<String>() {
            @Override
            public boolean test(String s) {
                return !StringUtils.equals(s, "c");
            }
        })
        .map(new Function<String, String>() {
            @Override
            public String apply(String s) {
                return s + ",";
            }
        })
        .reduce(new BinaryOperator<String>() {
            @Override
            public String apply(String current, String next) {
                return current + next;
            }
        })
        .orElse("");

两段的执行代码都可编译执行,对比可知第一段代码只是对第二段代码的简化,第二段代码中详细的显示了对列表转stream后的操作实现了哪些接口与实现的函数操作,显得十分臃肿,而第一段代码只显示了实现的函数操作,故个人认为将重点放在函数实现操作便是函数式编程的核心。
相信各个读者都发现了所有函数式接口所需实现的函数都有且仅有一个,个人认为目的除了更优雅的显示以外,还可以让程序知道即使我传入的是一个函数式接口实现类,程序依然会清楚它还要再去执行该类型的指定函数。

List中的函数式编程
List中含函数式接口参数的方法主要为foreach(Consumer),遍历元素时将元素作为参数传入Consumer执行,最简单的例子为list.forEach(System.out::println);,调用System.out对象的println方法打印遍历的当前元素。

Map中的函数式编程
Map中个人常用的含函数式接口参数的方法主要为foreach(BiConsumer<? super K, ? super V>)compute(K, BiFunction<? super K, ? super V, ? extends V>),其余的相信大家可以触类旁及。foreach为遍历当前map中的元素,前面介绍BiConsumer需要传入两个参数,而map.foreach()执行时每个key、value则作为参数传入到BiConsumer。虽然说需要传两个参数给BiConsumer,但不代表每个参数都必须用到,如下例中的BiConsumer只对每个val参数列表添加“z”字符串而没有用到key参数:

Map<String, List<String>> map = new HashMap<>();
map.put("0", Lists.newArrayList(""));
map.put("1", Lists.newArrayList("a"));
map.put("2", Lists.newArrayList("a", "b"));
map.put("3", Lists.newArrayList("a", "b", "c"));
map.forEach((k,v) -> v.add("z")); // ① 每个val列表末尾添加z字符串

如果觉得有点难理解的可看以下函数还原代码:

map.forEach(new BiConsumer<String, List<String>>() {
   @Override
    public void accept(String key, List<String> list) {
        list.add("z");
     }
});

Map的compute()方法根据名称大家也可以估到该方法是进行某些计算后再去设计key的值,可用于Map中指定key的值计算,在实际开发中个人常用于该情况:map的val为列表,map需要为指定key的val添加元素,添加前需判断val列表是否为空,为空则初始化后再添加,不为空则直接添加。

map.compute("4",(key, list) -> list == null ? Lists.newArrayList("a", "b", "c", "d") : ListUtils.add(list,"z"));

以上代码判断map中key为4的列表是否为空,若为空则将map中key为4的val设为元素为"a", "b", "c", "d"的列表,不为空则在原val列表中添加字符串"z"。其中ListUtils为自定义工具类,其add方法返回参数列表,便于一行代码实现目的,实现如下:

public static <T> List<T> add(List<T> list, T t) {
    list.add(t);
    return list;
}

看了map.compute()的都知道该函数可以替代在操作map一些情况下的if判断,若把上面的compute()方法使用if执行,则将变成以下代码块:

if(map.containsKey("4")){
    map.get("4").add("z");
}else {
    map.put("4",Lists.newArrayList("a", "b", "c", "d"));
}

可以看出适当的使用函数式编程可以为我们减少代码行。

Optional简化if
JDK1.8新增了Optional类使开发者可以减少if的语句块,类也含不少参数为函数式接口的方法,以下以一个简单的代码块进行介绍:

Classify classify = new Classify();
Optional.ofNullable(classify)
        .map(Classify::getName)
        .orElse("null");

上例中把classify对象交给Optional代理,如果classify对象为空或classify对象中的name属性为空则返回字符串“null”,其中map的参数为Function。

看到这相信大家都了解到JDK中的函数式方法都是异曲同工,区别只在于在实际使用时泛型对应的实际类型。

个人扩展用法

前面基本都是谈个人对函数式的认知与JDK原生类函数式参数方法的用法,而此处开始,是时候展现真正的技术了[doge]。函数式接口运用得当可以省略不少,下文将以几个个人实际开发中思考或使用过的例子进行函数式使用的思维拓展。

分类例子实体Classify:

@Data
@Accessors(chain = true)
public class Classify {
    private Long id;
    private String name;
    private Integer level;
    private Long parentId;
    private transient List<Classify> sonClassifies;
}
  • 自定义ListUtils替代Stream的简单操作

现有一个List<Classify>的列表对象,现在需要将列表中所有分类的名字重新提取为一个列表,了解Stream会这样写:

List<String> names = list.stream()
        .map(Classify::getName)
        .collect(Collectors.toList());

又有一个需求需要将列表元素转化成key为id,value为name的映射,这时会写成如下:

Map<Long,String> idNameMap =   list.stream()
        .collect(Collectors.toMap(Classify::getId, Classify::getName));

又又有一个需求需要将所有分类转换成key为parentId,value为子分类元素列表的映射,这时会写成如下:

Map<Long, List<Classify>> parentSonsMap = list.stream()
        .collect(Collectors.groupingBy(Classify::getParentId));

以上写法都是比较普通的写法,应该任何人都可以接受,但我想这么简单的操作可不可以一行解决呢?也有部分开发者认为把所有stream方法调用放到同一行就可以了,但对我而言这会影响代码的可读性(虽然影响可能不大)。在开发者以上List转换的状况虽然不多,但也不算少,为了可一行代码取代Stream的简单操作,个人撸了一个List工具类放到了自己的通用框架中,通过Function作为参数取代Stream的简单操作,完整如下:

public class ListUtils {
    private ListUtils() {
    }

    public static <T> List<T> add(List<T> list, T t) {
        list.add(t);
        return list;
    }

    public static boolean isEmpty(Collection collection) {
        return collection == null || collection.isEmpty();
    }

    public static boolean isNotEmpty(Collection collection) {
        return !isEmpty(collection);
    }

    public static <T> ArrayList<T> newArrayList(T... elements) {
        ArrayList<T> list = new ArrayList<>(elements.length + elements.length >> 1 + 5);
        Collections.addAll(list, elements);
        return list;
    }

    /**
     * 条件为true时才添加元素
     *
     * @param condition  条件
     * @param collection 集合
     * @param val
     * @return 添加结果
     */
    public static <T> boolean addIf(boolean condition, Collection<T> collection, T val) {
        return condition && collection.add(val);
    }

    /**
     * 从对象列表中提取对象属性
     *
     * @param list      对象列表
     * @param valGetter 对象属性get方法
     * @param <T>       对象
     * @param <V>       对象属性
     * @return 对象属性列表
     */
    public static <T, V> List<V> collectToList(Collection<T> list, Function<T, V> valGetter) {
        List<V> properties = new ArrayList<>(list.size());
        list.forEach(e -> properties.add(valGetter.apply(e)));
        return properties;
    }

    /**
     * 从对象列表中提取指定属性为key,当前对象为value转为map
     *
     * @param list
     * @param keyGetter
     * @param <T>
     * @param <K>
     * @return
     */
    public static <T, K> Map<K, T> collectToMap(Collection<T> list, Function<T, K> keyGetter) {
        Map<K, T> propertiesMap = new HashMap<>(list.size());
        list.forEach(e -> propertiesMap.put(keyGetter.apply(e), e));
        return propertiesMap;
    }

    /**
     * 从对象列表中提取指定属性T为key,属性V为value转为map
     *
     * @param list      对象列表
     * @param keyGetter
     * @param valGetter
     * @param <T>
     * @param <K>
     * @param <V>
     * @return
     */
    public static <T, K, V> Map<K, V> collectToMap(Collection<T> list, Function<T, K> keyGetter, Function<T, V> valGetter) {
        Map<K, V> propertiesMap = new HashMap<>(list.size());
        list.forEach(e -> propertiesMap.put(keyGetter.apply(e), valGetter.apply(e)));
        return propertiesMap;
    }

    /**
     * 根据列表对象中的某属性值为key划分列表,value为key的属性值相同的对象列表,
     * 功能同stream().collect(Collectors.groupingBy())
     *
     * @param list
     * @param keyGetter
     * @param <T>
     * @param <K>
     * @return
     */
    public static <T, K> Map<K, List<T>> groupToMap(Collection<T> list, Function<T, K> keyGetter) {
        Map<K, List<T>> propertiesMap = new HashMap<>(list.size());
        for (T each : list) {
            propertiesMap.compute(keyGetter.apply(each),
                    (key, valueList) -> isEmpty(valueList) ? add(new ArrayList<>(list.size()), each) : add(valueList, each));
        }
        return propertiesMap;
    }

    /**
     * 根据列表对象中的某属性值为key划分列表,value为key的属性值相同的对象列表,value为key的属性值相同的对象中指定属性的值列表,
     * 功能同stream().collect(Collectors.groupingBy())
     *
     * @param list
     * @param keyGetter
     * @param valGetter
     * @param <T>
     * @param <K>
     * @param <V>
     * @return
     */
    public static <T, K, V> Map<K, List<V>> groupToMap(Collection<T> list, Function<T, K> keyGetter, Function<T, V> valGetter) {
        Map<K, List<V>> propertiesMap = new HashMap<>(list.size());
        for (T each : list) {
            K key = keyGetter.apply(each);
            List<V> values = Optional.ofNullable(propertiesMap.get(key)).orElse(new ArrayList<>());
            values.add(valGetter.apply(each));
            propertiesMap.put(key, values);
        }
        return propertiesMap;
    }

    /**
     * 获取列表中重复的值
     *
     * @param list
     * @param <T>
     * @return
     */
    public static <T> Set<T> collectRepeats(Collection<T> list) {
        Set<T> set = new HashSet<>(list.size());
        return list.stream()
                .filter(e -> !set.add(e))
                .collect(Collectors.toSet());
    }

    /**
     * 按指定大小,分隔集合,将集合按规定个数分为n个部分
     *
     * @param <T>
     * @param list
     * @param len
     * @return
     */
    public static <T> List<List<T>> splitList(List<T> list, int len) {
        if (list == null || list.isEmpty() || len < 1) {
            return Collections.emptyList();
        }
        List<List<T>> result = new ArrayList<>();

        int size = list.size();
        int count = (size + len - 1) / len;
        for (int i = 0; i < count; i++) {
            List<T> subList = list.subList(i * len, ((i + 1) * len > size ? size : len * (i + 1)));
            result.add(subList);
        }
        return result;
    }

}

看看使用该工具类替代Stream简单操作后的效果吧:

List<String> namess = ListUtils.collectToList(list,Classify::getName);
Map<Long, String> idMap = ListUtils.collectToMap(list,Classify::getId,Classify::getName);
Map<Long, List<Classify>> parentSonsMap = ListUtils.groupToMap(list,Classify::getParentId);
// 将List转化成key为parentId,value为子分类name列表的映射
Map<Long, List<String>> parentSonNamesMap = ListUtils.groupToMap(list,Classify::getId,Classify::getName);

可以看出通过函数式接口作为参数传递,不仅可以增加程序的可读性,还可以为我们的编码开发添加不少扩展性。

  • 简化局部不同多处相同的代码块

局部不同多出相同的代码块重复出现的状况总会遇到,如一些业务代码前后都相同唯独中间不同,如DB连接-操作-释放、Ssh连接-操作-释放,以下将以一个ssh连接-操作-释放的代码来扩展函数式编程简化代码的用法。
可能会有人疑问ssh连接-操作-释放这样的实际操作业务不多吧?就在一段时间之前,上级让我去Zabbix查看各服务器的CPU、内存、磁盘使用率然后写入文档。看到机器数的我内心是拒接的,于是想出了使用java ssh连接到服务器执行相应的查看指令然后提取占用率打印到控制台上,再copy到文档中(反正得到了默许了)。以下是未优化前的两个查询方法:

/**
 * 查询cpu占用率
 */
public static String cpuPercent(String ip, String username, String passw
    JSch jsch = new JSch();
    Session session = null;
    Channel channel = null;
    String cpuPercent = null;
    try {
        session = jsch.getSession(username, ip, 22);
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        session.setPassword(password);
        session.connect();
        String cmd = "sar -u 3 1|awk '{print $8}'|tail -1";
        channel = session.openChannel("exec");
        ((ChannelExec) channel).setCommand(cmd);
        ((ChannelExec) channel).setErrStream(System.err);
        ((ChannelExec) channel).setPty(true);
        channel.connect();
        InputStream in = channel.getInputStream();
        String output = IOUtils.toString(in, StandardCharsets.UTF_8);
        cpuPercent = HUNDRED.subtract(BigDecimal.valueOf(Double.valueOf(
                .setScale(2, RoundingMode.HALF_UP)
                .toString() + "%";
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (channel != null) {
            channel.disconnect();
        }
        if (session != null) {
            session.disconnect();
        }
    }
    return cpuPercent;
}

/**
 * 磁盘占用率查询
 */
public static String diskPercent(String ip, String username, String pass
    JSch jsch = new JSch();
    Session session = null;
    Channel channel = null;
    String diskPercent = null;
    try {
        session = jsch.getSession(username, ip, 22);
        Properties config = new Properties();
        config.put("StrictHostKeyChecking", "no");
        session.setConfig(config);
        session.setPassword(password);
        session.connect();
          String cmd = "df -hl | grep apps|tail -1|awk '{print $4}'";
        String cmd = "df -hl | grep apps|tail -1|awk '{print $5}'";
        channel = session.openChannel("exec");
        ((ChannelExec) channel).setCommand(cmd);
        ((ChannelExec) channel).setErrStream(System.err);
        ((ChannelExec) channel).setPty(true);
        channel.connect();
        InputStream in = channel.getInputStream();
        diskPercent = IOUtils.toString(in, StandardCharsets.UTF_8);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (channel != null) {
            channel.disconnect();
        }
        if (session != null) {
            session.disconnect();
        }
    }
    return diskPercent;
}

相信大家可以看出Ssh连接与释放的代码块是相同的,唯独操作是不同的,于是我把相同的代码块写入了一个方法中,操作的代码块作为参数,优化后的完整代码如下:

public class SshClientUtils {
    private static final BigDecimal HUNDRED = BigDecimal.valueOf(100);
    private static final String RESULT_FORMAT = "%s\t\t%s\t\t%s\t%s";

    /**
     * 执行查询cpu、mem、disk命令并打印各占用率
     */
    public static void exec(SshConfig sshConfig) {
        System.err.println("cpu%\t\tmem%\t\tdisk\tip");
        String username = sshConfig.getUsername();
        String password = sshConfig.getPassword();
        List<String> ipList = sshConfig.getIpList();
        ipList.forEach(ip -> {
            String cpuPercent = cpuPercent(ip, username, password);
            String memoryPercent = memoryPercent(ip, username, password);
            String diskPercent = diskPercent(ip, username, password);
            System.out.println(String.format(RESULT_FORMAT, cpuPercent, memoryPercent, diskPercent, ip)
                    .replaceAll("\n|\r\n", ""));
        });
    }

    /**
     * 查询cpu占用率
     */
    public static String cpuPercent(String ip, String username, String password) {
        String cmd = "sar -u 3 1|awk '{print $8}'|tail -1";
        return exec(ip, username, password, cmd, output -> HUNDRED.subtract(BigDecimal.valueOf(Double.valueOf(output)))
                .setScale(2, RoundingMode.HALF_UP)
                .toString() + "%");
    }

    /**
     * 内存占用率查询
     */
    public static String memoryPercent(String ip, String username, String password) {
        String cmd = "free|grep Mem";
        return exec(ip, username, password, cmd, output -> {
            String[] memories = output.replaceAll("\\s+", ",")
                    .substring(5)
                    .split(",");
            double total = Integer.parseInt(memories[0]);
            double free = Integer.parseInt(memories[2]);
            double buffers = Integer.parseInt(memories[4]);
            double cache = Integer.parseInt(memories[5]);
            BigDecimal freePercent = BigDecimal.valueOf((free + buffers + cache) / total)
                    .setScale(6, RoundingMode.HALF_UP);
            return BigDecimal.ONE.subtract(freePercent)
                    .multiply(HUNDRED)
                    .setScale(2, RoundingMode.HALF_UP)
                    .toString() + "%";
        });
    }


    /**
     * 磁盘占用率查询
     */
    public static String diskPercent(String ip, String username, String password) {
        String cmd = "df -hl | grep apps|tail -1|awk '{print $5}'";
        return exec(ip, username, password, cmd, output -> output);
    }

    /**
     * 直接执行命令
     */
    public static String exec(String ip, String username, String password, String command, Function<String, String> execFunc) {
        JSch jsch = new JSch();
        Session session = null;
        Channel channel = null;
        try {
            session = jsch.getSession(username, ip, 22);
            Properties config = new Properties();
            config.put("StrictHostKeyChecking", "no");
            session.setConfig(config);
            session.setPassword(password);
            session.connect();
            channel = session.openChannel("exec");
            ((ChannelExec) channel).setCommand(command);
            ((ChannelExec) channel).setErrStream(System.err);
            ((ChannelExec) channel).setPty(true);
            channel.connect();
            InputStream in = channel.getInputStream();
            String output = IOUtils.toString(in, StandardCharsets.UTF_8);
            return func.apply(execFunc);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (channel != null) {
                channel.disconnect();
            }
            if (session != null) {
                session.disconnect();
            }
        }
        return null;
    }
}

可以看出优化的代码将Ssh的连接与操作都抽象到exec()方法中了,而实际操作则是由入参的Function实现决定,以上便是一个通过Function优化代码部分不同的例子。

将if-set对象属性通过函数式接口放到对象内部执行
话多不如实例,相信大家都遇到过类似以下这样的情况:

if(condition1){
    classify.setName("Wilson");
}
if(condition2){
    classify.setLevel(5);
}

好麻烦,能不能再简单一点(我的简单永远没有上限),现在先对以上代码块分析一下(简化的核心在于抽离),相同的部分主要有if、classify,不同的部分为condition的值、set方法、set的值,既然有相同的就作为方法,不同的就作为参数吧(是不是跟ssh的例子想法差别不大吧),于是我在Classify类中添加了以下方法:

public <V> Classify set(boolean isSet, V value, BiFunction<Classify, V, Classify> setFunction) {
    return isSet ? setFunction.apply(this, value) : this;
}

???
唔,这里可能有一些门槛,如果暂时不理解或觉得无法灵活运动的也不用着急,代码都是慢慢磨出来的,调用一下吧:

Classify classify = new Classify();
classify.set(true, "Wilson", Classify::setName)
        .set(false, 5, Classify::setLevel);
System.out.println(classify);
// 打印出Classify(id=null, name=Wilson, level=null, parentId=null, sonClassifies=null)

由于Classify在类上添加了Lombok的注解@Accessors(chain = true),所以每个set方法结果都会返回当前对象方便链式调用(我很喜欢链式),所以上面的set方法可以直接返回apply(this,setFunction)的结果。BiFunction前面有提过是需要两个参数并返回一个结果的,在该例子中,由于Classify的setProperty()是返回当前对象的,所以不能用Function<T,R>作为set()的函数式参数(否则T与R都是Classify,无法设置属性),Classify对象作为BiFunction的第一个参数,set()方法的value作为第二个参数,当前classify对象作为返回值,这样就可以保持我的对象可以继续链式调用各set方法。
也有会有人疑问set方法设置返回值不会影响程序的正常运行(如框架的调用)吗?这里个人是从反射与Java关键字void的角度思考过后就一直习惯使对象set方法返回当前对象了,这里希望大家也思考一下便不多作讲解了。

  • 使用Supplier再提高一下copyProperties的逼格

    相信接触过Spring的都会使用过其中BeanUtils的copyProperties()方法,个人经常使用该方法进行VO属性到Model属性的设置,Model一般都是现场new所以内部属性都是的,反正都是空的何不再通过Supplier函数式接口扩展一下工具类提高一下逼格呢?于是便有了以下代码:

    @NoArgsConstructor
    public class ObjectUtils {

    public static <S, T> T copyProperties(S source, T target) {
        BeanUtils.copyProperties(source, target);
        return target;
    }
    
    public static <S, T> T copyProperties(S source, Supplier<T> targetSupplier) {
        T target = targetSupplier.get();
        BeanUtils.copyProperties(source, target);
        return target;
    }

    }

再以一段Controller的伪代码演示一下:
Long id = classifyService.insert(ObjectUtils.copyProperties(classifyVO,Classify::new));