特点
-
只能标记在"有且仅有一个抽象方法"的接口上,表示函数式接口;
-
接口如果重写了Object中的方法,如toString(),equals()方法,不算抽象方法;
之所以只能有且仅有一个抽象方法是因为在调用函数编程时,如果有多个抽象方法的时候,那么()-> {}(或者a ->{…})这种写法,编译器就不知道这是重写的哪个方法了!
简单用例
-
java中的Runable接口;
源码如下
//Runable接口源码类 @FunctionalInterface public interface Runnable { public abstract void run(); } //Thread源码类: public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); }
所以我们这样写:
new Thread( ()->{ System.out.println(Thread.currentThread().getName()); } ).start();
编译器对这个语法糖知道该解析成如下:
new Thread( new Runnable() { @Override public void run() { System.out.println(Thread.currentThread().getName()); } } ).start();
大家注意看new Thread(Runable target),其中参数Runable是一个用@FunctionalInterface修饰的接口类,代表此参数形式可以已函数编程的形式传进来;
其中()->{} 就是代表对run()方法的重写
看到这里,我们是不是可以侠义上的理解:传入的参数是函数式写法,既然这样,那我们是不是可以通过这个特性来避免getter/setter拿属性名存在的硬编码问题!
项目落地(实际应用)
需求
HashMap<String, Integer> map = new HashMap<>(8); map.put("age", Constant.AGE); map.put("sex", Constant.SEX); map.put("height", Constant.HEIGHT); //其中,age,sex,height是people的属性
期望效果
HashMap<String, Integer> map = new HashMap<>(8); map.put(People::getAge, Constant.AGE); map.put(People::getSex, Constant.SEX); map.put(People::getHeight, Constant.HEIGHT);
这样的效果是避免字段修改时,硬编码导致出属性不存在(空指针)的bug;
代码实现
-
定义函数式接口
//getter @FunctionalInterface public interface IGetter<T> extends Serializable { //此方法暂时用不到,只是为了使函数式接口不报错 Object apply(T source); } //setter @FunctionalInterface public interface ISetter<T, U> extends Serializable { //此方法暂时用不到,只是为了使函数式接口不报错 void accept(T t, U u); }
-
定义getter/setter引用转换属性名的工具类
public class BeanUtils { ... /*** * 转换方法引用为属性名 * @param fn * @return */ public static <T> String convertToFieldName(IGetter<T> fn) { SerializedLambda lambda = getSerializedLambda(fn); String methodName = lambda.getImplMethodName(); String prefix = null; if(methodName.startsWith("get")){ prefix = "get"; } else if(methodName.startsWith("is")){ prefix = "is"; } if(prefix == null){ log.warn("无效的getter方法: "+methodName); } // 截取get/is之后的字符串并转换首字母为小写 return uncapFirst(StringUtils.substringAfter(methodName, prefix)); } /*** * 转换setter方法引用为属性名 * @param fn * @return */ public static <T,R> String convertToFieldName(ISetter<T,R> fn) { SerializedLambda lambda = getSerializedLambda(fn); String methodName = lambda.getImplMethodName(); if(!methodName.startsWith("set")){ log.warn("无效的setter方法: "+methodName); } // 截取set之后的字符串并转换首字母为小写(S为diboot项目的字符串工具类,可自行实现) return uncapFirst(StringUtils.substringAfter(methodName, "set")); } /*** * 获取类对应的Lambda * @param fn * @return */ private static SerializedLambda getSerializedLambda(Serializable fn){ //先检查缓存中是否已存在 SerializedLambda lambda = null; try{//提取SerializedLambda并缓存 Method method = fn.getClass().getDeclaredMethod("writeReplace"); method.setAccessible(Boolean.TRUE); lambda = (SerializedLambda) method.invoke(fn); } catch (Exception e){ log.error("获取SerializedLambda异常, class="+fn.getClass().getSimpleName(), e); } return lambda; } //字符串并转换首字母为小写 public static String uncapFirst(String input){ if(input != null){ return Character.toLowerCase(input.charAt(0)) + input.substring(1); } return null; } }
-
引用效果
HashMap<String, Integer> map = new HashMap<>(8); map.put(BeanUtils.convertToFieldName(People::getAge), Constant.AGE); map.put(BeanUtils.convertToFieldName(People::getSex), Constant.SEX); map.put(BeanUtils.convertToFieldName(People::getHeight), Constant.HEIGHT);
以上代码摘自:[利用Lambda实现通过getter/setter方法引用拿到属性名]
细节分析
在以上案例中,运用到了java的序列化以及反射的相关知识,按照我自己的理解我解释一下(如有错误,感谢大家的指正):
- 在调用BeanUtils.convertToFieldName()方法的时候,由于所有类已经进行序列化,此时传入的参数时在其调用类的序列化对象的对应的lambda属性;
- 再然后通过getSerializedLambda()方法的暴力反射,获取到“writeReplace”方法(后面会解释这个方法时干嘛用的),执行该方法后直接获取到调用lambda方法的对象以及其执行的方法,即people对象和getAge方法;
- 最后回到convertToFieldName()方法中的lambda.getImplMethodName();再取消前缀即可获取属性名。
在这里给大家解释一下”writeReplace“,他是在类进行序列化的时候,默认会通过此方法修改序列化的对象,所以这方法可以获取到序列化的对象,这个是关键点;
另外大家对序列化的知识点还模糊的话,可以参考这个:Java Serializable接口(序列化)理解及自定义序列化
引用一下最关键的地方:
自定义序列化是由ObjectInput/OutputStream在序列化/反序列化时候通过反射检查该类是否存在以下方法(0个或多个):执行顺序从上往下,序列化调用1和2,反序列调用3和4;transient关键字当某个字段被声明为transient后,默认序列化机制就会忽略该字段。
Object writeReplace() throws ObjectStreamException;可以通过此方法修改序列化的对象
void writeObject(java.io.ObjectOutputStream out) throws IOException; 方法中调用defaultWriteObject() 使用writeObject的默认的序列化方式,除此之外可以加上一些其他的操作,如添加额外的序列化对象到输出:out.writeObject(“XX”)
void readObject(java.io.ObjectInputStream in) throws Exception; 方法中调用defaultReadObject()使用readObject默认的反序列化方式,除此之外可以加上一些其他的操作,如读入额外的序列化对象到输入:in.readObject()
Object readResolve() throws ObjectStreamException;可以通过此方法修改返回的对象
参考文献
-
https://www.cnblogs.com/yoohot/p/6019767.html ---->序列化
-
https://segmentfault.com/a/1190000019389160 —>利用Lambda实现通过getter/setter方法引用拿到属性名
-
https://www.jianshu.com/p/52cdc402fb5d ---->函数式接口@FunctionalInterface
- 版权声明:文章来源于网络采集,版权归原创者所有,均已注明来源,如未注明可能来源未知,如有侵权请联系管理员删除。