接口常量有什么用途?

163

我正在学习Java,刚刚发现接口可以有字段,这些字段是公共静态和不可变的。到目前为止,我还没有看到任何关于这些的例子。这些接口常量的用例是什么,我能在Java标准库中看到一些吗?

13个回答

239
将静态成员放入接口中(并实现该接口)是一种不好的做法,甚至有一个名称叫做“常量接口反模式”,参见Effective Java,第17项:
引用块: 常量接口模式是接口的不良使用。一个类内部使用一些常量是实现细节。实现常量接口会导致这个实现细节泄漏到类的导出API中。对于类的用户来说,类是否实现了常量接口并不重要。事实上,这可能会让他们感到困惑。更糟糕的是,它代表了一种承诺:如果在未来的版本中,类被修改以不再需要使用常量,则仍必须实现接口以确保二进制兼容性。如果非最终类实现常量接口,则其所有子类的命名空间都将被接口中的常量污染。
Java平台库中有几个常量接口,例如java.io.ObjectStreamConstants。这些接口应被视为异常情况,不应模仿。
为避免常量接口的一些缺陷(因为无法防止人们实现它),应优先选择具有私有构造函数的适当类(示例借自Wikipedia)。
public final class Constants {

    private Constants() {
        // restrict instantiation
    }

    public static final double PI = 3.14159;
    public static final double PLANCK_CONSTANT = 6.62606896e-34;
}

如果不想完全限定常量(即不想在常量前面加上类名前缀),可以使用静态导入(自Java 5起):

import static Constants.PLANCK_CONSTANT;
import static Constants.PI;

public class Calculations {

    public double getReducedPlanckConstant() {
        return PLANCK_CONSTANT / (2 * PI);
    }
}

13
好的,但是如果你有一个不仅用于常量定义的接口呢?我有一些接口被许多类实现,它包含方法声明,但我还想添加一些常见值,例如大小。在这种情况下,这真的是一个糟糕的模式吗?总的来说,我同意仅为常量创建接口是反模式。 - Łukasz Rzeszotarski
21
只因为书上说了并不意味着它是坏事。只要你不实现那个接口来访问那些常量,就没问题。 - ACV
23
不,不,不。《Effective Java》中的那个引用是关于其他事情的!创建仅包含常量的接口是比包含这些常量的类更好的选择。 《Effective Java》说:“一个类内部使用一些常量是实现细节”。但这里不是这种情况。我们正在谈论“全局”常量。而且谁会想要实现一个没有方法声明的接口呢? - ACV
9
我同意 ACV 的观点。如果常量接口不是导出的 API 模块的一部分,或者它没有被实现,我并不认为存在问题。使用 const final 类很丑陋:你需要一个私有构造函数,这只会让代码变得混乱而毫无用处。 - Lawrence
3
这个回答没有正确呈现《Effective Java》一书的主要观点,因为它通过将主要观点放在括号中来“降低”了该观点的重要性:“(并实现该接口)”。相反,它强调了接口中的常量(除非您实现这样的接口,否则它们是可以接受的)。这不是一个完全错误的答案,因为如果你仔细阅读引用的片段,你就可以看到《Effective Java》的作者最初想表达的意思。但我认为这是误导人的。在我看来,“并实现该接口”这部分应该以粗体字突出显示。 - krm
显示剩余4条评论

33

"常量接口模式是接口的不良使用"

无论这个假设的提出者是多么牛的大师,他/她都是基于需要继续高效地实现不良习惯和实践而构建这个假设的。这个假设基于推广不良软件设计习惯的有效性。

我在这里写了一篇回应反驳这个假设:什么是在Java中实现常量的最佳方式? 解释了这个假设毫无根据的基础。

这个问题已经持续了10年,直到我发表不支持这个误导性假设的理由后,只用了2小时就被关闭了,从而暴露了那些深深坚信这个错误假设的人们不愿意进行辩论。

以下是我对这个假设的表达:

  • 持有这个假设的基础是需要方法和严格规则来应对不良软件习惯和方法所带来的影响。

  • 这种观点的支持者无法提供除了那些受到不良习惯和实践影响的必要性之外的其他原因。

  • 解决根本问题。

  • 然后,为什么不充分利用和开发Java语言结构的每一个语言特性来方便自己。无需夹克。为什么要发明规则来对抗你们无效的生活方式,歧视和指责更有效的生活方式?

