接口默认方法是什么鬼
接口之所以成为接口,就在于它没有实现,只是声明。但后来一切都变了,Java 里出现了默认方法,C# 也出现了默认方法。接口已经不像传统意义上的接口,其概念开始向抽象类靠近,一个纯抽象的东西,突然出现了实体,于是开始傻傻分不清了。
世界已经变了,可他是怎么开始改变的呢?
1. 缘起
虽然本文有提到 Java,但是笔者近年主要还是在写 C# 程序,所以未明确语言的命名规范会更倾向 C# 的规范一些,敬请谅解。
曾经,我们定义了 IStringList
接口,它声明了一个列表:
这只是个例子,为了避免引入更多的技术概念,这里没有使用泛型举例。
interface IStringList { void Add(string o); // 添加元素 void Remove(int i); // 删除元素 string Get(int i); // 获取元素 int Length { get; } // 获取列表长度 }
不管怎么说,这个列表已经拥有了基本的增删除改查功能,比如遍历,可以这样写
IStringList list = createList(); for (var i = 0; i < list.Length; i++) { string o = list.Get(i); // Do something with o }
这个 IStringList
作为一个基础接口在类库中发布之后,大量的程序员使用了这个接口,实现了一堆各种各种各样的列表,像 StringArrayList
、LinkedStringList
、StringQueue
、StringStack
、SortedStringList
……有抽象类,有扩展接口,也有各种实现类。总之,经过较长一段时间的积累,IStringList
的子孙遍布全球。
然后 IStringList
的发明者,决定为列表定义更多的方法,以适合在技术飞速发展下开发者们对 IStringList
使用便捷性的要求,于是
interface IStringList { int IndexOf(string o); // 查找元素的索引,未找到返回 -1 void Insert(string o, int i); // 在指定位置插入元素 // ------------------------------ void Add(string o); // 添加元素 void Remove(int i); // 删除元素 string Get(int i); // 获取元素 int Length { get; } // 获取列表长度 }
当然,接口变化之外所有实现类都必须实现它,不然编译器会报错,基础库的抽象类 AbstractStringList
中实现了上述新增加的接口。整个基础库完美编译,发布了 2.0 版本。
然而,现实非常残酷!
基础库的用户们(开发者)发出了极大的报怨声,因为他们太多代码编译不过了!
是的,并不是所有用户都会直接继承 AbstractStringList
,很多用户直接实现了 IStringList
。还有不少用户甚至扩展了 IStringList
,但他们没有定义 int IndexOf(string o)
而是定义的 int Find(string o)
。由于基础库接口 IStringList
的变化,用户们需要花大量地时间去代码来实现 IStringList
中定义的新方法。
这个例子是提到了 IStringList
,只添加了两个方法。这对用户造成的麻烦虽然已经不小,但工作量还算可以接受。但是想想 JDK 和 .NET Framework/Core 庞大的基础库,恐怕用户只能用“崩溃”来形容!
2. 办法
肯定不能让用户崩溃,得想办法解决这个问题。于是,Java 和 C# 的两个方案出现了
- Java 提出了默认方法,即在接口中添加默认实现
- C# 提出了扩展方法,即通过改变静态方法的调用形式来假装是对象调用
不得不说 C# 的扩展方法很聪明,但它毕竟不是真正对接口进行扩展,所以在 C# 8 中也加入了默认方法来解决接口扩展造成的问题。
接口扩展方法提出来之后,虽然解决了默认实现的问题,却又带出了新的问题。
- 接口实现了默认方法,实现接口的类还需要实现吗?如果不实现会怎么样?
- 无论 Java 还是 C# 都不允许类多继承,但是接口可以。而接口中的默认实现带来了类似于类多继承所产生的问题,怎么办?
- 在复杂的实现和继承关系中,最终执行的到底会是哪一个方法?
3. 问题一,默认方法和类实现方法的关系
忽略上面 IStringList
接口中补充的 Insert(Object, int)
方法,我们把关注点放在 IndexOf(Object)
上。Java 和 C# 的语法异曲同工:
3.1. 先来看看默认方法的语法
- Java 版
interface StringList { void add(Object s); void remove(int i); Object get(int i); int getLength(); default int indexOf(Object s) { for (int i = 0; i < getLength(); i++) { if (get(i) == s) { return i; } } return -1; } }
- C# 版
interface IStringList { public void Add(string s); void Remove(int i); string Get(int i); int Length { get; } int IndexOf(string s) { for (var i = 0; i < Length; i++) { if (Get(i) == s) { return i; } } return -1; } }
这里把 C# 和 Java 的接口都写出来,主要是因为二者讲法和命名规范略有不同。接下来进行的研究 C# 和 Java 行为相似的地方,就主要以 C# 为例了。
怎么区分是 C# 示例还是 Java 示例?看代码规范,最明显的是 C# 方法用 Pascal 命名规则,Java 方法用 camel 命名规则。当然,还有 Lambda 的箭头也不一样。
接下来的实现,仅以 C# 为例:
class MyList : IStringList { List<string> list = new List<string>(); // 偷懒用现成的 public int Length => list.Count; public void Add(string o) => list.Add(o); public string Get(int i) => list[i]; public void Remove(int i) => list.RemoveAt(i); }
MyList
没有实现 IndexOf
,但是使用起来不会有任何问题
class Program { static void Main(string[] args) { IStringList myList = new MyList(); myList.Add("First"); myList.Add("Second"); myList.Add("Third"); Console.WriteLine(myList.IndexOf("Third")); // 输出 2 Console.WriteLine(myList.IndexOf("first")); // 输出 -1,注意 first 大小写 } }
3.2. 在 MyList 中实现 IndexOf
现在,在 MyList
中添加 IndexOf
,实现对字符串忽略大小写的查找:
// 这里用 partial class 表示是部分实现, // 对不住 javaer,Java 没有部分类语法 partial class MyList { public int IndexOf(string s) { return list.FindIndex(el => { return el == s || (el != null && el.Equals(s, StringComparison.OrdinalIgnoreCase)); }); } }
然后 Main
函数中输出的内容变了
Console.WriteLine(myList.IndexOf("Third")); // 还是返回 2 Console.WriteLine(myList.IndexOf("first")); // 返回 0,不是 -1
显然这里调用了 MyList.IndexOf()
。
3.3. 结论,以及 Java 和 C# 的不同之处
上面主要是以 C# 作为示例,其实 Java 也是一样的。上面的示例中是通过接口类型来调用的 IndexOf
方法。第一次调用的是 IStringList.IndexOf
默认实现,因为这时候 MyList
并没有实现 IndexOf
;第二次调用的是 MyList.IndexOf
实现。笔者使用 Java 写了类似的代码,行为完全一致。
因此,对于默认方法,会优先调用类中的实现,如果类中没有实现具有默认方法的接口,才会去调用接口中的默认方法。
但是!!!前面的示例是使用的接口类型引用实现,如果换成实例类类型来引用实例呢?
如果 MyList
中实现了 IndexOf
,那结果没什么区别。但是如果 MyList
中没有实现 IndexOf
的时候,Java 和 C# 在处理上有就区别了。
先看看 C# 的 Main
函数,编译不过(Compiler Error CS1929),因为 MyList
中没有定义 IndexOf
。
而 Java 呢?通过了,一如既往的运行出了结果!
从 C# 的角度来看,MyList
既然知道有 IndexOf
接口,那就应该实现它,而不能假装不知道。但是如果通过 IStringList
来调用 IndexOf
,那么就可以认为 MyList
并不知道有 IndexOf
接口,因此允许调用默认接口。接口还是接口,不知道有新接口方法,没实现,不怪你;但是你明知道还不实现,那就是你的不对了。
但从 Java 的角度来看,MyList
的消费者并不一定是 MyList
的生产者。从消费者的角度来看,MyList
实现了 StringList
接口,而接口定义有 indexOf
方法,所以消费者调用 myList.indexOf
是合理的。
Java 的行为相对宽松,只要有实现你就用,不要管是什么实现。
而 C# 的行为更为严格,消费者在使用的时候可以通过编译器很容易了解到自己使用的是类实现,还是接口中的默认实现(虽然知道了也没多少用)。实际上,如果没在在类里面实现,接口文档中就不会写出来相关的接口,编辑器的智能提示也不会弹出来。实在要写,可以显示转换为接口来调用:
Console.WriteLine(((IStringList)myList).IndexOf("Third"));
而且根据上面的试验结果,将来 MyList
实现了 IndexOf
之后,这样的调用会直接切换到调用 MyList
中的实现,不会产生语义上的问题。
4. 问题二,关于多重继承的问题
无论 Java 还是 C# 都不允许类多继承,但是接口可以。而接口中的默认实现带来了类似于类多继承所产生的问题,怎么办?
举个例,人可以走,鸟也可以走,那么“云中君”该怎么走?
4.1. 先来看 C# 的
类中不实现默认接口的情况:
interface IPerson { void Walk() => Console.WriteLine("IPerson.Walk()"); } interface IBird { void Walk() => Console.WriteLine("IBird.Walk()"); } class BirdPerson : IPerson, IBird { }
调用结果:
BirdPerson birdPerson = new BirdPerson(); // birdPerson.Walk(); // CS1061,没有实现 Walk ((IPerson)birdPerson).Walk(); // 输出 IPerson.Walk() ((IBird)birdPerson).Walk(); // 输出 IBird.Walk()
不能直接使用 birdPerson.Walk()
,道理前面已经讲过。不过通过不同的接口类型来调用,行为是不一致的,完全由接口的默认方法来决定。这也可以理解,既然类没有自己的实现,那么用什么接口来引用,说明开发者希望使用那个接口所规定的默认行为。
说得直白一点,你把云中君看作人,他就用人的走法;你把云中君看作鸟,它就用鸟的走法。
然而,如果类中有实现,情况就不一样了:
class BirdPerson : IPerson, IBird { // 注意这里的 public 可不能少 public void Walk() => Console.WriteLine("BirdPerson.Walk()"); }
BirdPerson birdPerson = new BirdPerson(); birdPerson.Test(); // 输出 BirdPerson.Walk() ((IPerson)birdPerson).Walk(); // 输出 BirdPerson.Walk() ((IBird)birdPerson).Walk(); // 输出 BirdPerson.Walk()
输出完全一致,接口中定义的默认行为,在类中有实现的时候,就当不存在!
云中君有个性:不管你怎么看,我就这么走。
这里唯一需要注意的是 BirdPerson
中实现的 Walk()
必须声明为 public
,否则 C# 会把它当作类的内部行为,而不是实现的接口行为。这一点和 C# 对实现接口方法的要求是一致的:实现接口成员必须声明为 public
。
4.2. 接着看 Java 的不同
转到 Java 这边,情况就不同了,编译根本不让过
interface Person { default void walk() { out.println("IPerson.walk()"); } } interface Bird { default void walk() { out.println("Bird.walk()"); } } // Duplicate default methods named walk with the parameters () and () // are inherited from the types Bird and Person class BirdPerson implements Person, Bird { }
这个意思就是,Person
和 Bird
都为签名相同的 walk
方法定义了默认现,所以编译器不知道 BirdPerson
到底该怎么办了。那么如果只有一个 walk
有默认实现呢?
interface Person { default void walk() { out.println("IPerson.walk()"); } } interface Bird { void walk(); } // The default method walk() inherited from Person conflicts // with another method inherited from Bird class BirdPerson implements Person, Bird { }
这意思是,两个接口行为不一致,编译器还是不知道该怎么处理 BirdPerson
。
总之,不管怎么样,就是要 BirdPerson
必须实现自己的 walk()
。既然 BirdPerson
自己实现了 walk()
,那调用行为也就没有什么悬念了:
BirdPerson birdPerson = new BirdPerson(); birdPerson.walk(); // 输出 BirdPerson.walk() ((Person) birdPerson).walk(); // 输出 BirdPerson.walk() ((Bird) birdPerson).walk(); // 输出 BirdPerson.walk()
4.3. 结论,多继承没有问题
如果一个类实现的多个接口中定义了相同签名的方法,没有默认实现的情况下,当然不会有问题。
如果类中实现了这个签名的方法,那无论如何,调用的都是这个方法,也不会有问题。
但在接口有默认实现,而类中没有实现的情况下,C# 将实际行为交给引用类型去处理;Java 则直接报错,交给开发者去处理。笔者比较赞同 C# 的做法,毕竟默认方法的初衷就是为了不强制开发者去处理增加接口方法带来的麻烦。
5. 问题三,更复杂的情况怎么去分析
对于更复杂的情况,多数时候还是可以猜到会怎么去调用的,毕竟有个基本原则在那里。
5.1. 在类中的实现优先
比如,WalkBase
定义了 Walk()
方法,但没实现任何接口,BirdPerson
从 WalkBase
继承,实现了 IPerson
接口,但没实现 Walk()
方法,那么该执行哪个 Walk
呢?
会执行 WalkBase.Walk()
——不管什么情况下,类方法优先!
class WalkBase { public void Walk() => Console.WriteLine("WalkBase.Walk()"); } class BirdPerson : WalkBase, IPerson { } static void Main(string[] args) { BirdPerson birdPerson = new BirdPerson(); birdPerson.Walk(); // 输出 WalkBase.Walk() ((IPerson)birdPerson).Walk(); // 输出 WalkBase.Walk() }
如果父类子类都有实现,但子类不是“重载”,而是“覆盖”实现,那要根据引用类型来找最近的类,比如
class WalkBase : IBird // <== 注意这里实现了 IBird { public void Walk() => Console.WriteLine("WalkBase.Walk()"); } class BirdPerson : WalkBase, IPerson // <== 这里_没有_实现 IBird { // 注意:这里是 new,而不是 override public new void Walk() => Console.WriteLine("BirdPerson.Walk()"); } static void Main(string[] args) { BirdPerson birdPerson = new BirdPerson(); birdPerson.Walk(); // 输出 BirdPerson.Walk() ((WalkBase)birdPerson).Walk(); // 输出 WalkBase.Walk() ((IPerson)birdPerson).Walk(); // 输出 BirdPerson.Walk() ((IBird)birdPerson).Walk(); // 输出 WalkBase.Walk() }
如果 WalkBase
中以 virtual
定义 Walk()
,而 BirdPerson
中以 override
定义 Walk()
,那毫无悬念输出全都是 BirdPerson.Walk()
。
class WalkBase : IBird { public virtual void Walk() => Console.WriteLine("WalkBase.Walk()"); } class BirdPerson : WalkBase, IPerson { public override void Walk() => Console.WriteLine("BirdPerson.Walk()"); } static void Main(string[] args) { BirdPerson birdPerson = new BirdPerson(); birdPerson.Walk(); // 输出 BirdPerson.Walk() ((WalkBase)birdPerson).Walk(); // 输出 BirdPerson.Walk() ((IPerson)birdPerson).Walk(); // 输出 BirdPerson.Walk() ((IBird)birdPerson).Walk(); // 输出 BirdPerson.Walk() }
上面示例中的候最后一句输出,是通过 IBird.Walk()
找到 WalkBase.Walk()
,而 WalkBase.Walk()
又通过虚方法链找到 BirdPerson.Walk()
,所以输出仍然是 BirdPerson.Walk()
。学过 C++ 的同学这时候可能就会很有感觉了!
至于 Java,所有方法都是虚方法。虽然可以通过 final
让它非虚,但是在子类中不能定义相同签名的方法,所以 Java 的情况会更简单一些。
5.2. 类中无实现,根据引用类型找最近的默认实现
还是拿 WalkBase
和 BirdPerson
分别实现了 IBird
和 IPerson
的例子,
class WalkBase : IBird { } class BirdPerson : WalkBase, IPerson { } ((IPerson)birdPerson).Walk(); // 输出 IPerson.Walk() ((IBird)birdPerson).Walk(); // 输出 IBird.Walk()
哦,当然 Java 中不存在,因为编译器会要求必须实现 BirdPerson.Walk()
。
5.3. 如何还有更复杂的情况
讲真,如果真的还有更复杂的情况,我建议还是做做实验吧!
6. 慎用默认方法
默认方法的出现有其历史原因,所以在设计一个新库的时候,最好不要过早考虑默认方法这个问题。如果真的有需要实现的默认行为,可能还是抽象的基类更适合一些。
但是,如果设计出来的类和接口关系确实非常复杂,甚至需要类似多重继承的关系,那么适当的考虑一下默认方法也未尝不可。
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
研究揭露AMD处理器存在漏洞,可致两种新型侧信道攻击
根据最新发布的研究,从2011年至2019年,AMD处理器一直存在以前从未公开的漏洞,这些漏洞使它们容易受到两种新型的侧信道攻击。 这种新型的潜在攻击手段被称为“Take A Way”,利用AMD Bulldozer微体系结构中的L1数据(L1D)缓存预测变量,从处理器获取敏感数据,并在恢复加密过程中使用密钥,这对设备安全产生极大的威胁。 这项研究由格拉茨技术大学和计算机科学与随机系统研究所(IRISA)的一个研究团队发表,他们于2019年8月向AMD披露了这些漏洞。 AMD在其网站上发布的一份咨询报告中说: “我们注意到一份新的白皮书,该白皮书声称可以利用AMD CPU的潜在漏洞,恶意行为者借此可以操纵与缓存相关的功能,从而可能以意想不到的方式传输用户数据。” “然后,研究人员将该数据路径与现有的恢复软件或推测的侧通道漏洞进行配对。AMD认为这些不是新型攻击。” 尽管该通知并未具体说明防范攻击的细节,但该论文的主要研究人员之一Vedad Hadžić表示,该漏洞利用活动仍然很活跃。 随着英特尔CPU审查出一系列漏洞问题,从Meltdown,Spectre,ZombieLoad到最近无...
- 下一篇
云备份软件 v1.2 升级,可备份到自建 SFTP
更新日志 1. 增加SFTP支持,可以将文件备份到自建SFTP服务器 2.修复华为云 isUsable() 函数,如果填写错误,运行时无错误提示问题 3. 优化华为云OBS联网异常、授权异常 4.增加日志面板中,如果备份异常,在状态label中显示 5.版本更新增加 http 200 判断拦截 6.mac系统,优化右上角窗口关闭按钮,点击后弹出提醒 7.更新版本检测及windows自动更新机制, windows 64 的更新,不需要重新下载,在线升级。 yunbackups 云备份软件,纯Java编写,代码开源。提供命令行方式、以及可视化界面两种方式运行。用户可以将自己电脑、或手机、或服务器等文件,进行备份。 目前可以将备份的文件存放到华为云对象存储(归档存储)、以及FTP进行存储。
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Linux系统CentOS6、CentOS7手动修改IP地址
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- SpringBoot2全家桶,快速入门学习开发网站教程
- CentOS8编译安装MySQL8.0.19
- CentOS7,CentOS8安装Elasticsearch6.8.6
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,CentOS7官方镜像安装Oracle11G
- Windows10,CentOS7,CentOS8安装Nodejs环境
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池