使用接口和方法引用的异同及函数式接口的便利性
这篇文章主要想讨论两个事情:
1.接口和抽象类的相似性以及使用场景
2.java8中的函数式接口在使用上的便利
其中话题1对于话题2的实现和使用意义,其实是有关联性的
结论是:
接口和抽象类在某些场合的作用是一致的,区别是侧重点不同。
大概的体会就是:
1.都声明了当前对象可以做什么事情,是一个什么“类型”的对象,都有未实现的逻辑
2.抽象类的使用场景偏向于逻辑和内部实例属性的重用
3.接口更偏向于规范当前对象的类型和能够做出的行为——因为具体实现可以毫不相干,所以不在意是否有可以重用的属性/方法
函数式接口在方法引用和lambda的配合下淡化了接口“类型”的功能,单纯约定了它唯一的方法的入参和返回值。使得函数式接口更加泛用,也更加抽象。
由此在编码方面更加自由和不受控制——当前,我很喜欢这一点。
一个没意思的老生常谈 ——抽象类和接口有什么区别?
1.极端的例子
HashTable
现在看,HashTable实现了Map接口,看起来和HashMap没有太大区别。但是在早期的jdk版本中,HashTable只是继承了抽象类Dictionary,在稍后的版本里才增加了实现Map接口
最迟在jdk 1.2.2_017时增加了Map
仔细观察这个Dictionary类,会发现它的方法全部都是抽象的,干活全靠子类。反过来说,把这个抽象类改成一个接口,在使用上不会有影响(当然,据说保留它是为了兼容)。
2.另一个极端的例子
StringBuilder和StringBuffer,这两个类有共同的父类AbstractStringBuilder。
同Dictionary相反,AbstractStringBuilder虽然叫抽象类,但是只有一个toString方法是abstract的,它的两个子类在实现逻辑时,大体是原封不动地调用父类方法,区别仅在于StringBuffer加了同步关键字synchronized。
这个AbstractStringBuilder类基本上把活全都做了。实际上,它唯一的抽象方法toString也完全可以不写出来,反正有Object的toString兜底,也实现了CharSequence接口,抽象类里没实现的CharSequence.toString方法由子类去实现也是天经地义。
3.java8中的接口
在java8中,接口有了默认方法和静态方法。
相对于之前的版本,现在接口除了有需要实现的抽象方法,还可以有一些逻辑更加确定的"非抽象方法",这使得接口使用起来有了抽象类的意思。
一个例子是spring的org.springframework.web.servlet.config.annotation.WebMvcConfigurer
这个接口在spring-webmvc5.0的时候增加了默认方法,同时它的抽象实现类WebMvcConfigurerAdapter被标记了@Deprecated。
而这个WebMvcConfigurerAdapter里面的内容,就是提供了WebMvcConfigurer接口方法的默认实现,和新版WebMvcConfigurer的默认方法一样。
如果不考虑多继承,现在的WebMvcConfigurer的默认方法意见完全可以替代旧的抽象类WebMvcConfigurerAdapter。如果考虑多继承,WebMvcConfigurer还更加自由。
与之类似的还有HandlerInterceptor
在上面,展示了三个比较典型的例子:
一个只有未实现逻辑的抽象方法,长得像接口的抽象类Dictionary,一个只有默认方法,长得像抽象类的接口WebMvcConfigurer,和一个几乎没有抽象方法的抽象类AbstractStringBuilder。
把这3个类放在一起,再问抽象类和接口在使用场景上有什么区别,什么场景下适合用抽象类,什么场景下用接口更好,就可以迷惑一下人了。
4.更加相似的行为,以及区别
实现接口,经常会有直接在代码里创建匿名类,如
同样的方式也可以实现抽象类,
甚至不抽象的类也可以创建匿名类
编译之后都会多出一个名字有$的class文件
这样的情形,都是某个类型的对象存在一些未被实现的方法(逻辑),在创建对象实例时必须实现这些方法。单从代码上看,判断不出上面两个类型哪个是接口哪个是抽象类。
至少在此刻,它们做的是一种事情,把未实现的逻辑具体化。
这时接口和抽象类是如此的相似,而它们的区别也出现了。
Runnable接口只有一个抽象方法run,这个接口只是为了告诉外界,这个类型的对象可以运行一个没有返回值的方法。
抽象类除了有需要实现的抽象方法外,还额外给子类传递了一套已经实现的实例方法和属性,子类是不是使用父类现成的逻辑自然是自由的。在逻辑非常复杂时,必然会需要很多成员变量和固定的方法,从一个光秃秃的接口开始实现非常麻烦,不如继承一个实现了大部分通用功能的父类,然后在子类里去实现抽象方法。
所以这给了我一个印象,在都有未实现的方法(逻辑)的共同点时:
接口侧重于告诉外界,自身是一个什么类型,能做哪些事情,
抽象类侧重于向子类传递一些已实现的东西,比如类变量,实例方法。
它们都有抽象的部分,不过侧重点不一样。
这是我的想法。
5.反过来说
如果应用场景业务非常简单,不需要很多额外的属性变量,只需要实现有限的抽象方法来体现逻辑,
那么在允许有默认方法和静态方法的java8版本以后,接口和抽象类的实际作用其实是一样的。
函数式接口的统一
1.函数式接口是个接口
虽然j在ava8中的java.util.function包下的那些默认的函数式接口都有@FunctionalInterface注解,但这不是必需的。
一个接口如果只有一个抽象方法,那么它就是个函数式接口。它,就是一个接口而且。
所以,要实现一个函数式的接口可以建个类直接继承,也可以使用匿名类。但在java8中增加了lambda表达式和方法引用的实现方式
在这里,无论是匿名类还是lambda表达式或是方法的引用,都可以看成一个接口的实例。
各种实现的背后机制当然是不同的,在我看来,目的是一样的。
即,实现了一段抽象的逻辑。这段逻辑被包装成一个普通的对象传递到任何一个类型匹配的方法里,在合适的地方调用对象的apply方法,来运行逻辑。
以上操作的实质在过去的java版本里也可以实现,实现一个接口或继承一个抽象类,把这个对象的实例传进某些通用的方法里,在合适的地方调用指定接口的指定方法。
现在使用了函数接口,它只有一个抽象方法,只对外保证入参和返回值的类型,弱化了接口对象是什么样的类型这样一个接口本来的特点
简直就像在告诉调用者,"我能 do something,但是我大概是什么指定的类型?这个不是重点"。
非要说这可能是什么东西?
这个对象就是个Function/Consumer/Supplier/......的类型,或者干脆是个自定义的接口。它的具体实现可能是某个子类,可能是个匿名类,可能是某个对象的方法引用,或者干脆是在运行时才生成具体方法的一段lambda表达式(这里牵扯到lambda和方法引用的实现,不做展开了。。。)
2.仔细看
仔细看,上面的函数式接口所做的,其实并没有超出一般意义上接口的定义。
只不过,函数式接口又向抽象这个方向迈了一步。一个参数有限的函数式接口,有太多的对象可以套进去。
它模糊了自身的类型声明,在一个方法的情况下,只保留了入参和返回值的关系。
再进一步,极端一点,在单方法的情况下,接口的对象,和它所拥有的方法,可以说是等效的。
即,在某些场合,一个方法约等于一个接口的匿名实现。
由此看一看函数式接口可以用一个 ArrayList::new(方法引用) 或 t -> t.size() (lambda表达式) 来赋值,是很正常的事情了。
3.稍微自由点的例子
一个有多个抽象方法的接口可以拆成多个函数式接口,多个函数式接口也可以组成一个具体实现不固定的非抽象类。
这个DoSomething 类,在类定义的时候不知道它的三个属性能做什么事,实例化后能做的事情也可以随意更改。即便不考虑安全性,好像也是有点灵活过头了。
不过在某些场合可以用一下:
假设有一个组织数据,通过不同渠道:网站消息、app消息,发送消息的功能
public void send3(String id, ISender sender){
String data = sender.getData(id);
//公共逻辑1,比如记录日志
IMsg msg = sender.getMsg(data);
//公共逻辑2,比如:将组织的msg信息保存到数据库,状态为未发送
boolean res = sender.send(msg);
//公共逻辑3,比如:根据是否成功来回写msg表状态/记录日志/发送报警通知
}
流程是通过id获取数据,组织成不同渠道的消息后发送,再进行一些个性处理和公共逻辑处理
ISender 是接口,依据不同渠道的实例来做不同的操作
public interface ISender {
String getData(String id);
IMsg getMsg(String data);
boolean send(IMsg msg);
}
IMsg 是接口,有网站消息和app消息的不同实现
public interface IMsg {
String getData();
}
比如网站渠道的
public class WebSender implements ISender {
@Override
public String getData(String id) {
System.out.println("webDao 假装在查询...");
return "web数据";
}
@Override
public IMsg getMsg(String data) {
return new WebMsg(data);
}
@Override
public boolean send(IMsg msg) {
System.out.println("webSerivce 发送消息:"+msg.getData());
return true;
}
}
class WebMsg implements IMsg {
String data;
public WebMsg(String data){
this.data=data;
}
@Override
public String getData() {
return data;
}
}
这些也是比较常用的做法。
如果使用了函数式接口,就耳目一新了
public class FunctionSender{
public Function<String, String> getData;
public Function<String, IMsg> getMsg;
public Function<IMsg, Boolean> send;
}
public FunctionSendergetFunctionSender(){
FunctionSendersender = new FunctionSender();
WebSender webSender = new WebSender();
AppSender appSender = new AppSender();
sender.getData = webSender::getData;
sender.getMsg = appSender::getMsg;
sender.send = t -> true;
return sender;
}
public void send4(String id, FunctionSendersender){
String data = sender.getData.apply(id);
//公共逻辑1
IMsg msg = sender.getMsg.apply(data);
//公共逻辑2
boolean res = sender.send.apply(msg);
//公共逻辑3
}
上面的FunctionSender类,特殊之处在于它的属性都是函数式接口,在FunctionSender的类定义的时候,以及它声明一个实例对象的时候,它能做什么都不确定。
只有给FunctionSender的三个属性赋值的时候,才能知道它能做什么。
所以就出现了这个FunctionSender
1.对象获取的是Web渠道的数据,
2.组织的是App渠道的消息对象,
3.发消息始终返回true。
构造出FunctionSender这个类,如果在生产中使用,我也觉得过分自由,不过便利也很明显,就是通过方法引用和lambda表达式,可以动态地定义对象的行为。
这个FunctionSender和一般的接口实现类在代码中使用起来是一样的,但更加抽象。
待续......