Java 一组常量应该如何存储?

在 Java 当中用哪种方式储存一组常量比较合理呢?是使用常量接口、常量类还是枚举类型呢?

先说结论:定义常量不要使用接口常量,要在类中定义,使用 final 类以及 private 构造方法,如果常量可以进行归类,最好使用枚举定义:枚举 > 类 > 接口。

第一种:使用接口

1
2
3
4
public interface Constants {
public static final int AUDIT_STATUS_PASS = 1;
public static final int AUDIT_STATUS_NOT_PASS = 2;
}

使用接口来保存常量的话会产生一些问题,常量接口是一种严重的反模式行为,《Effective Java》这么写道:

The constant interface pattern is a poor use of interfaces. That a class uses some constants internally is an implementation detail. Implementing a constant interface causes this implementation detail to leak into the class’s exported API. It is of no consequence to the users of a class that the class implements a constant interface. In fact, it may even confuse them. Worse, it represents a commitment: if in a future release the class is modified so that it no longer needs to use the constants, it still must implement the interface to ensure binary compatibility. If a nonfinal class implements a constant interface, all of its subclasses will have their namespaces polluted by the constants in the interface.

翻译如下:

常量接口模式是对接口的不良使用。类在内部使用某些常量,这纯粹是实现细节。实现常量接口会导致把这样的实现细节泄露到该类的导出API中。类实现常量接口,这对于类的用户来讲并没有什么价值。实际上,这样做反而会使他们更加糊涂。更糟糕的是,它代表了一种承诺:如果在将来的发行版本中,这个类被修改了,它不再需要使用这些常量了,它依然必须实现这个接口,以确保兼容性。如果非final类实现了常量接口,它的所有子类的命名空间也会被接口中的常量所“污染”。

总结下:

  1. 接口是不能阻止被实现或继承的,也就是说子接口或实现中是能够覆盖掉常量的定义,这样通过父,子接口(或实现) 去引用常量是可能不一致的

  2. 同样的,由于被实现或继承,造成在继承树中可以用大量的接口, 类 或实例去引用 同一个常量,从而造成接口中定义的常量污染了命名空间。(Java 编译器竟然允许使用实例去引用类变量)

  3. 接口暗含的意思是:它是需被实现的,代表着一种类型,它的公有成员是要被暴露的 API。而在接口中定义的常量说不上是 API


第二种:使用类

1
2
3
4
5
6
public final class Constans{
public static final int AUDIT_STATUS_PASS = 1;
public static final int AUDIT_STATUS_NOT_PASS = 2;
//私有的构造方法
private Constans(){};
}

添加 final 关键字来避免被继承,隐藏构造方法防止实例化,常量类解决了这两个问题,常量类会比常量接口编译出来的源文件字节多一点点。


第三种:使用枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum Constants {
AUDIT_STATUS_PASS(1),
AUDIT_STATUS_NOT_PASS(2);

private int status;

private Constants(int status){
this.setStatus(status);
}

public int getStatus() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

}

《Effective Java》中有这么一个例子

1
2
3
4
5
6
public static final int APPLE_FUJI = 0;
public static final int APPLE_PIPPIN = 1;
public static final int APPLE_GRANNY_SMITH = 2;
public static final int ORANGE_NAVEL = 0;
public static final int ORANGE_TEMPLE = 1;
public static final int ORANGE_BLOOD = 2;

针对int常量以下不足:

  1. 在类型安全方面,如果你想使用的是ORANGE_NAVEL,但是传递的是APPLE_FUJI,编译器并不能检测出错误;
  2. 因为int常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的int发生了变化,客户端需重新编译,否则它们的行为就不确定;
  3. 没有便利方法将int常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是ORANGE_NAVEL,debug的时候显示的是0,但你不能确定是APPLE_FUJI还是ORANGE_NAVEL

如果你想使用String常量,虽然提供了可打印的字符串,但是性能会有影响。特殊是对于有些新手开发,有可能会直接将String常量硬编码到代码中,导致以后修改很困难。

所有这一切,enum都给出了具体的解决。唯一的缺点只是需要增加enum加载和实例化的时间。

以上面的 APPLE,ORANGE为例,用 enum 重写:

1
2
3
4
针对int常量以下不足: 
1. 在类型安全方面,如果你想使用的是 ORANGE_NAVEL,但是传递的是 APPLE_FUJI,编译器并不能检测出错误;
2. 因为 int 常量是编译时常量,被编译到使用它们的客户端中。若与枚举常量关联的 int 发生了变化,客户端需重新编译,否则它们的行为就不确定;
3. 没有便利方法将 int 常量翻译成可打印的字符串。这里的意思应该是比如你想调用的是 ORANGE_NAVEL,debug 的时候显示的是 0,但你不能确定是 APPLE_FUJI 还是 ORANGE_NAVEL
如果你想使用 String 常量,虽然提供了可打印的字符串,但是性

能会有影响。特殊是对于有些新手开发,有可能会直接将String常量硬编码到代码中,导致以后修改很困难。
所有这一切,enum 都给出了具体的解决。唯一的缺点只是需要增加enum 加载和实例化的时间。