【问题标题】:Java default interface methods concrete use casesJava 默认接口方法具体用例
【发布时间】:2017-09-25 11:08:36
【问题描述】:

Java 9 即将到来,Java 接口将添加更多功能,例如私有方法。接口中的 default 方法是在 Java 8 中添加的,基本上是在 support the use of lambdas inside collections 中添加的,而不会破坏与该语言先前版本的复古兼容性。

在 Scala 中,traits 中的方法非常有用。然而,Scala 处理traits 的方法与处理Java 的default 方法不同。考虑多重继承解决方案或将traits 用作mixins。

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

【问题讨论】:

  • 我认为有很多设计模式可以在接口中进一步使用。例如:使用静态工厂方法归档工厂模式,使用默认方法实现模板方法模式,我介绍了一个装饰器模式在我的回答中,等等。
  • "你确定静态工厂方法吗?" - 在我的回答中已经有一个静态工厂方法will(Answer)
  • @holi-java 是的,确实如此,你是对的。函数式编程和 OO 设计模式的问题是,有时函数式编程会隐式地使用许多 OO 设计模式。请参阅this question
  • @FedericoPeraltaSchaffner 感谢您的反馈。所以这个问题没有答案,因为它太大而无法回答。
  • @riccardo.cardin 所以您希望这个问题及其答案变成类似博客文章的内容吗?如果是,我会添加一些用例。

标签: design-patterns interface java-8 java-9 default-method


【解决方案1】:

使用默认方法移除监听器的默认适配器

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

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

interface WindowListener extends EventListener {

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

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

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

【讨论】:

    【解决方案2】:

    Brian Goetz 和我在我们的 JavaOne 2015 演讲中介绍了其中的一些内容,使用 Java 8 Lambda 和 Streams 进行 API 设计。尽管标题如此,但最后还是有一些关于默认方法的材料。 p>

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

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

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

    界面演变

    默认方法的主要用例是接口演化。主要是,这是在不破坏向后兼容性的情况下向接口添加方法的能力。如问题中所述,这主要用于添加允许将 Collections 转换为 Streams 的方法以及将基于 lambda 的 API 添加到 Collections。

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

    可选方法

    有时接口方法在逻辑上是“可选的”。例如,考虑不可变集合上的 mutator 方法。当然,实现是必需的,但通常在这种情况下它会做的是抛出异常。这可以通过默认方法轻松完成。如果他们不想提供异常抛出方法,实现可以继承它,或者如果他们想提供一个实现,他们可以覆盖它。示例:Iterator.remove

    便捷方法

    有时为了调用者的方便提供了一个方法,并且有一个明显的优化的实现。此实现可以由默认方法提供。实现覆盖默认值是合法的,但通常没有理由,因此实现通常会继承它。示例:Comparator.reversedSpliterator.getExactSizeIfKnownSpliterator.hasCharacteristics。请注意,Spliterator 是在 Java 8 中引入的,包括默认方法,因此这显然不是接口演变的情况。

    简单的实现,旨在被覆盖

    默认方法可以提供适用于所有实现的简单通用实现,但这可能不是最理想的。这有助于在初始启动期间实现,因为它们可以继承默认值并确保正确操作。但是,从长远来看,实现可能需要覆盖默认值并提供改进的自定义实现。

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

    现在,很明显 sort 在 Java 8 中被改造为 ListArrayList,所以进化并没有以这种方式发生。但是你可以很容易地想象出一个新的List 实现。您最初可能会继承 sort 默认实现,而您正在正确实现基础知识。稍后,您可能会考虑实施一种定制的排序算法,该算法已针对您的新实施的内部数据组织进行了调整。

    【讨论】:

    • 默认接口是否违反接口隔离原则?
    【解决方案3】:

    嗯,我有一个真实世界的场景,我在其中使用过它们。以下是上下文:我从google maps api(通过提供纬度和经度)以Array 的结果形式获得结果,如下所示:

    GeocodingResult[] result
    

    该结果包含一些我需要的信息,例如 zip-codelocalitycountry。不同的服务需要响应的不同部分。该数组的解析是一样的——你只需要搜索不同的部分。

    所以我在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
    

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

    【讨论】:

    • @riccardo.cardin 正是我这样写时的想法。此外,如果有意义的话,我会将代码 close 放在需要的地方。但这当然不能滥用。
    • @riccardo.cardin 和 Eugene,这对我来说似乎是一种静态方法。它根据其他参数解析第一个参数。可以完美地转移到实用程序类。我没有看到在默认方法中使用此功能的好处。
    • @FedericoPeraltaSchaffner 静态方法不支持多态性,当您想要有几个 parseResult 变体时。
    • @FedericoPeraltaSchaffner 应避免使用具有静态方法的实用程序类。它们带有过程编程的味道,并且难以作为依赖项进行管理(想想单元测试)。我将 Eugene 示例视为所谓的 companion types 的演变。
    • @riccardo.cardin 我并不是说我们应该在所有类型的实用程序类中使用静态方法。我的观点是,这个默认方法的例子让我想起了这一点。不过,我想你说服了我。我认为这种方法更像是特征。
    【解决方案4】:

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

    @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
    

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

    您可以使用默认方法来装饰父接口(正如@holi-java 在他的回答中解释的那样),还有很多适配器模式的示例(currying 和 binding 实际上是适配器)。


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

    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
        }
    }
    

    现在,每当您的类需要记录某些内容并报告某些指标时,您都可以使用:

    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);
        }
    }
    

    同样,这不是最好的实现,只是示例代码以了解如何通过默认方法使用特征。

    【讨论】:

    • 很抱歉我离开了一段时间。我喜欢咖喱区。在第二部分中,我喜欢使用策略而不是过程方法。
    • @holi-java 是的,也许不是最好的例子,我也会使用策略而不是 if 语句...
    【解决方案5】:

    使用默认方法装饰函数接口链接

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

    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));
    

    这是我昨天的答案:Mockito returnsFirstArg() to use。由于Answer没有链式方法,所以我引入另一个Answer类型AnswerPipeline来提供链式方法。

    AnswerPipeline 类

    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));
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-08-16
      • 2019-11-14
      • 2019-04-02
      • 2018-06-13
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-02-24
      相关资源
      最近更新 更多