Java 默认接口方法具体用例
Java default interface methods concrete use cases
Java 9 即将到来,更多功能将添加到 Java 接口,如私有方法。 default
接口中的方法已在 Java 8 中添加,本质上是为了 support the use of lambdas inside collections 而不会破坏与该语言先前版本的追溯兼容性。
在 Scala 中,trait
中的方法非常有用。但是,Scala 处理 trait
的方法与使用 default
方法处理 Java 的方法不同。考虑多重继承解析或使用 trait
s 作为 mixins.
除了上述用途,在哪些真实场景中使用default
方法是值得的?这些年来是否出现了一些使用它们的模式?使用这种方法可以解决哪些问题?
使用默认方法修饰函数接口链接
有时我想链接 @FunctionalInterface,我们已经在 Function 中看到了链接函数的默认方法,例如:compose
、andThen
使代码更优雅。最重要的是我们以后可以重用部分函数,例如:
Predicate<?> isManager = null;
Predicate<?> isMarried = null;
marriedManager = employeeStream().filter(isMarried.and(isManager));
unmarriedManager = employeeStream().filter(isMarried.negate().and(isManager));
但是,有时我们无法链接@FunctionalInterface,因为它没有提供任何链接方法。但我可以写另一个 @FunctionalInterface 扩展原始的并添加一些默认方法用于链接目的。例如:
when(myMock.myFunction(anyString()))
.then(will(returnsFirstArg()).as(String.class).to(MyObject::new));
这是我昨天的回答:。由于 Answer
没有链式方法,所以我引入另一种 Answer
类型 AnswerPipeline
来提供链式方法。
AnswerPipeline class
interface AnswerPipeline<T> extends Answer<T> {
static <R> AnswerPipeline<R> will(Answer<R> answer) {
return answer::answer;
}
default <R> AnswerPipeline<R> as(Class<R> type) {
return to(type::cast);
}
default <R> AnswerPipeline<R> to(Function<T, R> mapper) {
return it -> mapper.apply(answer(it));
}
}
首先想到的是使用默认方法来支持一些函数式编程技术:
@FunctionalInterface
public interface Function3<A, B, C, D> {
D apply(A a, B b, C c);
default Function<A, Function<B, Function<C, D>>> curry() {
return a -> b -> c -> this.apply(a, b, c);
}
default Function<B, Function<C, D>> bindFirst(A a) {
return b -> c -> this.apply(a, b, c);
}
}
示例用法:
Function3<Long, Long, Long, Long> sum = (a, b, c) -> a + b + c;
long result = sum.apply(1L, 2L, 3L); // 6
Function<Long, Function<Long, Function<Long, Long>>> curriedSum = sum.curry();
result = curriedSum.apply(1L).apply(2L).apply(3L); // 6
Function<Long, Function<Long, Long>> incr = sum.bindFirst(1L);
result = incr.apply(7L).apply(3L); // 11
result = incr.apply(6L).apply(7L); // 14
您可以对其他参数使用类似的绑定方法,使用默认方法实现,例如 bindSecond
和 bindThird
。
你可以使用默认方法来装饰父接口(正如@holi-java在他的回答中解释的那样),还有很多适配器模式的例子(柯里化和绑定实际上是适配器)。
除了函数式编程,你还可以使用默认方法来支持种,有限的多重继承:
public interface Animal {
String getHabitat();
}
public interface AquaticAnimal extends Animal {
@Override
default String getHabitat() {
return "water";
}
}
public interface LandAnimal extends Animal {
@Override
default String getHabitat() {
return "ground";
}
}
public class Frog implements AquaticAnimal, LandAnimal {
private int ageInDays;
public Frog(int ageInDays) {
this.ageInDays = ageInDays;
}
public void liveOneDay() {
this.ageInDays++;
}
@Override
public String getHabitat() {
if (this.ageInDays < 30) { // is it a tadpole?
return AquaticAnimal.super.getHabitat();
} // else
return LandAnimal.super.getHabitat();
}
}
样本:
Frog frog = new Frog(29);
String habitatWhenYoung = frog.getHabitat(); // water
frog.liveOneDay();
String habitatWhenOld = frog.getHabitat(); // ground
也许不是最好的例子,但你明白了......
另一种用法是特征:
public interface WithLog {
default Logger logger() {
return LoggerFactory.getLogger(this.getClass());
}
}
public interface WithMetrics {
default MetricsService metrics() {
return MetricsServiceFactory.getMetricsService(
Configuration.getMetricsIP(
Environment.getActiveEnv())); // DEV or PROD
}
}
现在,每当您 class 需要记录某些内容并报告某些指标时,您可以使用:
public class YourClass implements WithLog, WithMetrics {
public void someLongMethod() {
this.logger().info("Starting long method execution...");
long start = System.nanoTime();
// do some very long action
long end = System.nanoTime();
this.logger().info("Finished long method execution");
this.metrics().reportExecutionTime("Long method: ", end - start);
}
}
同样,这不是最好的实现,只是示例代码以了解如何通过默认方法使用特征。
好吧,我有一个使用过它们的真实场景。这是上下文:我从 google maps api
(通过提供纬度和经度)以 Array
结果的形式得到结果,看起来像这样:
GeocodingResult[] result
该结果包含一些我需要的信息,例如 zip-code
或 locality
或 country
。不同的服务需要该响应的不同部分。该数组的解析是相同的 - 您只需要搜索不同的部分。
所以我在 interface
中的 default
方法中定义了它:
default Optional<String> parseResult(
GeocodingResult[] geocodingResults,
AddressComponentType componentType,// enum
AddressType addressType) { // enum
... Some parsing functionality that returns
city, address or zip-code, etc
}
现在在接口的实现中我就是用这个方法。
class Example implements Interface {
@Override
public Optional<String> findZipCode(Double latitude, Double longitude) {
LatLng latLng = new LatLng(latitude, longitude);
return parseResult(latLng,
AddressComponentType.POSTAL_CODE,
AddressType.POSTAL_CODE);
}
.. other methods that use the same technique
这个曾经通过摘要类来完成。我本可以使用私有方法,但是这个接口被许多其他服务使用。
Brian Goetz 和我在我们的 Java2015 年一次演讲中介绍了其中的一些内容,API 使用 Java 8 Lambda 和 Streams 进行设计。 尽管有标题,但最后有一些关于默认方法的 material。
幻灯片:https://stuartmarks.files.wordpress.com/2015/10/con6851-api-design-v2.pdf
视频:https://youtu.be/o10ETyiNIsM?t=24m
我将在这里总结一下我们所说的关于默认方法的内容。
界面进化
默认方法的主要用例是接口演化。主要是,这是在不破坏向后兼容性的情况下向接口添加方法的能力。如问题中所述,这最突出地用于添加允许将 Collections 转换为 Streams 的方法,并将 lambda-based APIs 添加到 Collections.
不过还有其他几个用例。
可选方法
有时接口方法在逻辑上是"optional"。例如,考虑不可变 collections 上的增变器方法。当然,一个实现是必需的,但通常在这种情况下它会做的是抛出一个异常。这可以通过默认方法轻松完成。如果实现不想提供它,可以继承 exception-throwing 方法,或者如果它们想提供实现,可以覆盖它。示例:Iterator.remove
.
便捷方法
有时为了调用者的方便而提供了一种方法,并且有明显的优化实现。该实现可以由默认方法提供。实现覆盖默认值是合法的,但通常没有理由,因此实现通常会继承它。示例:Comparator.reversed
, Spliterator.getExactSizeIfKnown
, Spliterator.hasCharacteristics
. Note that Spliterator
是在 Java 8 中引入的,包括默认方法,因此这显然不是接口演变的情况。
简单实现,旨在被覆盖
默认方法可以提供适用于所有实现的简单、通用的实现,但这可能不是最优的。这有助于初始 bring-up 期间的实施,因为它们可以继承默认值并确保正确操作。但是,从长远来看,实现可能希望覆盖默认值并提供改进的自定义实现。
示例:List.sort
. The default implementation copies the list elements to a temporary array, sorts the array, and copies the elements back to the list. This is a correct implementation, and sometimes it can't be improved upon (e.g. for LinkedList
). However, ArrayList
overrides sort
并对其内部数组 in-place 进行排序。这避免了复制开销。
现在,显然 sort
在 Java 8 中被改装到 List
和 ArrayList
上,所以进化不是这样发生的。但是您可以很容易地想象提出一个新的 List
实现。在正确实现基础知识的同时,您可能最初会继承 sort
默认实现。稍后,您可能会考虑实施定制的排序算法,该算法已针对新实施的内部数据组织进行调整。
使用默认方法删除监听器的默认适配器
有时候,我们需要引入一个defaultAdapter
class,因为java.util.EventListener
有多个事件需要触发,但是我们只对某些事件感兴趣。例如:swing create each *Adapter
class for each *Listener
.
我最近发现这在我们使用默认方法声明侦听器时非常有用,我们可以删除中间适配器 class。例如:
interface WindowListener extends EventListener {
default void windowOpened(WindowEvent e) {/**/}
default void windowClosing(WindowEvent e) {/**/}
default void windowClosed(WindowEvent e) {/**/}
}
Java 9 即将到来,更多功能将添加到 Java 接口,如私有方法。 default
接口中的方法已在 Java 8 中添加,本质上是为了 support the use of lambdas inside collections 而不会破坏与该语言先前版本的追溯兼容性。
在 Scala 中,trait
中的方法非常有用。但是,Scala 处理 trait
的方法与使用 default
方法处理 Java 的方法不同。考虑多重继承解析或使用 trait
s 作为 mixins.
除了上述用途,在哪些真实场景中使用default
方法是值得的?这些年来是否出现了一些使用它们的模式?使用这种方法可以解决哪些问题?
使用默认方法修饰函数接口链接
有时我想链接 @FunctionalInterface,我们已经在 Function 中看到了链接函数的默认方法,例如:compose
、andThen
使代码更优雅。最重要的是我们以后可以重用部分函数,例如:
Predicate<?> isManager = null;
Predicate<?> isMarried = null;
marriedManager = employeeStream().filter(isMarried.and(isManager));
unmarriedManager = employeeStream().filter(isMarried.negate().and(isManager));
但是,有时我们无法链接@FunctionalInterface,因为它没有提供任何链接方法。但我可以写另一个 @FunctionalInterface 扩展原始的并添加一些默认方法用于链接目的。例如:
when(myMock.myFunction(anyString()))
.then(will(returnsFirstArg()).as(String.class).to(MyObject::new));
这是我昨天的回答:Answer
没有链式方法,所以我引入另一种 Answer
类型 AnswerPipeline
来提供链式方法。
AnswerPipeline class
interface AnswerPipeline<T> extends Answer<T> {
static <R> AnswerPipeline<R> will(Answer<R> answer) {
return answer::answer;
}
default <R> AnswerPipeline<R> as(Class<R> type) {
return to(type::cast);
}
default <R> AnswerPipeline<R> to(Function<T, R> mapper) {
return it -> mapper.apply(answer(it));
}
}
首先想到的是使用默认方法来支持一些函数式编程技术:
@FunctionalInterface
public interface Function3<A, B, C, D> {
D apply(A a, B b, C c);
default Function<A, Function<B, Function<C, D>>> curry() {
return a -> b -> c -> this.apply(a, b, c);
}
default Function<B, Function<C, D>> bindFirst(A a) {
return b -> c -> this.apply(a, b, c);
}
}
示例用法:
Function3<Long, Long, Long, Long> sum = (a, b, c) -> a + b + c;
long result = sum.apply(1L, 2L, 3L); // 6
Function<Long, Function<Long, Function<Long, Long>>> curriedSum = sum.curry();
result = curriedSum.apply(1L).apply(2L).apply(3L); // 6
Function<Long, Function<Long, Long>> incr = sum.bindFirst(1L);
result = incr.apply(7L).apply(3L); // 11
result = incr.apply(6L).apply(7L); // 14
您可以对其他参数使用类似的绑定方法,使用默认方法实现,例如 bindSecond
和 bindThird
。
你可以使用默认方法来装饰父接口(正如@holi-java在他的回答中解释的那样),还有很多适配器模式的例子(柯里化和绑定实际上是适配器)。
除了函数式编程,你还可以使用默认方法来支持种,有限的多重继承:
public interface Animal {
String getHabitat();
}
public interface AquaticAnimal extends Animal {
@Override
default String getHabitat() {
return "water";
}
}
public interface LandAnimal extends Animal {
@Override
default String getHabitat() {
return "ground";
}
}
public class Frog implements AquaticAnimal, LandAnimal {
private int ageInDays;
public Frog(int ageInDays) {
this.ageInDays = ageInDays;
}
public void liveOneDay() {
this.ageInDays++;
}
@Override
public String getHabitat() {
if (this.ageInDays < 30) { // is it a tadpole?
return AquaticAnimal.super.getHabitat();
} // else
return LandAnimal.super.getHabitat();
}
}
样本:
Frog frog = new Frog(29);
String habitatWhenYoung = frog.getHabitat(); // water
frog.liveOneDay();
String habitatWhenOld = frog.getHabitat(); // ground
也许不是最好的例子,但你明白了......
另一种用法是特征:
public interface WithLog {
default Logger logger() {
return LoggerFactory.getLogger(this.getClass());
}
}
public interface WithMetrics {
default MetricsService metrics() {
return MetricsServiceFactory.getMetricsService(
Configuration.getMetricsIP(
Environment.getActiveEnv())); // DEV or PROD
}
}
现在,每当您 class 需要记录某些内容并报告某些指标时,您可以使用:
public class YourClass implements WithLog, WithMetrics {
public void someLongMethod() {
this.logger().info("Starting long method execution...");
long start = System.nanoTime();
// do some very long action
long end = System.nanoTime();
this.logger().info("Finished long method execution");
this.metrics().reportExecutionTime("Long method: ", end - start);
}
}
同样,这不是最好的实现,只是示例代码以了解如何通过默认方法使用特征。
好吧,我有一个使用过它们的真实场景。这是上下文:我从 google maps api
(通过提供纬度和经度)以 Array
结果的形式得到结果,看起来像这样:
GeocodingResult[] result
该结果包含一些我需要的信息,例如 zip-code
或 locality
或 country
。不同的服务需要该响应的不同部分。该数组的解析是相同的 - 您只需要搜索不同的部分。
所以我在 interface
中的 default
方法中定义了它:
default Optional<String> parseResult(
GeocodingResult[] geocodingResults,
AddressComponentType componentType,// enum
AddressType addressType) { // enum
... Some parsing functionality that returns
city, address or zip-code, etc
}
现在在接口的实现中我就是用这个方法。
class Example implements Interface {
@Override
public Optional<String> findZipCode(Double latitude, Double longitude) {
LatLng latLng = new LatLng(latitude, longitude);
return parseResult(latLng,
AddressComponentType.POSTAL_CODE,
AddressType.POSTAL_CODE);
}
.. other methods that use the same technique
这个曾经通过摘要类来完成。我本可以使用私有方法,但是这个接口被许多其他服务使用。
Brian Goetz 和我在我们的 Java2015 年一次演讲中介绍了其中的一些内容,API 使用 Java 8 Lambda 和 Streams 进行设计。 尽管有标题,但最后有一些关于默认方法的 material。
幻灯片:https://stuartmarks.files.wordpress.com/2015/10/con6851-api-design-v2.pdf
视频:https://youtu.be/o10ETyiNIsM?t=24m
我将在这里总结一下我们所说的关于默认方法的内容。
界面进化
默认方法的主要用例是接口演化。主要是,这是在不破坏向后兼容性的情况下向接口添加方法的能力。如问题中所述,这最突出地用于添加允许将 Collections 转换为 Streams 的方法,并将 lambda-based APIs 添加到 Collections.
不过还有其他几个用例。
可选方法
有时接口方法在逻辑上是"optional"。例如,考虑不可变 collections 上的增变器方法。当然,一个实现是必需的,但通常在这种情况下它会做的是抛出一个异常。这可以通过默认方法轻松完成。如果实现不想提供它,可以继承 exception-throwing 方法,或者如果它们想提供实现,可以覆盖它。示例:Iterator.remove
.
便捷方法
有时为了调用者的方便而提供了一种方法,并且有明显的优化实现。该实现可以由默认方法提供。实现覆盖默认值是合法的,但通常没有理由,因此实现通常会继承它。示例:Comparator.reversed
, Spliterator.getExactSizeIfKnown
, Spliterator.hasCharacteristics
. Note that Spliterator
是在 Java 8 中引入的,包括默认方法,因此这显然不是接口演变的情况。
简单实现,旨在被覆盖
默认方法可以提供适用于所有实现的简单、通用的实现,但这可能不是最优的。这有助于初始 bring-up 期间的实施,因为它们可以继承默认值并确保正确操作。但是,从长远来看,实现可能希望覆盖默认值并提供改进的自定义实现。
示例:List.sort
. The default implementation copies the list elements to a temporary array, sorts the array, and copies the elements back to the list. This is a correct implementation, and sometimes it can't be improved upon (e.g. for LinkedList
). However, ArrayList
overrides sort
并对其内部数组 in-place 进行排序。这避免了复制开销。
现在,显然 sort
在 Java 8 中被改装到 List
和 ArrayList
上,所以进化不是这样发生的。但是您可以很容易地想象提出一个新的 List
实现。在正确实现基础知识的同时,您可能最初会继承 sort
默认实现。稍后,您可能会考虑实施定制的排序算法,该算法已针对新实施的内部数据组织进行调整。
使用默认方法删除监听器的默认适配器
有时候,我们需要引入一个defaultAdapter
class,因为java.util.EventListener
有多个事件需要触发,但是我们只对某些事件感兴趣。例如:swing create each *Adapter
class for each *Listener
.
我最近发现这在我们使用默认方法声明侦听器时非常有用,我们可以删除中间适配器 class。例如:
interface WindowListener extends EventListener {
default void windowOpened(WindowEvent e) {/**/}
default void windowClosing(WindowEvent e) {/**/}
default void windowClosed(WindowEvent e) {/**/}
}