根本问题

是信息组织。在工程或补充过程的解决方案之前,应该首先理解信息中介进程和行为,以及所谓的业务规则。几十年前这种信息组织方法被称为数据归一化。

只有将解决方案的组件的粒度和模块化与信息的组件的粒度和模块化对齐才是最佳策略,从而使解决方案的工程化成为可能。

组织信息存在两三个重要障碍:

  1. 缺乏对数据模型“规范化”需求的感知。

  2. EF Codd关于数据规范化的陈述是有缺陷、有缺陷和含糊不清的。

  3. 最新的敏捷工程伪装成不需要提前计划和条件模块组织的错误观念,因为你可以在进行重构时进行。使用会计技巧延迟利润和资产化,因此将过程信息的基本发现视为不需要现在处理的东西。

使用接口常量是良好的实践。

不要制定规则或发布任何法令反对它,只因为你喜欢你的特别编写和运行程序的习惯。

不要以禁止枪支拥有为由,理由是有人不知道如何操作枪支或容易滥用枪支。

如果你所设计的规则只适用于无法专业编写代码的编程新手,并且你认为自己是其中的一员,请说明-不要声明你的法令适用于经过适当规范化的数据模型。

一个愚蠢的推理-创始人并没有打算将接口用于这种方式?

我不关心美国宪法的创始人对原意的看法。我不关心未经书面编写的意图。我只关心在书面宪法中所记录的内容,以及如何将它们利用于有效地运行社会。

我只关心Java语言/平台规范允许我做什么,我打算充分利用它们,为我提供一种有效和高效表达软件解决方案的介质。毋需外套。

实际上使用Enum常量是可怕的做法。

它需要编写额外的代码来映射参数到值。Java的创始人没有提供参数值映射而要求你编写该映射代码的事实表明,枚举常量只是Java语言的不合适用法。

尤其是当你不被鼓励将参数规范化和组件化时,会给人一种假象,即混合到枚举容器中的参数属于同一维度。

常量是API合约

不要忘记这一点。如果您设计并规范化了数据模型,并包括常量,则这些常量是合约。如果您没有规范化数据模型,则应遵守如何实践限制性编码以应对该坏习惯的法令。

因此,接口是实现常量合约的完美方式。

奇怪的假设-如果意外地实现了接口。

是的,任何人都可能意外地实现任何接口。没有什么能阻止这些失误的程序员。

设计和规范化您的数据模型以防止泄漏

不要制定限制性法令以保护导致未经合约/流浪参数进入API的假定不良实践。解决根本问题,而不是将责任归咎于接口常量。

不使用IDE是不好的实践

一个正常运转和高效的程序员不在那里证明她可以在水下呆多长时间,在炎热的天气或湿润的雷雨中走多远。她要使用像汽车、公共汽车或至少自行车这样的高效工具,每天上班走10英里。

不要因为您对没有IDE编程的奇特禁欲主义迷恋而限制其他程序员。

一些框架被设计为帮助程序员继续有效地练习不良习惯。

OSGI就是这样一个框架。对接口常量的法令也是如此。

因此,结论是...

接口常量是将数据模型的经过良好设计和规范化的组件放入合约的有效和高效方式。

在类文件中嵌套适当命名的私有接口中的接口常量也是一个好的实践,可以将所有私有常量分组而不是散布到整个文件中。


63
你可以不用开玩笑、讽刺或带情绪地表达你的观点。Stackoverflow 不是一个博客平台。 - tkruse
需要编写额外的代码来将参数映射到值。Java的创始人没有提供参数-值映射而需要编写该映射代码的事实表明,枚举常量是Java语言同样意外使用的结果。如果不编写额外的代码,你还能以什么方式获得参数-值映射呢? - GabrielOshiro
使用接口常量。一定要准确。 - Blessed Geek
@BlessedGeek 枚举常量提供更多的安全性... 一个人可以将 int 字段与任何其他 int 常量进行比较,例如 if (userRole == Consts.MALE)...if (userRole == Consts.US_ZIP_CODE)...,但是使用枚举则更加安全。 - ALZ
1
  1. 永远不要基于int值比较枚举。
  2. 从字符串解析枚举所需的时间甚至比哈希映射还要长,因此许多程序员使用字符串常量而不是枚举。
- Blessed Geek
显示剩余3条评论

29

