Java 中带有静态字段的接口,用于共享“常量”

我正在研究一些进入 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


}
130254 次浏览

这来自 Java 1.5存在之前的时代,并且给我们带来了枚举。在此之前,没有定义一组常量或约束值的好方法。

在很多项目中,这种方法仍然被使用,大多数时候是为了向下兼容,或者是为了摆脱重构的困扰。

Instead of implementing a "constants interface", in Java 1.5+, you can use static imports to import the constants/static methods from another class/interface:

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

这样就避免了让类实现没有功能的接口的丑陋。

至于使用类来存储常量的做法,我认为有时候是必要的。有些常量在类中没有固定的位置,所以最好将它们放在“中立”的位置。

但是不要使用接口,而是使用带有私有构造函数的 final 类。(使得不可能实例化或子类化该类,从而发出一个强有力的信息,即它不包含非静态功能/数据。)

例如:

/** 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;
}

这通常被认为是不好的做法。问题是这些常量是实现类的公共“接口”的一部分。这意味着实现类正在将所有这些值发布到外部类,即使它们只是内部需要的。这些常数在整个代码中不断增加。Swing 中的 摇摆常数接口就是一个例子,它由几十个类实现,所有这些类都“重新导出”其常量的 所有(甚至是它们没有使用的那些)作为它们自己的 所有

But don't just take my word for it, Josh Bloch 也说 it's bad:

常量接口模式对接口的使用很差。类在内部使用一些常量是一个实现细节。实现常量接口会导致该实现细节泄漏到类的导出 API 中。类实现常量接口对于类的用户来说是无关紧要的。事实上,这甚至会让他们感到困惑。更糟糕的是,它代表了一种承诺: 如果在将来的发行版中,类被修改为不再需要使用常量,那么它仍然必须实现接口以确保二进制兼容性。如果一个非 final 类实现了一个常量接口,那么它的所有子类的名称空间都会受到接口中常量的污染。

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

Given the advantage of hindsight, we can see that Java is broken in many ways. One major failing of Java is the restriction of interfaces to abstract methods and static final fields. Newer, more sophisticated OO languages like Scala subsume interfaces by traits which can (and typically do) include concrete methods, which may have arity zero (constants!). For an exposition on traits as units of composable behavior, see http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf. For a short description of how traits in Scala compare with interfaces in Java, see http://www.codecommit.com/blog/scala/scala-for-java-refugees-part-5. In the context of teaching OO design, simplistic rules like asserting that interfaces should never include static fields are silly. Many traits naturally include constants and these constants are appropriately part of the public "interface" supported by the trait. In writing Java code, there is no clean, elegant way to represent traits, but using static final fields within interfaces is often part of a good workaround.

我不会假装自己是对的,但让我们看看这个小例子:

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 doesnt know anything about FordCar, and FordCar doesnt know about ToyotaCar. principle CarConstants should be changed, but...

Constants should not be changed, because the wheel is round and egine is mechanical, but... 在未来,丰田的研究工程师发明了电子发动机和扁平车轮! 让我们看看我们的新界面

public interface InnovativeCarConstants {


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

and now we can change our abstraction:

public interface ToyotaCar extends CarConstants

public interface ToyotaCar extends InnovativeCarConstants

现在,如果我们需要改变核心价值,如果引擎或车轮,我们可以改变丰田汽车接口的抽象层,不要触及实施

Its NOT SAFE, I know, 但我还是想知道你有没有想过

根据 JVM 规范,接口中的字段和方法只能包含 Public、 Static、 Final 和 Abtic.从 JavaVM 内部引用

默认情况下,接口中的所有方法都是抽象的,即使你没有明确地提到它。

Interfaces are meant to give only specification. It can not contain any implementations. So To avoid implementing classes to change the specification, it is made final. Since Interface cannot be instantiated, they are made static to access the field using interface name.

我没有足够的声誉来评论 Pleerock,因此我必须创造一个答案。对此我很抱歉,但是他在这方面付出了很大的努力,我想回答他。

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

如果一个客户想要在购买之前比较汽车,他可以有这样一个方法:

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

接口是关于行为的契约,从一个角度显示不同的对象。你设计它的方式,将每一个汽车品牌都有自己的传承路线。虽然它在现实中是相当正确的,因为汽车可以是不同的,它可以像比较完全不同类型的对象,最后有不同的汽车之间的选择。这就是所有品牌必须共享的界面的视角。常量的选择不应该使这一点成为不可能。请考虑一下 Zarkonnen 的答案。

在 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";
}

Now later, a new developer thinks he needs to build a better index, so he comes and builds an R* implementation. By implementing this interface in his new tree he guarantees that the different indexes will have identical syntax in the query language. Moreover, if you later decided that "nearTo" was a confusing name, you could change it to "withinDistanceInKm", and know that the new syntax would be respected by all your index implementations.

PS: The inspiration for this example is drawn from the Neo4j spatial code.