几周前,我写了一个博客,说开发人员学习新语言是因为它们很酷。 我仍然坚持这个主张,因为关于Java 8的事情真的很酷。 毫无疑问,该节目的明星是添加了Lambdas以及将函数提升为一等变量,而我目前最喜欢的是默认方法。 这是因为它们是在不破坏旧代码的情况下向现有接口添加新功能的一种巧妙方法。
实现很简单:采用一个接口,添加一个具体方法,并将关键字default
附加为修饰符。 结果是,接口的所有现有实现突然都可以使用此代码。 在第一个简单示例中,我添加了默认方法,该方法返回接口1的版本号。
public interface Version {
/**
* Normal method - any old interface method:
*
* @return Return the implementing class's version
*/
public String version();
/**
* Default method example.
*
* @return Return the version of this interface
*/
default String interfaceVersion() {
return "1.0";
}
}
然后,您可以在任何实现类上调用此方法。
public class VersionImpl implements Version {
@Override
public String version() {
return "My Version Impl";
}
}
您可能会问:为什么这很酷? 如果采用java.lang.Iterable接口并添加以下默认方法,则会使for
循环失效。
default void forEach(Consumer<? super T> action) {
Objects.requireNonNull(action);
for (T t : this) {
action.accept(t);
}
}
forEach
方法采用实现Consumer<T>
接口作为参数的类的实例。 Consumer<T>
可以在新的java.util.function
包中找到,它是Java 8所谓的功能接口 ,该接口仅包含一个方法。 在这种情况下,方法accept(T t)
接受一个参数并且返回一个void
。
java.util.function
软件包可能是Java 8中最重要的软件包之一。它包含一堆描述通用函数类型的单一方法或函数接口。 例如, Consumer<T>
包含一个接受一个参数并返回void
的函数,而Predicate<T>
是一个包含一个接受一个参数并返回boolean
的函数的接口,通常用于编写过滤lambda。
该接口的实现应包含您先前在for循环括号之间编写的内容。
那么,您可能会想,这给了我什么? 如果不是Java 8,那么答案是“不多”。 要在Java 8之前使用forEach(…)方法,您需要编写如下代码:
List<String> list = Arrays.asList(new String[] { "A", "FirsT", "DefaulT", "LisT" });
System.out.println("Java 6 version - anonymous class");
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String t) {
System.out.println(t);
}
};
list.forEach(consumer);
但是,如果将其与lambda表达式或方法引用结合使用,则可以编写一些看起来很酷的代码。 使用方法引用,前面的示例变为:
list.forEach(System.out::println);
您可以使用lambda表达式执行相同的操作:
list.forEach((t) -> System.out.println(t));
所有这些似乎都与Java 8背后的一个重要思想保持一致:让JDK为您完成工作。 用政治家和连环友约翰·肯尼迪(John F Kennedy)的话来形容“不要问您对JDK可以做什么,请问您的JDK可以为您做什么” 2 。
默认方法的设计问题
那是编写无处不在的for
循环的一种很酷的新方法,但是在接口中添加默认方法是否存在问题?如果是的话,它们是什么?Java 8项目中的人如何修复它们?
首先要考虑的是继承。 当您拥有一个扩展了另一个接口的接口并且两个接口都具有带有相同签名的默认方法时,会发生什么? 例如,如果您拥有由MiddleInterface
扩展的SubInterface
和由SuperInterface
扩展的MiddleInterface
, SubInterface
怎么SubInterface
?
public interface SuperInterface {
default void printName() {
System.out.println("SUPERINTERFACE");
}
}
public interface MiddleInterface extends SuperInterface {
@Override
default void printName() {
System.out.println("MIDDLEINTERFACE");
}
}
public interface SubInterface extends MiddleInterface {
@Override
default void printName() {
System.out.println("SUBINTERFACE");
}
}
public class Implementation implements SubInterface {
public void anyOldMethod() {
// Do something here
}
public static void main(String[] args) {
SubInterface sub = new Implementation();
sub.printName();
MiddleInterface middle = new Implementation();
middle.printName();
SuperInterface sup = new Implementation();
sup.printName();
}
}
无论用哪种方式剪切, printName()
都将始终打印“ SUBINTERFACE”。
当您具有包含相同方法签名的类和接口时,会出现相同的问题:哪个方法在运行? 答案是“阶级胜利”法则。 接口默认方法将始终被类方法所忽略。
public interface AnyInterface {
default String someMethod() {
return "This is the interface";
}
}
public class AnyClass implements AnyInterface {
@Override
public String someMethod() {
return "This is the class - WINNING";
}
}
运行上面的代码将始终打印出:“这是课程-WINNING”
最后,如果一个类实现两个接口并且都包含具有相同签名的方法,会发生什么? 这是古老的C ++钻石问题 ; 您如何解决歧义? 运行哪种方法?
public interface SuperInterface {
default void printName() {
System.out.println("SUPERINTERFACE");
}
}
public interface AnotherSuperInterface {
default void printName() {
System.out.println("ANOTHERSUPERINTERFACE");
}
}
在Java 8的情况下,答案都不是。 如果您尝试同时实现这两个接口,则会收到以下错误:
Duplicate default methods named printName with the parameters () and () are inherited from the types AnotherSuperInterface and SuperInterface.
在绝对必须实现两个接口的情况下,解决方案是调用“类获胜”规则并覆盖实现中的歧义方法。
public class Diamond implements SuperInterface, AnotherSuperInterface {
/** Added to resolve ambiguity */
@Override
public void printName() {
System.out.println("CLASS WINS");
}
public static void main(String[] args) {
Diamond instance = new Diamond();
instance.printName();
}
}
何时使用默认方法
从纯粹的角度来看,默认方法的添加意味着Java接口不再是接口。 接口被设计为用于拟议/预期行为的规范或合同:实施类必须履行的合同。 添加默认方法意味着接口和抽象基类之间实际上没有区别3 。 这意味着他们容易受到滥用,因为一些经验不足的开发人员可能认为从其代码库中删除基类并用基于默认方法的接口替换它们很酷–只是因为它们可以,而其他人可能只是将抽象类与实现默认值的接口混淆了方法。 我目前建议仅将默认方法用于其预期的使用情况:在不破坏现有代码的情况下改进传统接口。 虽然我可能会改变主意。
1它不是很有用,但是它说明了一点……
2肯尼迪(John F Kennedy)的就职演说1961年1月20日。
3抽象基类可以具有构造函数,而接口则不能。 类可以具有私有实例变量(即状态)。 接口不能。
翻译自: https://www.javacodegeeks.com/2014/08/default-methods-java-8s-unsung-heros.html