我现在已经多次遇到这个老问题了,但是被采纳的答案仍然让我困惑。经过很多思考,我认为这个问题还可以进一步澄清。

为什么要使用接口常量?

只需要比较它们:

public final class Constants {

    private Constants() {
        // restrict instantiation
    }

    public static final double PI = 3.14159;
    public static final double PLANCK_CONSTANT = 6.62606896e-34;
}

对比

public interface Constants {

    double PI = 3.14159;
    double PLANCK_CONSTANT = 6.62606896e-34;
}

同样的用法,代码量更少。

不好的实践?

我认为@Pascal Thivent的回答强调有误,以下是我的版本:

将静态成员放入接口中(并实现该接口)是一种不好的实践。

《Effective Java》中的引用假设常量接口由其他人实现,我认为这不应该(也不会)发生。

当您创建一个名为Constants之类的常量接口时,它不应被任何人实现。(虽然在技术上是可能的,但这是唯一的问题)

标准库中不会出现此问题

标准库不能承受任何设计上的可能误用,因此您在标准库中看不到这样的东西。

然而,对于普通开发者的日常项目来说,使用常量接口要容易得多,因为您无需担心staticfinal空构造函数等,并且它不会导致任何错误的设计问题。我能想到的唯一缺点是它仍然具有“接口”的名称,但除此之外没有其他问题。

永无止境的辩论

最后,我认为每个人都只是引用书籍,并对他们的立场提出意见和理由。我也不例外。也许决定仍取决于每个项目的开发人员。如果您感到舒适,就继续使用吧。我们所能做的最好的事情就是使它在整个项目中保持一致


将静态成员放入接口中(并实现该接口)是一种不好的做法。不,它不是。 - VimNing
修饰符 public 也可以省略,因为它是一个接口,使其变成简单的 double PI = 3.14159;。使用 Constants.PI 不需要使用此类实现 Constants 接口!我认为接口方法在使用方面更加清晰,个人观点。 - krozaine
3
更新了,谢谢提醒。对于任何有疑虑的人,请参考以下引用:“在接口中定义的所有常量值都会自动成为 public、static 和 final。”- https://docs.oracle.com/javase/tutorial/java/IandI/interfaceDef.html - Nakamura

5
Joshua Bloch在他的著作《Effective Java - Programming Language Guide》中指出,常量接口模式是接口的一种不良应用。一个类内部使用常量是一个实现细节,实现常量接口会导致这个实现细节泄露到类的公共API中。对于一个类的用户来说,这个类是否实现了常量接口并不重要,甚至可能会使他们感到困惑。更糟糕的是,这代表着一种承诺:如果在将来的版本中,这个类不再需要使用常量,它仍然必须实现接口以确保二进制兼容性。如果一个非final类实现了常量接口,那么它的所有子类都会受到接口中常量的命名空间污染。

8
你所提供的信息并没有增添任何实质性价值,因为这些信息已经被其他人说过了。 - Donal Fellows
1
这个答案比使用这个引用的其他答案要旧。 - Matthew Read

4

有些回答非常合理。

但是我对这个问题有一些想法。(可能是错误的)

在我看来,接口中的字段不应该是整个项目的常量,它们只是接口和扩展接口以及实现这些接口或与它们有密切关系的类的手段。它们应该在特定范围内使用而不是全局。


它们确实应该在这些实现类中使用。 - VimNing

3

2

关于接口的两点:

  • 接口描述了实现它的对象所能做的 子集。(这是本质)

  • 接口描述了被实现对象所遵循的常量规范

    • 这些常量旨在为客户端提供更多关于这些对象的信息。
    • 因此,常量接口用于定义全局常量确实有些不直观,因为接口用于描述某些对象而不是所有对象/没有对象(考虑全局的含义)。

所以我想如果不用常量接口来定义全局常量,那么就可以接受:

  • 如果这些公共常量有好的名称,它们将会推荐实现者使用它们(参见我的第一个例子)
  • 如果一个类要与遵循规范的那些类同步,只需implement它(当然,在实现中使用这些公共常量)。

例子:

interface Drawable {

    double GOLDEN_RATIO = 1.618033988;
    double PI = 3.141592653;
    ...

    // methods
    ...
}

public class Circle implements Drawable {
    ...
    public double getCircumference() {
        return 2 * PI * r;
    }
}

