与Java中的静态字段进行接口以共享“常量”


116

我正在查看一些开放源代码的Java项目以进入Java,并注意到其中许多具有某种“常量”接口。

例如,processing.org有一个名为PConstants.java的接口,大多数其他核心类都实现了该接口。该接口充满了静态成员。是否有这种方法的原因,或者这被认为是不良做法?为什么不使用有意义的枚举或静态类呢?

我发现使用接口允许某种伪“全局变量”很奇怪。

public interface PConstants {

  // LOTS OF static fields...

  static public final int SHINE = 31;

  // emissive (by default kept black)
  static public final int ER = 32;
  static public final int EG = 33;
  static public final int EB = 34;

  // has this vertex been lit yet
  static public final int BEEN_LIT = 35;

  static public final int VERTEX_FIELD_COUNT = 36;


  // renderers known to processing.core

  static final String P2D    = "processing.core.PGraphics2D";
  static final String P3D    = "processing.core.PGraphics3D";
  static final String JAVA2D = "processing.core.PGraphicsJava2D";
  static final String OPENGL = "processing.opengl.PGraphicsOpenGL";
  static final String PDF    = "processing.pdf.PGraphicsPDF";
  static final String DXF    = "processing.dxf.RawDXF";


  // platform IDs for PApplet.platform

  static final int OTHER   = 0;
  static final int WINDOWS = 1;
  static final int MACOSX  = 2;
  static final int LINUX   = 3;

  static final String[] platformNames = {
    "other", "windows", "macosx", "linux"
  };

  // and on and on

}

15
注意:static final不必要,对于接口来说是多余的。
ThomasW

还要注意的是platformNames可能publicstatic而且final,但它绝对不是一个常数。唯一的常量数组是长度为零的数组。
Vlasec

@ThomasW我知道这已经有几年了,但是我需要指出您的评论中有一个错误。static final不一定是多余的。final当您创建类或接口的对象时,仅包含关键字的类或接口字段将创建该字段的单独实例。使用static final将使每个对象共享该字段的存储位置。换句话说,如果类MyClass具有一个field final String str = "Hello";,则对于MyClass的N个实例,内存中将存在该字段str的N个实例。添加static关键字只会导致1个实例。
Sintrias

Answers:


160

通常认为这是不好的做法。问题在于常量是实现类的公共“接口”(为了更好的用词)的一部分。这意味着实现类将所有这些值发布到外部类,即使仅在内部需要它们也是如此。常量在整个代码中不断扩散。一个示例是Swing中的SwingConstants接口,该接口由数十个类实现,它们全部“重新导出” 其所有常量(甚至是它们不使用的常量)作为自己的常量。

但是,不要只听我的话,乔什·布洛赫Josh Bloch)也说这很不好:

恒定接口模式是对接口的不良使用。类内部使用一些常量是一个实现细节。实现常量接口会导致此实现细节泄漏到类的导出API中。对类的用户而言,该类实现一个常量接口并不重要。实际上,它甚至可能使他们感到困惑。更糟糕的是,它表示一种承诺:如果在将来的版本中对该类进行了修改,使其不再需要使用常量,则它仍必须实现该接口以确保二进制兼容性。如果非最终类实现了常量接口,则其所有子类的名称空间都将受到接口中常量的污染。

枚举可能是更好的方法。或者,您可以简单地将常量作为公共静态字段放在无法实例化的类中。这允许另一个类访问它们而不会污染其自己的API。


8
枚举在这里是一个红鲱鱼-或至少是一个单独的问题。当然应该使用枚举,但是如果实现者不需要,也应该将其隐藏。
DJClayworth

12
顺便说一句:您可以将没有实例的枚举用作无法实例化的类。;)
Peter Lawrey 2010年

5
但是,为什么要首先实现这些接口呢?为什么不将它们仅用作常量存储库?如果我需要全局共享的某种常量,则看不到“更干净”的方法。
shadox

2
@DanDyer是的,但是接口使某些声明隐式存在。像public static final只是默认值。为什么要上课?枚举-好吧,这取决于。枚举应为一个实体定义一个可能值的集合,而不是为不同实体定义一个值的集合。
shadox

