因此,默认方法是……昨天的新闻,对不对? 是的,但是使用一年后,积累了很多事实,我想将这些事实收集在一个地方,供刚开始使用它们的开发人员使用。 甚至有经验的人都可以找到他们不知道的一两个细节。
如果有新发现,我将在将来扩展此职位。 因此,我要我的读者(是的,你们两个!)向我提供有关默认方法的每个小事实,您在这里找不到这些方法。 如果您有任何东西,请鸣叫 , 发邮件或发表评论。
总览
我想我没有给这篇文章一个有意义的叙述。 原因在于,从本质上讲,这是一篇Wiki文章。 它涵盖了默认方法的不同概念和细节,尽管它们与自然相关,但它们并不适合进行连续叙述。
但这也有好处! 您可以轻松地在文章中跳来跳去,而不会大大降低您的阅读体验。 查看目录,以全面了解所涵盖的内容,并了解您的好奇心将您带到何处。
默认方法
到现在为止,大多数开发人员将已经使用,阅读甚至实现默认方法,因此,我将为每个人省去语法的详细介绍。 在介绍更广泛的概念之前,我将花更多时间在它的细节上。
句法
默认方法的新语言功能归结为接口现在可以声明非抽象方法,即带有主体的方法。
以下示例是JDK 8中Comparator.thenComparing(Comparator)( link )的修改版:
比较器中的默认方法
default Comparator<T> thenComparing(Comparator<? super T> other) {return (o1, o2) -> {int res = this.compare(o1, o2);return (res != 0) ? res : other.compare(o1, o2);};
}
除了关键字default
之外,这看起来像一个“常规”方法声明。 将这样的方法添加到接口而没有编译错误并提示方法调用解决策略是必要的。
现在,实现Comparator
每个类都将包含thenComparing(Comparator)
公共方法,而不必自己实现-可以这么说,它是免费的。
显式调用默认方法
在下面的内容中,我们将看到一些人们可能想要从某个特定的超级接口显式调用方法的默认实现的原因。 如果有需要,请按照以下步骤进行:
明确调用默认实现
class StringComparator implements Comparator<String> {// ...@Overridepublic Comparator<String> thenComparing(Comparator<? super String> other) {log("Call to 'thenComparing'.");return Comparator.super.thenComparing(other);}
}
请注意,接口的名称如何用于指定以下super
,否则将引用超类(在本例中为Object
)。 从句法上讲,这类似于可以从嵌套类访问对外部类的引用 。
解决策略
因此,让我们考虑一个使用默认方法实现接口的类型的实例。 如果调用存在默认实现的方法会怎样? (请注意,方法由其签名标识,该签名包括名称和参数类型。)
规则1 :
- 类胜过接口。 如果超类链中的一个类具有方法的声明(具体的或抽象的),则说明操作已完成,默认设置无关紧要。
规则2 :- 更加具体的接口将胜过较不具体的接口(此处的特异性表示“子类型化”)。 无论
规则3 :List
和Collection
在何处,如何或多少次进入继承图,List
的默认值都将胜过Collection
的默认值。- 没有规则#3。 如果根据上述规则没有唯一的获胜者,则具体班级必须手动消除歧义。
Brian Goetz – 2013年3月3日(格式化我的)
首先,这阐明了为什么将这些方法称为默认方法以及为什么必须使用关键字default
启动它们:
这样的实现是备用的,以防万一一个类并且其超类都不考虑该方法,即不提供任何实现,也不将其声明为抽象(请参见规则#1 )。 等效地,仅在类未实现扩展X
并声明相同方法的接口Y
(默认或抽象;请参见规则#2 )时,才使用接口X
的默认方法。
尽管这些规则很简单,但是它们并不能阻止开发人员创建复杂的情况。 这篇文章提供了一个示例,其中分辨率的预测并非微不足道,并提出了应谨慎使用此功能的说法。
解决策略包含一些有趣的细节……
解决冲突
规则#3 ,或者说它的缺失,意味着具体的类必须实现存在竞争默认实现的每个方法。 否则,编译器将引发错误。 如果有竞争的实现之一是合适的,则方法主体可以显式调用该方法 。
这也意味着向接口添加默认实现会导致编译错误。 如果一个类A
实现无关接口X
和Y
并且其已经存在于默认方法X
被添加到Y
,类A
将不再编译。
如果未A
, X
和Y
一起编译,并且JVM偶然发现这种情况,会发生什么情况? 有趣的问题, 答案似乎还不清楚 。 看起来JVM将抛出IncompatibleClassChangeError。
重新提取方法
如果抽象类或接口A
某个方法声明为抽象,而某些超级接口X
存在默认实现,则X
的默认实现将被覆盖。 因此,所有子类A
具体类都必须实现该方法。 这可以用作强制执行不合适的默认实现的有效工具。
此技术在整个JDK中使用,例如在ConcurrentMap
( link )上使用,该方法重新抽象了Map
( link )提供默认实现的许多方法,因为它们不是线程安全的(搜索术语“不合适的默认值”)。
请注意,具体类仍可以选择显式调用重写的默认实现 。
“对象”上的覆盖方法
接口不可能为Object
的方法提供默认实现。 尝试这样做将导致编译错误。 为什么?
首先,这将是无用的。 由于每个类都继承自Object
,因此规则1明确表示永远不会调用这些方法。
但是那条规则不是自然法则,专家组本可以例外。 邮件中还包含规则, Brian Goetz给出了为什么没有规则的许多原因 。 我最喜欢的一个(格式化我的):
从根本
toString
,来自Object
的方法(例如toString
,equals
和hashCode
)都是关于对象的state的 。 但是接口没有状态。 类有状态。 这些方法属于拥有对象状态的代码-类。
修饰符
请注意,有很多修饰符不能在默认方法上使用:
- 可见性固定为公开(与其他界面方法一样)
- 关键字
synchronized
禁止(如在抽象方法) - 关键字
final
是被禁止的(与抽象方法一样)
当然,需要这些功能,并且对它们的缺失进行了全面的解释(例如,针对final和synced )。 参数总是相似的:这不是默认方法的目的 ,引入这些功能将导致更复杂且易于出错的语言规则和/或代码。
不过,您可以使用static
,这将减少对复数形式的实用程序类的需求 。
一点上下文
现在我们已经知道了如何使用默认方法,下面将这些知识放到上下文中。
接口演变
经常可以找到介绍默认方法的专家组,他们指出他们的目标是允许“接口演变”:
默认方法 […]的目的是使接口在首次发布后能够以兼容的方式进行开发。
Brian Goetz – 2013年9月
在使用默认方法之前,几乎不可能(不包括某些组织模式;请参阅此概述 )在不破坏所有实现的情况下向接口添加方法。 尽管这对于控制这些实现的绝大多数软件开发人员都无关紧要,但对于API设计人员而言,这是一个至关重要的问题。 Java始终保持安全,在发布接口后也从未更改过接口。
但是随着引入lambda表达式,这变得难以忍受。 想象一下总是编写Stream.of(myList).forEach(...)
的集体痛苦,因为无法将forEach
添加到List
。
因此,引入了lambdas的专家组决定寻找一种方法,以在不破坏任何现有实现的情况下实现接口演化。 他们对这一目标的关注解释了默认方法的特征 。
在小组认为有可能不降低该主要用例的可用性的情况下,他们还启用了使用默认方法来创建特征的方法,或者甚至是接近它们的方法。 尽管如此,他们还是因为没有“一路走到”混合蛋白和特质而经常受到攻击,对此人们经常重复回答:“是的,因为那不是我们的目标。”
实用工具类
JDK以及特别常见的辅助库(例如Guava和Apache Commons)充满了实用程序类。 它们的名称通常是为其提供方法的接口的复数形式,例如Collections或Sets 。 它们存在的主要原因是那些实用程序方法在发布后不能添加到原始接口中。 使用默认方法,这成为可能。
现在,所有将接口实例作为参数的静态方法都可以转换为接口上的默认方法。 例如,查看静态Collections.sort(List)
( link ),从Java 8开始,它静态地委派给新实例默认方法List.sort(Comparator)
( link )。 我的帖子中给出了另一个示例, 说明如何使用默认方法来改进装饰器模式 。 其他不带参数的实用程序方法(通常是生成器)现在可以成为接口上的静态默认方法。
虽然可以在代码库中删除所有与接口相关的实用程序类,但建议不要这样做。 界面的可用性和内聚性应该仍然是主要优先事项,而不是在其中填充所有可想象的功能。 我的猜测是,只有将这些方法中最通用的方法移到接口上,而在一个(或多个?)实用工具类中可以保留更多晦涩的操作,这才有意义。 (或完全删除它们 ,如果您愿意的话。)
分类
在对新Javadoc标签的争论中,Brian Goetz对迄今为止已引入JDK(格式化我的格式)的默认方法进行了弱分类:
1.可选方法 :
- 这是默认实现几乎不符合的情况,例如Iterator中的以下内容:
2.具有合理默认值的方法,但可能会被足够关注的实现所覆盖 :default void remove() {throw new UnsupportedOperationException("remove"); }
它遵守合同,因为合同显然是薄弱的,但是任何关心删除的类肯定会覆盖它。
- 例如,再次从Iterator中进行:
3.几乎没有人会覆盖它们的方法 :default void forEach(Consumer<? super E> consumer) {while (hasNext())consumer.accept(next()); }
对于大多数实现来说,这种实现是完美的,但是如果某些类(例如
ArrayList
)的维护者有足够的动力去做的话,可能会有做得更好的机会。Map
上的新方法(例如putIfAbsent
)也在此存储桶中。- 如谓词中的此方法:
default Predicate<T> and(Predicate<? super T> p) {Objects.requireNonNull(p);return (T t) -> test(t) && p.test(t); }
Brian Goetz – 2013年1月31日
我将此分类称为“弱”分类,因为它自然缺乏关于在何处放置方法的硬性规定。 但是,这并没有使它无用。 恰恰相反,我认为这对交流它们有很大帮助,在阅读或编写默认方法时要牢记一件好事。
文献资料
请注意,默认方法是引入新的(非正式)Javadoc标记@apiNote , @implSpec和@implNote的主要原因 。 JDK经常使用它们,因此了解它们的含义很重要。 了解它们的一个好方法是阅读我的上一篇文章 (平稳,对吗?),其中详细介绍了它们。
继承与建立类
继承的不同方面以及如何使用它来构建类经常在关于默认方法的讨论中出现。 让我们仔细看看它们,看看它们与新语言功能的关系。
多重继承-什么?
通过继承,一个类型可以假定另一个类型的特征。 存在三种特征:
- 类型 ,即通过子类型化类型是另一种类型
- 行为 ,即一种类型继承方法,因此其行为与另一种类型相同
- state ,即一个类型继承了定义另一个类型状态的变量
由于类是其父类的子类型,并且继承了所有方法和变量,因此类继承显然涵盖了所有这三个特征。 同时,一个类只能扩展另一个类,因此仅限于单一继承。
接口是不同的:一个类型可以继承许多接口,并成为每个接口的子类型。 因此,从第一天开始,Java就一直支持这种多重继承。
但是在Java 8之前,实现类仅继承了接口的类型。 是的,它也继承了合同,但没有继承其实际实施,因此它必须提供自己的行为。 使用默认方法时,此更改发生了变化,因此从Java的版本8开始,还支持行为的多重继承。
Java仍然没有提供明确的方法来继承多种类型的状态。 但是,使用恶意方法或虚拟字段模式可以通过默认方法实现类似的效果。 前者很危险,不应该使用,后者也有一些缺点(特别是在封装方面),应谨慎使用。
默认方法与混合和特质
在讨论默认方法时,有时会将它们与mixins和traits进行比较。 本文无法详细介绍这些内容,但将粗略介绍它们与具有默认方法的接口的区别。 (可以在StackOverflow上找到mixins和trait的有用比较。)
混合蛋白
Mixins允许继承其类型,行为和状态。 一个类型可以从多个mixin继承,从而提供所有三个特征的多重继承。 根据语言的不同,也许还可以在运行时将混合添加到单个实例。
由于具有默认方法的接口不允许状态的继承,因此显然它们不是mixin。
特质
与mixin相似,特征允许类型(和实例)从多个特征继承。 它们还继承了它们的类型和行为,但是与混合混合不同,常规特征没有定义自己的状态。
这使得特征类似于具有默认方法的接口。 概念仍然不同,但是这些差异并非完全无关紧要。 将来我可能会再次讨论并进行更详细的比较,但是在那之前,我将为您提供一些建议:
- 如我们所见, 方法调用解析并不总是那么琐碎,这会使快速使用默认方法的不同接口的交互成为复杂性的负担。 性状通常以一种或另一种方式缓解此问题。
- 特性允许Java不完全支持的某些操作。 请参阅有关特质的Wikipedia文章中的“选择操作”后的项目符号列表。
- 论文“ Java 8中的面向特征的编程”探讨了使用默认方法的面向特征的编程风格,并遇到了一些问题。
因此,尽管具有默认方法的接口没有任何特征,但是相似性允许像以前那样以有限的方式使用它们。 这与专家组的设计目标一致, 该目标试图在不与原始目标冲突的地方适应这种用例,即界面的发展和易用性。
默认方法与抽象类
现在,接口可以提供行为,它们可以进入抽象类的领域,很快就会出现问题,可以在给定的情况下使用。
语言差异
首先让我们说明一下语言层面的一些差异:
尽管接口允许多重继承,但它们基本上在类构建的其他各个方面都达不到要求。 默认方法永远不会是最终方法,无法同步并且不能覆盖Object
的方法。 它们始终是公共的,这严重限制了编写简短且可重用方法的能力。 此外,接口仍无法定义字段,因此每个状态更改都必须通过公共API进行。 为适应该用例而对API进行的更改通常会破坏封装。
尽管如此,仍然存在一些用例,其中这些差异无关紧要,并且两种方法在技术上都是可行的。
概念差异
然后是概念上的差异。 类定义什么是什么,而接口通常定义什么可以做 。
抽象类是完全特殊的东西。 有效的Java项目18全面解释了为什么接口在定义具有多个子类型的类型时优于抽象类。 (而且这甚至没有考虑默认方法。)要点是:抽象类对于接口的骨架(即部分)实现有效,但如果没有匹配的接口就不应存在。
因此,当有效地将抽象类简化为低可见性,接口的基本实现时,默认方法是否也可以消除这种情况? 决定: 不! 实现接口几乎总是需要某些或所有默认方法所缺少的类构建工具。 而且,如果某些界面没有,那显然是一种特殊情况,这不会使您误入歧途。 (有关使用默认方法实现接口时可能发生的情况,请参见此早期文章 。)
更多连结
- Lambda状态的最终版本(第10章介绍了默认方法)
- 官方教程
- 关于如何发展接口的官方教程
- JavaCodeGeeks教程
- DZone教程
反射
这篇文章应该都谈过了一个需要了解的默认方法。 如果您不同意,请鸣叫 , 发邮件或发表评论。 批准和+1也可以接受。
翻译自: https://www.javacodegeeks.com/2015/02/everything-you-need-to-know-about-default-methods.html