void usage() {

    Circle circle = new Circle(radius: 3.0);
    double maxRadius = 5.0;

    if ( circle.getCircumference() < 2 * Circle.PI * maxRadius ) {
        ...
    }

}

在这个例子中:
Circle implements Drawable,你立即知道Circle可能符合在Drawable中定义的常量,否则他们必须选择更糟糕的名称,因为好的名称PIGOLDEN_RATIO已经被使用了!
只有这些实现了Drawable接口的对象才符合在Drawable中定义的特定精度的PIGOLDEN_RATIO,还可以存在不是Drawable接口的对象,其π和黄金分割比可能具有不同的精度。

1
我是否误解了您的答案,还是您误解了接口的目的?一个接口不是作为一个子集。一个接口是一个契约,订阅该契约的任何人都必须提供该契约规定的交互。接口的唯一有效使用是将其用于声明/满足合同。 - Blessed Geek
1
@BlessedGeek 你履行了一份合同,那么合同所描述的能力就是你所能做的一部分。 - VimNing
常量并不是合同约定,它们是实现细节。完全没有理由公开宣布你的PI值,相反,公共方法应该声明它们的精确程度。没有任何客户会发现了解前者而不了解后者有用。 - Matthew Read

2
我看到了这个问题,并想补充一些未提及的内容。总的来说,我同意Pascal在这里的回答。然而,我不认为接口上的常量“总是”是反模式。
例如,如果您定义的常量是该接口的契约的一部分,我认为接口是放置常量的好地方。在某些情况下,私下验证参数而不向实现的用户公开契约是不合适的。没有公共面向合同,用户只能猜测您正在进行哪种验证,除非反编译类并阅读代码。
因此,如果您实现一个接口,并且该接口具有您用于确保合同的常量(例如整数范围),那么您的类的用户可以通过检查接口中的常量来确保正确使用接口实例。如果常量对于您的实现或者您的实现是包私有的等,则这将是不可能的。

2
你所描述的接口和常量存在的问题是,你可以向接口添加一个常量,但没有任何绑定来强制API的使用者实际上使用该常量。 - Daniel
@Daniel:我已经点赞了你的评论,但现在我有一个很好的解释来回答你的问题:“没有任何绑定强制API的使用者实际上使用该常量”,但现在客户端不能再使用CONSTANT_NAME了,这对我来说是一个好迹象! - VimNing
因此,常量名称在您的类中不可用,但这假定我将命名常量完全相同。使用接口表示常量仍然是一种谬论,接口是API的合同。它绝不能用于表示常量值。 - Daniel
常量不能成为合同的一部分。 合同是 执行 的,即实现。 只有方法可以做到这一点。 常量只是存在。 “用户只能猜测您正在验证什么”--不,您应该指定方法将接受什么,无论是否涉及常量,并且将常量放入接口中并不意味着它们在合同上是必需的。 此外,将常量正确放置在最终类中不会防止在接口,实现,Javadoc或其他文档中引用它们。 - Matthew Read

2
在枚举中,接口常量优于静态final字段。接口常量可以在枚举常量声明中使用,以使它们作为附加到枚举的离散值也可用,而无需任何其他规范。例如,此接口:
public interface Name {
    String MANNY = "Manny";
    String MOE = "Moe";
    String JACK = "Jack";

    String getName();
}

...可以提供字符串常量,这些常量可以用于类似以下的枚举常量中:

public enum PepBoys implements Name {
    BOY1(MANNY),
    BOY2(MOE),
    BOY3(JACK);

    private String name;

    PepBoys(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }
}

注解属性的值必须是常量值,而(具有讽刺意味的是)枚举常量在此上下文中不符合“常量”的要求。然而,在接口中定义的字符串常量确实符合要求:

@MyAnnotation(PepBoys.MANNY)
public void annotatedMethod() {
    ...
}

这样的字符串常量可以轻松地映射回它们关联的枚举常量,并且每个常量只声明一次。有其他实现类似结果的方法,但没有一个像这样简洁,并且都需要至少两个并行声明。

0

javax.swing.SwingConstants 接口是一个例子,其中包含了在 swing 类中使用的静态字段。这使您可以轻松地使用以下内容:

  • this.add(LINE_START, swingcomponent);
  • this.add(this.LINE_START, swingcomponent); 或者
  • this.add(SwingComponents.LINE_START, swingcomponent);

然而,该接口没有方法...


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