4
我个人觉得乔希(Josh)打错了球。如果您不希望常量泄漏(无论您放置哪种对象类型),都需要确保它们不属于导出代码。接口或类都可以导出。因此,要问的正确问题不是:我将它们放在哪种类型的对象中,而是如何组织该对象。并且,如果常量在导出的代码中使用,则仍要确保它们一旦导出就可用。因此,以我的拙见,“不良做法”主张无效。
劳伦斯

99

在Java 1.5+中,可以使用静态导入来从另一个类/接口导入常量/静态方法,而不是实现“常量接口”:

import static com.kittens.kittenpolisher.KittenConstants.*;

这避免了使您的类实现没有功能的接口的麻烦。

至于只存储常量的类的做法,我认为有时是必要的。有些常量在班级中并不自然,因此最好将它们放在“中立”的位置。

但是,不要使用接口,而是使用带有私有构造函数的最终类。(这使得无法实例化或子类化该类,从而发出强烈的信息,即该类不包含非静态功能/数据。)

例如:

/** Set of constants needed for Kitten Polisher. */
public final class KittenConstants
{
    private KittenConstants() {}

    public static final String KITTEN_SOUND = "meow";
    public static final double KITTEN_CUTENESS_FACTOR = 1;
}

因此,您正在解释,由于静态导入,我们应该使用类而不是接口来重新执行与以前相同的错误?真傻!
gizmo

11
不,我根本不是在说什么。我说的是两件事。1:使用静态导入而不是滥用继承。2:如果必须具有常量存储库,请使其成为最终类而不是接口。
Zarkonnen

永远不要将“常量接口”设计为不会成为任何继承的一部分。因此,静态导入仅用于语法糖,并且从此类接口继承是一个可怕的错误。我知道Sun这样做了,但是他们也犯了很多其他的基本错误,这不是模仿它们的借口。
gizmo

3
针对该问题发布的代码的问题之一是接口实现只是为了更轻松地访问常量。当我看到某种实现FooInterface的东西时,我希望它会影响它的功能,而以上内容违反了这一点。静态导入可解决该问题。
Zarkonnen

2
gizmo-我不喜欢静态导入,但是他在那里所做的是避免使用类名,即ConstClass.SOME_CONST。进行静态导入不会将这些成员添加到您Z.所说的类中,而不是要从接口继承,他说相反。
mtruesdell

8

我不假装是对的权利,但让我们看这个小例子:

public interface CarConstants {

      static final String ENGINE = "mechanical";
      static final String WHEEL  = "round";
      // ...

}

public interface ToyotaCar extends CarConstants //, ICar, ... {
      void produce();
}

public interface FordCar extends CarConstants //, ICar, ... {
      void produce();
}

// and this is implementation #1
public class CamryCar implements ToyotaCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

// and this is implementation #2
public class MustangCar implements FordCar {

      public void produce() {
           System.out.println("the engine is " + ENGINE );
           System.out.println("the wheel is " + WHEEL);
      }
}

ToyotaCar对FordCar一无所知,而FordCar对ToyotaCar一无所知。原则上应该改变CarConstants,但是...

不应更改常量,因为车轮是圆形的,而eegine是机械的,但是……将来,丰田的研究工程师发明了电子发动机和扁平车轮!让我们看看我们的新界面

public interface InnovativeCarConstants {

          static final String ENGINE = "electronic";
          static final String WHEEL  = "flat";
          // ...
}

现在我们可以更改抽象了:

public interface ToyotaCar extends CarConstants

public interface ToyotaCar extends InnovativeCarConstants 

现在,如果我们需要更改引擎或车轮的核心价值,我们可以在抽象级别更改ToyotaCar接口,请不要触摸实现

我知道这并不安全,但我仍然想知道您对此有何看法


我想在2019年知道您的想法。对我来说,界面字段应在某些对象之间共享。
下雨

我写了一个与您的想法有关的答案:stackoverflow.com/a/55877115/5290519
下雨了

这是PMD规则非常有限的一个很好的例子。试图通过官僚机构获得更好的代码仍然是徒劳的尝试。
bebbo

