作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
在上一篇设计山寨版Stream API时,有一个技巧被频繁使用:接口多态。
接口,用的是函数式接口,即接口内部有且仅有一个抽象方法。
多态,原本指的是接口下有多个子类实例可以指向接口引用,但由于函数式接口恰好仅有一个方法,此时接口多态等同于“方法多态”,即一个抽象方法拥有多个不同的具体实现。
接口多态
我们都知道Java是面向对象的语言,它具备多态性。私以为,多态的精髓在于晚绑定。什么意思呢?
PocketMon pocketMon = new Pikaqiu();
pocketMon.releaseSkill();
只看pocketMon.releaseSkill()你能猜出来技能是电击还是喷火吗?
哦?一眼就看出来了?
这样呢?
Properties pro = new Properties();
FileInputStream in = new FileInputStream("pocketmon.properties");
pro.load(in);
PocketMon pocketMon = Class.forName(pro.getProperty("nextPocketMon")).newInstance();
pocketMon.releaseSkill();
完全看不出来了。
即使你打开pocketmon.properties看了是皮卡丘,运行时虚拟机看到的可能是我修改后的喷火龙。
这种现象其实很奇妙:明明代码都写死了,但虚拟机却无法提前确定具体会是哪只神奇宝贝在调用releaseSkill(),除非实际运行到这行代码。而这,正是得益于多态。
多态的原理,本质是还是JVM层面通过运行时查找方法表实现的。可以简单理解为,JVM在运行时需要去循环遍历这个方法对应的多态实现,选择与当前运行时对象匹配的方法进行调用。所以,从理论上来说,晚绑定的多态在性能上是不如早绑定的(直接写死,不用多态)。而多态是设计模式的灵魂,所以对于一些非常、非常、非常要求性能的场景来说,过于繁重的设计反而会降低性能。说白了,这世上就不存在多、快、好、省。
多态是“晚绑定”思想的体现:对于Java而言,方法的调用并不是编译时绑定,而是运行时动态绑定的,取决于引用具体指向的实例。
方法多态
我生造了“方法多态”这个概念,但这个概念在函数式接口的前提下是站得住脚的,而且有利于跳出面向对象,贴近函数式编程。
我们来看一个需求:
要求写一个cook()方法,传入鸡翅和可乐,你给我做出可乐鸡翅。
很多人可能下意识地就把代码写死了:
public static CokaChickenWing cook(Chicken chicken, Coka coka){
1.放油、放姜;
2.放鸡翅;
3.倒可乐;
4.return CokaChickenWing;
}
但是,网上也有人说应该先倒可乐再放鸡翅,每个人的口味不同,做法也不同。有没有办法把这两步延迟确定呢?让调用者自己来安排到底是先倒可乐还是先放鸡翅。
可以这样:
public static CokaChickenWing cook(Chicken chicken, Coka coka, function twoStep){
1.放油、放姜;
2~3.twoStep();
4.return CokaChickenWing;
}
想法很好:既然这两步不确定,那么就由调用者来决定吧,让调用者自己传进来。
我们知道Java是不能直接传递方法的,但利用策略模式可以解决这个问题。
定义一个接口:
interface TwoStep {
void excute();
}
然后呢?
public static CokaChickenWing cook(Chicken chicken, Coka coka, TwoStep twoStep){
1.放油、放姜;
2~3.twoStep.excute();
4.return CokaChickenWing;
}
这里twoStep.excute()是确定的吗?
没有。
你说它是先倒可乐,再放鸡翅?我偏要说它是先放鸡翅,再倒可乐!反正接口也没方法体,具体实现要看你传进来什么对象。
所以twoStep.excute()充其量只是先替“某些操作占个坑”,后面再确定。
什么时候确定呢?
main(){
TwoStep twoStep = new TwoStep(){
@Override
public void excute(){
2.先放鸡翅
3.再倒可乐
}
}
// 调用cook时确定(运行时)
cook(chicken, coka, twoStep);
}
public static CokaChickenWing cook(Chicken chicken, Coka coka, TwoStep twoStep){
1.放油、放姜;
2~3.twoStep.excute();
4.return CokaChickenWing;
}
来,学过Lambda表达式后,我们换个时髦的写法:
main(){
// 调用cook时确定 方案1
cook(chicken, coka, (鸡翅, 可乐) -> 2.先放鸡翅,3.再倒可乐);
// 调用cook时确定 方案2
cook(chicken, coka, (鸡翅, 可乐) -> 2.先倒可乐,3.再放鸡翅);
}
public static CokaChickenWing cook(Chicken chicken, Coka coka, TwoStep twoStep){
1.放油、放姜;
2~3.twoStep.excute();
4.return CokaChickenWing;
}
这就是我所谓的“方法多态”:通过函数式接口把形参的坑占住,后续传入不同的Lambda实现各自逻辑。
晚绑定与模板方法模式
在设计模式中策略模式和模板方法看起来有点像,但其实不一样。策略模式使用接口占坑,然后传入实际对象调用需要的方法,而模板方法模式是用抽象方法占坑,粒度其实小一些。
晚绑定最典型的应用就是模板方法模式:抽象类确定基本的算法骨架,把不确定的、变化的部分做成抽象方法剥离出去,由子类来实现。
还是以发送验证码为例:
/**
* 验证码发送器
*
* @author mx
*/
public abstract class AbstractValidateCodeSender {
/**
* 生成并发送验证码
*/
public void sendValidateCode() {
// 1.生成验证码
String code = generateValidateCode();
// 2.把验证码存入Session
// ....
// 3.抽象方法占坑,用于发送验证码
sendCode();
}
/**
* 具体发送逻辑,留给子类实现:发送邮件、或发送短信都行
*/
protected abstract void sendCode();
/**
* 生成验证码
*
* @return
*/
public String generateValidateCode() {
return "123456";
}
}
对于上面的模板,我们可以有多种实现方式,以便把sendCode()这个坑填上:
/**
* 短信验证码发送
*
* @author mx
*/
public class SmsValidateCodeSender extends AbstractValidateCodeSender {
@Override
protected void sendCode() {
// 通过阿里云短信发送
}
}
/**
* QQ邮箱验证码发送
*
* @author mx
*/
public class EmailValidateCodeSender extends AbstractValidateCodeSender {
@Override
protected void sendCode() {
// 通过QQ邮箱发送
}
}
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