关于设计模式:Java默认接口方法的具体用例

Java default interface methods concrete use cases

Java 9即将到来,更多的特性将被添加到Java接口,比如私有方法。在Java 8中添加了接口中的EDCOX1和0种方法,本质上是为了支持LAMBDAS在集合内的使用,而不破坏与以前版本的语言的反向兼容性。

在scala中,trait中的方法非常有用。然而,斯卡拉用EDCX1和0种方法治疗EDCX1 1比S的方法不同。考虑多重继承解决或使用trait作为混合。

除上述用途外,使用default方法的真实场景有哪些价值?在这几年中,是否出现了一些使用它们的模式?使用这种方法可以解决哪些问题?


Brian Goetz和我在JavaOne 2015 Talk,API设计中用Java 8 lambda和流覆盖了一些。尽管有这个标题,但在最后还有一些关于默认方法的材料。

幻灯片:https://stuartmarks.files.wordpress.com/2015/10/con6851-api-design-v2.pdf

视频:https://youtu.be/o10etynism?t=24m

我将在这里总结一下我们所说的默认方法。

界面演化

默认方法的主要用例是接口演进。主要地,这是在不破坏向后兼容性的情况下向接口添加方法的能力。正如问题中提到的,这是最显著的用于添加方法,允许将集合转换为流,并将基于lambda的API添加到集合。

不过,还有其他几个用例。

可选方法

有时接口方法在逻辑上是"可选的"。例如,考虑对不可变集合使用mutator方法。当然,实现是必需的,但在这种情况下,它通常要做的是抛出一个异常。这可以用默认方法轻松完成。如果不想提供异常引发方法,则实现可以继承该方法;如果想提供实现,则实现可以重写该方法。示例:Iterator.remove

方便方法

有时,为了方便调用者,提供了一种方法,并且有一个明显的最佳实现。此实现可以由默认方法提供。实现重写默认值是合法的,但通常没有理由,因此实现通常会继承它。示例:Comparator.reversedSpliterator.getExactSizeIfKnownSpliterator.hasCharacteristics。请注意,在Java 8中引入了EDCOX1×4的含义,包括默认方法,因此这显然不是界面演化的情况。

简单实现,旨在重写

默认方法可以提供一个简单、通用的实现,适用于所有实现,但这可能是次优的。这有助于在初始启动期间实现,因为它们可以继承默认值并确保正确操作。但是,从长远来看,实现可能希望覆盖默认值,并提供一个改进的、定制的实现。

示例:List.sort。默认实现将列表元素复制到临时数组,对数组进行排序,并将元素复制回列表。这是一个正确的实现,有时无法改进(例如,对于LinkedList)。但是,ArrayList重写sort并对其内部数组进行适当排序。这样可以避免复制开销。

显然,sort在JAVA 8中被改写为EDCX1、10和EDOCX1×7,因此没有发生这种演变。但是,您可以很容易地想象提出一个新的List实现。在正确实现基础知识的同时,您可能首先继承了sort默认实现。稍后,您可能会考虑实现一个定制的排序算法,该算法被调到新实现的内部数据组织中。


首先想到的是使用默认方法来支持一些函数式编程技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
@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);
    }
}

样品使用情况:

1
2
3
4
5
6
7
8
9
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

您可以对其他参数使用类似的绑定方法,使用默认方法实现,如bindSecondbindThird

您可以使用默认方法来装饰父接口(如在他的回答中解释的"HORIJAVA"),也有许多适配器模式的示例(Currand and Band实际上是适配器)。

除了函数式编程,您还可以使用默认方法来支持某种有限的多重继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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();
    }
}

Sample:

1
2
3
4
5
6
Frog frog = new Frog(29);

String habitatWhenYoung = frog.getHabitat(); // water

frog.liveOneDay();
String habitatWhenOld = frog.getHabitat(); // ground

也许不是最好的例子,但你知道…

另一种用法是特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
    }
}

现在,只要您有一个类需要记录一些东西并报告一些度量,就可以使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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的结果,如下所示:

1
GeocodingResult[] result

这个结果包含了一些我需要的信息,比如zip-codelocalitycountry。不同的服务需要该响应的不同部分。该数组的解析是相同的-您只需要搜索不同的部分。

所以我在interface内部的default方法中定义了:

1
2
3
4
5
6
7
8
default Optional<String> parseResult(
        GeocodingResult[] geocodingResults,
        AddressComponentType componentType,// enum
        AddressType addressType) { // enum

     ... Some parsing functionality that returns
      city, address or zip-code, etc
}

现在在接口的实现中,我只使用这个方法。

1
2
3
4
5
6
7
8
9
10
11
12
 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

这以前是通过抽象类完成的。我本可以使用私有方法,但这个接口被许多其他服务使用。


使用默认方法删除侦听器的默认适配器

有时,我们需要为java.util.EventListener引入一个默认的Adapter类,它需要触发多个事件,但我们只对其中一些事件感兴趣。例如:Swing为每个*Listener创建每个*Adapter类。

我最近发现,当我们用默认方法声明侦听器时,这非常有用,我们可以删除中间适配器类。例如:

1
2
3
4
5
6
7
8
interface WindowListener extends EventListener {

    default void windowOpened(WindowEvent e) {/**/}

    default void windowClosing(WindowEvent e) {/**/}

    default void windowClosed(WindowEvent e) {/**/}
}

用默认方法修饰函数接口链接

我有时想链接@functionalinterface,我们已经在函数中看到了链接函数的默认方法,例如:composeandThen以使代码更优雅。最重要的是我们可以稍后重用部分函数,例如:

1
2
3
4
5
Predicate<?> isManager = null;
Predicate<?> isMarried = null;

marriedManager = employeeStream().filter(isMarried.and(isManager));
unmarriedManager = employeeStream().filter(isMarried.negate().and(isManager));

但是,有时我们不能链接@functionalinterface,因为它没有提供任何链方法。但我可以编写另一个@functionalinterface来扩展原始的方法,并添加一些默认方法来链接。例如:

1
2
when(myMock.myFunction(anyString()))
       .then(will(returnsFirstArg()).as(String.class).to(MyObject::new));

这是我昨天的答案:mockito returnsfirstarg()使用。由于Answer没有链方法,所以我引入另一个AnswerAnswerPipeline来提供链方法。

AnswerPipeline类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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));
    }
}