与java中的静态字段接口共享“常量”
我正在看一些开源的Java项目进入Java,并注意到他们中的很多有一些“常量”接口。
例如, processing.org有一个名为PConstants.java的接口,其他大部分核心类都实现了这个接口。 界面充斥着静态成员。 这种方法是有原因的,还是被认为是不好的做法? 为什么不使用有意义的枚举或静态类呢?
我觉得使用接口来允许某种伪“全局variables”是很奇怪的。
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 }
这通常被认为是不好的做法。 问题在于常数是实现类的公共“接口”(为了更好的词)。 这意味着实现类将所有这些值发布到外部类,即使它们只在内部需要。 常量遍及整个代码。 Swing中的SwingConstants接口就是一个例子,它由几十个类来实现,它们都将所有常量(甚至是不使用的)重新导出为自己的类。
但是不要只听我的话, Josh Bloch也说这很糟糕:
常量接口模式是接口使用不佳。 一个类内部使用一些常量是一个实现细节。 实现一个常量接口会导致这个实现细节泄露到类的导出的API中。 对类的用户来说,类实现一个常量接口是没有意义的。 事实上,它甚至可能混淆它们。 更糟糕的是,它代表了一个承诺:如果在将来的发行版中,类被修改以便不再需要使用常量,它仍然必须实现接口以确保二进制兼容性。 如果一个非终结类实现了一个常量接口,那么它的所有子类的接口中的常量都会使其名称空间受到污染。
枚举可能是一个更好的方法。 或者你可以简单地把常量作为公共静态字段放在一个无法实例化的类中。 这允许另一个类访问它们而不会污染它自己的API。
您可以使用静态导入从另一个类/接口导入常量/静态方法,而不是在Java 1.5+中实现“常量接口”:
import static com.kittens.kittenpolisher.KittenConstants.*;
这避免了让你的类实现没有function的接口的丑陋。
至于有一个类只是为了存储常量的做法,我认为有时是必要的。 有一些常数在课堂上没有自然的地方,所以最好让他们在“中立”的地方。
但是,而不是使用一个接口,使用一个私有构造函数的最后一个类。 (使得不能实例化或inheritance这个类,发送一个强信息,它不包含非静态的function/数据。)
例如:
/** 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; }
我不假装正确的权利,但让我们看看这个小例子:
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); } }
丰田汽车对福特汽车不了解,而福特汽车也不了解丰田汽车。 原则CarConstants应该改变,但…
常量不应该改变,因为车轮是圆的,而且是机械的,但是…未来,丰田的研究工程师发明了电子发动机和扁平车轮! 让我们看看我们的新界面
public interface InnovativeCarConstants { static final String ENGINE = "electronic"; static final String WHEEL = "flat"; // ... }
现在我们可以改变我们的抽象:
public interface ToyotaCar extends CarConstants
至
public interface ToyotaCar extends InnovativeCarConstants
而现在,如果我们需要改变核心价值,如果我们可以改变ToyotaCar接口的抽象水平的引擎或轮,不要触摸实现
我知道这不安全,但我仍然想知道你是否想过这个问题
鉴于事后的好处,我们可以看到Java在很多方面都被打破了。 Java的一个主要失败是接口对抽象方法和静态final字段的限制。 更新,更复杂的面向对象的语言,如Scala包含的接口可以(通常包括)具体的方法,可能有零(常数!)。 有关作为可组合行为单位的性状的论述,请参阅archive/papers/Scha03aTraits.pdf 。 有关Scala中的特征如何与Java中的接口进行比较的简短说明,请参阅http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5 。 在教授面向对象devise的情况下,像断言接口不应该包含静态字段的简单规则是愚蠢的。 许多特质自然包括常数,而这些常数恰好是特质所支持的公共“界面”的一部分。 在编写Java代码时,没有简洁明了的表示特征的方法,但是在接口中使用静态最终字段通常是一个很好的解决方法的一部分。
在Java中这种模式有很多讨厌的东西。 但是,静态常量的接口有时是有价值的。 你需要基本满足以下条件:
-
这些概念是几个类的公共接口的一部分。
-
他们的价值可能在未来的版本中改变
- 所有实现都使用相同的值是至关重要的。
例如,假设您正在编写一个假设查询语言的扩展。 在这个扩展中,你将用一些索引支持的新操作来扩展语言语法。 例如,你将有一个支持地理空间查询的R-Tree。
所以你用静态常量编写一个公共接口:
public interface SyntaxExtensions { // query type String NEAR_TO_QUERY = "nearTo" // params for query String POINT = "coordinate" String DISTANCE_KM = "distanceInKm" }
后来,一位新开发人员认为他需要build立一个更好的索引,所以他来创build一个R *实现。 通过在他的新树中实现这个接口,他保证了不同的索引在查询语言中将具有相同的语法。 此外,如果您稍后决定“nearTo”是一个令人困惑的名称,则可以将其更改为“withinDistanceInKm”,并且知道所有索引实现都将遵守新的语法。
PS:这个例子的灵感来自Neo4j空间代码。
根据JVM规范,接口中的字段和方法只能包含Public,Static,Final和Abstract。 来自Inside Java VM
默认情况下,界面中的所有方法都是抽象的,甚至很难,你没有明确提到它。
接口只是为了给出规范。 它不能包含任何实现。 所以为了避免实现类来改变规范,它是最终的。 由于接口不能被实例化,所以它们被设置为静态的以使用接口名称访问该字段。
我没有足够的声望给Pleerock发表评论,因此我必须创build一个答案。 我为此感到遗憾,但他付出了很多努力,我想回答他。
Pleerock,你创build了一个完美的例子来说明为什么这些常量应该独立于接口和独立于inheritance。 对于应用程序的客户来说,在汽车的实施方面存在技术差异并不重要。 他们对客户来说是一样的,只是汽车。 所以,客户要从这个angular度来看待他们,就像I_Somecar这样的界面。 在整个应用程序中,客户对每个不同的汽车品牌只使用一个视angular而不是不同的视angular。
如果客户想要在购买之前比较汽车,他可以有这样的方法:
public List<Decision> compareCars(List<I_Somecar> pCars);
界面是关于行为的契约,从一个angular度展示不同的对象。 你devise的方式,每一个汽车品牌都有自己的inheritance路线。 虽然实际上是相当正确的,但因为汽车可以是不同的,就像比较完全不同types的物体,最后在不同的汽车之间有select。 这就是所有品牌必须共享的界面的angular度。 常量的select不应该使这不可能。 请考虑Zarkonnen的答案。
这是Java 1.5存在之前的时代,并且给我们带来了枚举。 在此之前,没有好方法来定义一组常量或约束值。
在大多数项目中,这仍然是大部分时间用于向后兼容,或者是由于需要重构的数量。