接口只用於定義類型,不該該使用常量接口

《Effective Java》中說的感受比較合理,貌似這個問題也是這裏提出的,後面那篇是一開始搜到的,也有點用,反正就是不要這麼使用就對了。java

《Effective Java》第19條:接口只用於定義類型

當類實現接口時,接口就充當能夠引用這個類的實例的類型(type)。所以,類實現了接口,就代表客戶端能夠對這個類的實例實施某些動做(接口中定義的方法)。爲了任何其餘目的而定義接口是不恰當的。c++

有一種接口被稱爲常量接口(constant interface),它不知足上面的條件。這種接口沒有包含任何方法,它只包含靜態的final域,每一個域都導出一個常量。使用這些常量的類實現這個接口,以免用類名來修飾常量名。下面是一個例子:app

<!-- lang: java -->
// Constant interface antipattern —— do not use!
public interface PhysicalConstants {
  // Avogadro's number (1/mol)
  static final double AVOGADROS_NUMBER   = 6.02214199e23;

  // Boltzmann constant (J/K)
  static final double BOLTZMANN_CONSTANT = 1.3806503e-23;

  ...
}

**常量接口模式是對接口的不良使用。**類在內部使用某些常量,這純粹是實現細節。實現常量接口,會致使把這樣的實現細節泄露到該類的導出API中。**類實現常量接口,這對於這個類的用戶來說並無什麼價值。**實際上,這樣作反而會使他們更加糊塗。更糟糕的是,它表明了一種承諾:若是在未來的發行版本中,這個類被修改了,它再也不須要使用這些常量了,它依然必須實現這個接口,以確保二進制兼容性。若是非final類實現了常量接口,它的全部子類的命名空間也會被接口中的常量所「污染」。工具

在Java平臺類庫中有幾個常量接口,例如java.io.ObjectStreamConstants。這些接口應該被認爲是反面的典型,不值得效仿。atom

若是要導出常量,能夠有幾種合理的選擇方案code

若是這些常量與某個現有的類或者接口緊密相關,就應該把這些常量添加到這個類或者接口中。例如,在Java平臺類庫中全部的數值包裝類,如Integer和Double,都導出了MIN_VALUEMAX_VALUE常量。接口

若是這些常量最好被看做枚舉類型的成員,就應該用枚舉類型(enum type)(見第30條)來導出這些常量。ip

不然,應該使用不可實例化的工具類(utility class)(見第4條)來導出這些常量。ci

下面的例子是前面的PhysicalConstants例子的工具類翻版:get

<!-- lang: java -->
// Constant utility class
package com.effectivejava.science;

public class PhysicalConstants {
  private PhysicalConstants() { } // Prevents instantiation

  public static final double AVOGADROS_NUMBER   = 6.02214199e23;
  public static final double BOLTZMANN_CONSTANT = 1.3806503e-23;
  ...
}

工具類一般要求客戶端要用類名來修飾這些常量名。若是大量利用工具類導出的常量,能夠經過利用**靜態導入(static import)**機制,避免用類名來修飾常量名,不過,靜態導入機制是在Java發行版本1.5中才引入的:

<!-- lang: java -->
// Use of static import to avoid qualifying constants
import static com.effectivejava.science.PhysicalConstants.*;

public class Test {
  double atoms(double mols) {
    return AVOGADROS_NUMBER * mols;
  }
  ...
  // Many more uses of PhysicalConstants justify static import
}

簡而言之,接口應該只被用來定義類型,它們不該該被用來導出常量。

=====

Java Interface(接口) 是常量存放的最佳地點嗎?(不是)

因爲java interface中聲明的字段在編譯時會自動加上static final的修飾符,即聲明爲常量。於是interface一般是存放常量的最佳地點。然而在java的實際應用時卻會產生一些問題。

問題的原由有兩個,

第一,是咱們所使用的常量並非一成不變的,而是相對於變量不能賦值改變。

例如咱們在一個工程初期定義常量∏=3.14,而因爲計算精度的提升咱們可能會從新定義∏=3.14159,此時整個項目對此常量的引用都應該作出改變。

第二,java是動態語言

與c++之類的靜態語言不一樣,java對一些字段的引用能夠在運行期動態進行,這種靈活性是java這樣的動態語言的一大優點。也就使得咱們在java工程中有時部份內容的改變不用從新編譯整個項目,而只需編譯改變的部分從新發布就能夠改變整個應用。

