Java 8中,接口是否可以有效替代工具类?

28
在过去的十年左右,我一直在为我的Java实用类使用以下模式。该类仅包含静态方法和字段,被声明为final以便不能被继承,并且具有private构造函数以便不能被实例化。
public final class SomeUtilityClass {
    public static final String SOME_CONSTANT = "Some constant";

    private SomeUtilityClass() {}

    public static Object someUtilityMethod(Object someParameter) {
        /* ... */

        return null;
    }
}

现在,随着Java 8中引入接口静态方法,我最近发现自己正在使用一个实用的接口模式:

(了解更多)
public interface SomeUtilityInterface {
    String SOME_CONSTANT = "Some constant";

    static Object someUtilityMethod(Object someParameter) {
        /* ... */

        return null;
    }
}

这使我能够摆脱构造函数,以及在接口中隐含的许多关键字(publicstaticfinal)。

这种方法有什么缺点吗?与使用实用程序接口相比,是否有任何好处?


1
@SotiriosDelimanolis:它将类的实现细节放入类的公共API中。我认为静态导入存在的一个具体原因是让人们不太倾向于这样做。 Oracle关于静态导入的文档特别指出常量接口反模式是一个非常糟糕的想法。 - user2357112
1
我还不是完全信服……如果接口没有公开任何抽象方法的话。但实际上这并不重要,因为这些方法并没有被继承。我不会真的在只有字段的情况下费力去做那些事情。 - Sotirios Delimanolis
2
正如其创造常量接口反模式的人(Joshua Bloch)所说:“API 应易于使用且难以误用。它应该容易做简单的事情;可以完成复杂的事情;而且做错误的事情是不可能或至少很困难的”。通过我使用一些主流使用常量接口的 API 的经验(例如 ASM),我总是发现实现一个接口(来自 ASM 的操作码)比编写静态导入更容易。我猜这取决于你对设计的严格程度。 - Dioxin
1
@Holger 我不需要也不想要构造函数,但如果我省略私有构造函数的声明,那么就会得到默认的公共构造函数,从而允许实例化类。对于接口来说则不是这种情况(但我无法防止它被类、匿名或其他方式实现)。我从未建议将 enum 用作实用程序类。我已经滥用了它们来创建我的单例(在极少数情况下)。 - Robby Cornelissen
2
请注意,您可以通过在 interface 中添加一个包含非“public”类型引用的抽象方法来防止对 interface 的任意实现。这样,包外的实现就是不可能的。然而,这并不会提高清晰度,因为抽象方法将出现在 public API 中,开发人员会开始想知道该方法的目的,并且当他们尝试实现接口时发现不可能时,他们会认为一定有错误。 - Holger
显示剩余12条评论
4个回答

11
只有当你预期有人实现它时,才应该使用接口。例如,java.util.stream.Stream接口有一堆静态方法,这些方法在Java 8之前可能位于某个StreamsStreamUtils类中。然而,它是一个有效的接口,也有非静态方法可以被实现。 java.util.Comparable是另一个例子:所有静态方法只支持该接口。你不能禁止用户实现你的公共接口,但对于实用程序类,你可以禁止他们实例化它。因此出于代码清晰性的考虑,建议除非打算实现接口,否则不要使用接口。
关于 @saka1029 的回答,需要说明的是,虽然不能在同一个接口中定义帮助器私有方法和常量,但创建一个包私有类在同一个包中,例如MyInterfaceHelper,并将所有必要的与实现相关的内容放入其中,也不是问题。通常情况下,包私有类可以很好地隐藏你的实现细节。

我也认为代码的清晰易懂确实是一个有效的关注点。 - Robby Cornelissen
1
顺便提一下,Java 9允许在接口中使用private方法,因此很好的是你的答案不依赖于该特性的缺失,即使有了这个特性,你的答案仍然有效。 - Holger
1
Java 9 应该引入私有方法 http://openjdk.java.net/jeps/213 - assylias
1
@Holger 哈哈 - 你比我快了12秒,在帖子发布后5个小时...我想知道那种情况发生的概率是多少 :-) - assylias
1
@assylias:我先用了一个jdk快照版本试了一下。所以编写一个小接口和运行javac的时间决定了我发送评论的时间... - Holger

6
根据反对常量接口模式的人所说,即使您不打算让客户端实现接口,仍然有可能(甚至更容易)实现它,因此不应该允许使用。API应该易于使用且难以误用。简单的事情应该很容易做到;复杂的事情可能会有所难度;而错误的事情则应该是不可能或至少很困难的。尽管如下所述,这确实取决于目标受众。许多易于使用的设计模式都受到了批评(例如上下文模式、单例模式、常量接口模式)。甚至像迪米特法则这样的设计原则也因过于冗长而受到批评。我不想这么说,但这些决策都是基于观点的。尽管上下文模式被视为反模式,但在主流框架(如Spring和Android SDK)中,它显而易见。这归结于环境和目标受众。我能找到的主要缺点是在“缺点”下的第三个列表中,在Constant Interface wiki中列出:如果未来版本需要二进制代码兼容性,则常量接口必须永远保持为接口(不能转换为类),即使它没有像传统意义上的接口那样被使用。如果您认为“嘿,这实际上不是一个合同,我想要强制执行更强的设计”,则无法更改它。但是正如我所说,这取决于您;也许您将来不会介意更改它。此外,正如@TagirValeev所提到的那样,代码清晰度也很重要。接口的意图是被实现的;如果您不希望其他人实现您提供的API,则不要使其可实现。但我认为这与“目标受众”声明有关。老实说,我赞同您使用更简洁的基础知识,但这取决于我的代码面向的受众;我不想在可能会受到审查的代码中使用常量接口。

2
你不应该使用接口。 接口不能有私有常量和静态初始化器。
public class Utility {

    private Utility() {}

    public static final Map<String, Integer> MAP_CONSTANT;
    static {
        Map<String, Integer> map = new HashMap<>();
        map.put("zero", 0);
        map.put("one", 1);
        map.put("three", 3);
        MAP_CONSTANT = Collections.unmodifiableMap(map);
    }

    private static String PRIVATE_CONSTANT = "Hello, ";

    public static String hello(String name) {
        return PRIVATE_CONSTANT + name;
    }
}

2
首先,不应该有全局可变状态(这就是为什么单例模式是反面模式的原因)。但我认为这并不足够好的理由。 - Dioxin

1

我认为这个方法会起作用。我认为变量SOME_CONSTANT在您的SomeUtilityInterface中默认为静态常量,即使您没有明确说明。因此,它可以作为一个实用程序工作,但是您使用所有成员变量必须是最终的常规类时,可能会有一些可变性问题吗?只要这不是默认方法特定实现的问题,我想不到任何问题。


1
它绝对有效。而且接口字段隐式地是publicstaticfinal,所以我认为没有任何可变性问题。 - Robby Cornelissen
通常情况下,final成员只能在构造函数中被修改(而不是在包含方法中)。那么,在实现类的构造函数中它们可以被设置吗?你试过了吗? - djangofan
1
最终成员可以在声明或构造函数中初始化,但只能初始化一次。由于接口中的字段默认为“public static final”,并且它们已在声明时初始化,我不明白您所描述的如何实现。 - Robby Cornelissen
是的,我认为这不是一个好主意。我通常使用Spring自动装配实用程序类和实例成员,因此将其作为常规类处理效果非常好。 - djangofan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接