6

Java中对此模式有很多讨厌之处。但是,静态常量的接口有时确实有价值。您需要基本满足以下条件:

  1. 这些概念是几个类的公共接口的一部分。

  2. 它们的价值可能在将来的版本中改变。

  3. 所有实现都使用相同的值至关重要。

例如,假设您正在编写假设查询语言的扩展。在此扩展中,您将使用一些索引支持的新操作来扩展语言语法。例如,您将有一个R-Tree支持地理空间查询。

因此,您可以使用静态常量编写一个公共接口:

public interface SyntaxExtensions {
     // query type
     String NEAR_TO_QUERY = "nearTo";

     // params for query
     String POINT = "coordinate";
     String DISTANCE_KM = "distanceInKm";
}

后来,新开发人员认为他需要建立一个更好的索引,因此他来构建R *实现。通过在他的新树中实现此接口,他保证了不同的索引在查询语言中将具有相同的语法。此外,如果以后您确定“ nearTo”是一个令人困惑的名称,则可以将其更改为“ withinDistanceInKm”,并且知道所有索引实现都将遵循新语法。

PS:此示例的灵感来自Neo4j空间代码。


5

有了事后观察的优势,我们可以看到Java在许多方面都被破坏了。Java的一个主要失败是接口对抽象方法和静态final字段的限制。较新的,更复杂的OO语言(例如Scala)通过特征包含接口,这些特征可以(并且通常确实)包括具体方法,这些方法可能具有零零(常数!)。有关特征作为可组合行为单位的论述,请参见http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf。有关Scala中的特征与Java中的接口相比的简短描述,请参见http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5。在讲授面向对象设计的上下文中,简单的规则(例如断言接口不应包含静态字段)是愚蠢的。许多特性自然包括常量,并且这些常量适当地是特性支持的公共“接口”的一部分。在编写Java代码时,没有干净,优雅的方式来表示特征,但是在接口内使用静态的final字段通常是一种不错的解决方法。


12
非常自命不凡,如今已过时。
Esko 2014年

1
很棒的见解(+1),尽管对Java可能有些挑剔。
彼得-恢复莫妮卡

0

根据JVM规范,接口中的字段和方法只能具有Public,Static,Final和Abstract。来自Java VM内部的参考

默认情况下,接口中的所有方法都是抽象的,即使您没有明确提及它也是如此。

接口仅用于提供规范。它不能包含任何实现。因此,为避免实现用于更改规范的类,将其定型。由于无法实例化接口,因此将其设置为静态以使用接口名称访问字段。


0

我没有足够的声誉来对Pleerock发表评论,因此我必须创建一个答案。对此我感到抱歉,但是他在其中付出了很大的努力,我想回答他。

Pleerock,您创建了一个完美的示例,以说明为什么这些常量应该独立于接口和独立于继承。对于应用程序的客户来说,在汽车的实现之间存在技术差异并不重要。对于客户来说,它们是相同的,只是汽车。因此,客户端希望从这种角度来看它们,这是一个类似于I_Somecar的接口。在整个应用程序中,客户将仅使用一个视角,而对于每个不同的汽车品牌则不会使用不同的视角。

如果客户要在购买前比较汽车,可以采用以下方法:

public List<Decision> compareCars(List<I_Somecar> pCars);

界面是关于行为的契约,从一个角度显示了不同的对象。您设计的方式,将使每个汽车品牌都有自己的传承路线。尽管实际上是完全正确的,但是由于汽车可能像比较完全不同类型的对象一样大不相同,因此最终可以在不同的汽车之间进行选择。这就是所有品牌必须共享的界面的观点。常量的选择不应使这不可能。请考虑Zarkonnen的答案。


-1

这是从Java 1.5出现并为我们带来枚举之前发生的。在此之前,没有好的方法来定义一组常数或约束值。

在许多项目中,大部分时间还是为了向后兼容或由于需要大量的重构而使用了此方法。


2
在Java 5之前,您可以使用类型安全的枚举模式(请参见java.sun.com/developer/Books/shiftintojava/page1.html)。
Dan Dyer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.