講了這麼多,你還不知道我要說什麼嗎?好,咱們來看一個簡單的例子:

有一個interface A,一個class B,代碼以下:

<!-- lang: java -->
//file A.java
public interface A {
  String name = "bright";
}
//file B.java
public class B {
  public static void main(String[] args) {
    System.out.println("Class A's name = " + A.name);
  }
}

夠簡單吧,好,編譯A.java和B.java。

運行,輸入java B,顯然結果以下:

Class A's name = bright

咱們如今修改A.java以下:

<!-- lang: java -->
//file A.java
public interface A{
  String name = "bright sea";
}

編譯A.java後從新運行B class,輸入java B,注意:結果以下

Class A's name = bright

爲何不是Class A's name = bright sea?讓咱們使用jdk提供的反編譯工具javap反編譯B.class看個究竟,輸入:javap -c B ,結果以下:

<!-- lang: java -->
Compiled from B.java
public class B extends java.lang.Object {
  public B();
  public static void main(java.lang.String[]);
}
Method B()
  0 aload_0
  1 invokespecial #1 <Method java.lang.Object()>
  4 return
Method void main(java.lang.String[])
  0 getstatic #2 <Field java.io.PrintStream out>
  3 ldc #3 <String "Class A's name = bright">
  5 invokevirtual #4 <Method void println(java.lang.String)>
  8 return

注意到標號3的代碼了嗎?因爲引用了一個static final的字段,編譯器已經將interface Aname的內容編譯進了class B中,而不是對interface A中的name的引用。所以除非咱們從新編譯class Binterface Aname發生的變化沒法在class B中反映。若是這樣去作那麼java的動態優點就消失殆盡。

解決方案,有兩種解決方法。

第一種方法是再也不使用常量,將所需字段放入class中聲明,並去掉final修飾符。但這種方法存在必定的風險,因爲再也不是常量着於是在系統運行時有可能被其餘類修改其值而發生錯誤,也就違背了咱們設置它爲常量的初衷,於是不推薦使用

第二種方法,將常量放入class中聲明,使用class方法來獲得此常量的值。爲了保持對此常量引用的簡單性,咱們可使用一個靜態方法。咱們將A.java和B.java修改以下:

<!-- lang: java -->
//file A.java
public class A {
  private static final String name = "bright";
  public static String getName() {
    return name;
  }
}
//file B.java
public class B {
  public static void main(String[] args) {
    System.out.println("Class A's name = " + A.getName());
  }
}

一樣咱們編譯A.java和B.java。運行class B,輸入java B,顯然結果以下:

Class A's name = bright

如今咱們修改A.java以下:

<!-- lang: java -->
//file A.java
public class A{
  private static final String name = "bright";
  public static String getName(){
    return name;
  }
}

咱們再次編譯A.java後從新運行B class,輸入java B:結果以下

Class A's name = bright sea

終於獲得了咱們想要的結果,咱們能夠再次反編譯B.class看看class B的改變,輸入:

javap -c B,結果以下:

<!-- lang: java -->
Compiled from B.java
public class B extends java.lang.Object {
  public B();
  public static void main(java.lang.String[]);
}
Method B()
   0 aload_0
   1 invokespecial #1 <Method java.lang.Object()>
   4 return
Method void main(java.lang.String[])
   0 getstatic #2 <Field java.io.PrintStream out>
   3 new #3 <Class java.lang.StringBuffer>
   6 dup
   7 invokespecial #4 <Method java.lang.StringBuffer()>
  10 ldc #5 <String "Class A's name = ">
  12 invokevirtual #6 <Method java.lang.StringBuffer append(java.lang.String)>
  15 invokestatic #7 <Method java.lang.String getName()>
  18 invokevirtual #6 <Method java.lang.StringBuffer append(java.lang.String)>
  21 invokevirtual #8 <Method java.lang.String toString()>
  24 invokevirtual #9 <Method void println(java.lang.String)>
  27 return

注意標號10至15行的代碼,class B中已經變爲對A classgetName()方法的引用,當常量name的值改變時咱們只需對class A中的常量作修改並從新編譯,無需編譯整個項目工程咱們就能改變整個應用對此常量的引用,既保持了java動態優點又保持了咱們使用常量的初衷,於是方法二是一個最佳解決方案。

相關文章
相關標籤/搜索