该文章主要介绍JDK中各类常见的函数式接口,并会附上一些我的对函数式编程的一些扩展思考与实际用法。java
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();}
等。session
Supplier<T>
:供应商接口,可理解为对象的无参构造函数代理接口,每次调用其get()方法都会产生一个新的对象。常见于类Stream{generate();collect();}
Objects{requireNonNull();}
、ThreadLocal{withInitial();}
app
Predicate<T>
:传入一个对象返回其指定行为方法执行结果布尔值,方法可临时实现。常见于类Optional{filter();}
、Stream{filter();anyMatch();allMatch();noneMatch();}
、ArrayList{removeIf();}
等框架
BiPredicate<T, U>
:可根据前面的Bi接口与Predicate
推断,再也不多做阐述ssh
Stream中的函数式编程ide
如下先以一段代码简单的介绍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;
}
复制代码
现有一个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方法返回当前对象了,这里但愿你们也思考一下便很少做讲解了。
相信接触过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));
实际开发中还有不少能够经过函数式接口简化代码的地方,如从数据库中查出全部分类后而后将分类树形排列再进行缓存预热,树形排列也可经过函数式接口抽象为一个工具方法,但因为通用性不强把该方法tree
从ListUtils
中移除了(当时实现是经过Map避免递归,只需遍历2次全部分类列表完成排序完成),更多的用法能够从实际开发中思考实现,具体想过多少种本身的记不清了。
该文章是我的最耗时的少有的基础用法文章,以上的全部例子并不必定适合于全部人,但相信能够扩展一下思惟,若是是初接触者但愿能够为你雪中送炭,若是有必定熟练度了但愿能为你锦上添花。这篇鬼文花了我好多多多多多多时间~难受。