1. 程式人生 > >effectiveJava學習筆記:列舉

effectiveJava學習筆記:列舉

使用列舉代替int常量

在列舉型別出現之前,一般都常常使用int常量或者String常量表示列舉相關事物。如:

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常量硬編碼到程式碼中,導致以後修改很困難。

4.遍歷一個組中所有的int列舉常量,獲得int列舉組的大小,沒有可靠的方法,如想知道APPLE的常量有多少個,除了檢視int列舉常量所在位置的程式碼外,別無他法,而且靠的是觀察APPLE_字首有多少個。

列舉:

public enum Apple {
    APPLE_FUJI,
    APPLE_PIPPIN,
    APPLE_GRANNY_SMITH;
}

public enum Orange {
    ORANGE_NAVEL,
    ORANGE_TEMPLE,
    ORANGE_BLOOD;
}

enum列舉常量與資料關聯

我們先看我們常用到的判斷狀態的常量類在列舉中的用法

/**
 * @author ligz
 */
public enum Database {
	mysql(0),
	mongodb(1),
	redis(2),
	hbase(3);
	
	private int status;
	
	Database(int status){
		this.status = status;
	}
	
	public int Status() {
		return status;
	}
}
/**
 * @author ligz
 */
public class TestDataBase {
	public static void main(String[] args) {
		for(Database d : Database.values()) {
			if(0 == d.Status()) {
				System.out.println("mysql");
			}
		}
				
	}
}

我們再來看書上的例子

如以太陽系為例,每個行星都擁有質量和半徑,可以依據這兩個屬性計算行星表面物體的重量。程式碼如下:

package com.ligz.Chapter6.Item30;

/**
 * @author ligz
 */
public enum Planet {
	MERCURY(3.302e+23, 2.439e6),
    VENUS (4.869e+24, 6.052e6),
    EARTH (5.975e+24, 6.378e6),
    MARS (6.419e+23, 3.393e6),
    JUPITER(1.899e+27, 7.149e7),
    SATURN (5.685e+26, 6.027e7),
    URANUS (8.683e+25, 2.556e7),
    NEPTUNE(1.024e+26, 2.477e7);
	
	private final double mass; // In kilograms
    private final double radius; // In meters
    private final double surfaceGravity; // In m / s^2

    // Universal gravitational constant in m^3 / kg s^2
    private static final double G = 6.67300E-11;

    Planet(double mass, double radius){
    	this.mass = mass;
    	this.radius = radius;
    	surfaceGravity = G * mass / (Math.pow(radius, 2));
    }

	public double getMass() {
		return mass;
	}

	public double getRadius() {
		return radius;
	}

	public double getSurfaceGravity() {
		return surfaceGravity;
	}
    
    public double getSurfaceWight(double mass) {
    	return mass * surfaceGravity; // F = ma
    }
    
}
package com.ligz.Chapter6.Item30;

/**
 * @author ligz
 */
public class WeightTable {
public static void main(String[] args) {
	double earthWeight = 30;
	double mass = earthWeight / Planet.EARTH.getSurfaceGravity();
	
	for(Planet p : Planet.values()) {
		System.out.printf("Weight on %s is %f%n", p, p.getSurfaceWight(mass));
	}
}
}

輸出為

Weight on MERCURY is 11.337201
Weight on VENUS is 27.151530
Weight on EARTH is 30.000000
Weight on MARS is 11.388120
Weight on JUPITER is 75.890383
Weight on SATURN is 31.965423
Weight on URANUS is 27.145664
Weight on NEPTUNE is 34.087906

enum列舉常量與行為關聯

有些時候將enum列舉常量與資料關聯還不夠,還需要將列舉常量與行為關聯。

如採用列舉來寫加、減、乘、除的運算。程式碼如下:

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;

    double apply(double x, double y) {
        switch(this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("Unknown op: " + this);
    }
}

大家一開始都會這樣寫的。實際開發中,有很多開發者也這樣寫。但是有個不足:如果需要新增加運算,譬如模運算,不僅僅需要新增列舉型別常量,還需要修改apply方法。萬一忘記修改了,那就是執行時錯誤。

public enum Operation {
  PLUS("+") {
    @Override
    double apply(double x, double y) {
      return x + y;
    }
  },

  MINUS("-") {
    @Override
    double apply(double x, double y) {
      return x - y;
    }
  },

  TIMES("*") {
    @Override
    double apply(double x, double y) {
      return x * y;
    }
  },

  DIVIDE("/") {
    @Override
    double apply(double x, double y) {
      return x / y;
    }
  };

  private String symbol;
  Operation(String symbol) {
    this.symbol = symbol;
  }

  @Override
  public String toString() {
    return symbol;
  }

  abstract double apply(double x, double y);
}

public class OperationDemo {

  public static void main(String[] args) {
    double x = 2;
    double y = 4;

    for (Operation op : Operation.values()) {
      System.out.println(String.format("%f %s %f = %f%n", x, op, y, op.apply(x, y)));
    }
}

 輸入2 4
    2.000000 + 4.000000 = 6.000000
    2.000000 - 4.000000 = -2.000000
    2.000000 * 4.000000 = 8.000000
    2.000000 / 4.000000 = 0.500000
  }

一般,enum中重寫了toString方法之後,enum中自生成的valueOf(String)方法不能根據列舉常量的字串(toString生成)來獲取列舉常量。我們通常需要在enum中新增個靜態常量來獲取。如:

public enum Operation {
  PLUS("+") {
    @Override
    double apply(double x, double y) {
      return x + y;
    }
  },

  MINUS("-") {
    @Override
    double apply(double x, double y) {
      return x - y;
    }
  },

  TIMES("*") {
    @Override
    double apply(double x, double y) {
      return x * y;
    }
  },

  DIVIDE("/") {
    @Override
    double apply(double x, double y) {
      return x / y;
    }
  };

  private String symbol;
  public static final Map<String, Operation> OPERS_MAP = Maps.newHashMap();

  static {
    for (Operation op : Operation.values()) {
      OPERS_MAP.put(op.toString(), op);
    }
  }

  Operation(String symbol) {
    this.symbol = symbol;
  }

  @Override
  public String toString() {
    return symbol;
  }

  abstract double apply(double x, double y);
}

可以通過呼叫Operation.OPERS_MAP.get(op.toString())來獲取對應的列舉常量。

在有些特定的情況下,此寫法有個缺點,即如果每個列舉常量都有公共的部分處理該怎麼辦,如果每個列舉常量關聯的方法裡都有公共的部分,那不僅不美觀,還違反了DRY原則。這就是下面的列舉策略模式

enum PayrollDay {
    MONDAY, 
    TUESDAY, 
    WEDNESDAY, 
    THURSDAY, 
    FRIDAY,
    SATURDAY, 
    SUNDAY;

    private static final int HOURS_PER_SHIFT = 8;

    double pay(double hoursWorked, double payRate) {
        double basePay = hoursWorked * payRate;

        double overtimePay; // Calculate overtime pay
        switch(this) {
            case SATURDAY: case SUNDAY:
                overtimePay = hoursWorked * payRate / 2;
                break;
            default: // Weekdays
                overtimePay = hoursWorked <= HOURS_PER_SHIFT ?
                0 : (hoursWorked - HOURS_PER_SHIFT) * payRate / 2;
        }

        return basePay + overtimePay;
    }
}

以上程式碼是計算工人工資。平時工作8小時,超過8小時,以加班工資方式另外計算;如果是雙休日,都按照加班方式處理工資。

上面程式碼的寫法和上一小節給出的差不多,通過switch來分拆計算。還是一樣的問題,如果此時新增加一種工資的計算方式,列舉常量需要改,pay方法也需要改。按上一小節的介紹繼續修改:
 

public enum PayRoll {
  MONDY(PayType.WEEKDAY),
  TUESDAY(PayType.WEEKDAY),
  WEDNESDAY(PayType.WEEKDAY),
  THURSDAY(PayType.WEEKDAY),
  FRIDAY(PayType.WEEKDAY),
  SATURDAY(PayType.WEEKEND),
  SUNDAY(PayType.WEEKEND);

  private final PayType payType;
  PayRoll(PayType payType) {
    this.payType = payType;
  }

  double pay(double hoursWorked, double payRate) {
    return payType.pay(hoursWorked, payRate);
  }

  private enum PayType {
    WEEKDAY {
      @Override
      double overtimePay(double hoursWorked, double payRate) {
        double overtime = hoursWorked - HOURS_PER_SHIFT;
        return overtime <= 0 ? 0 : overtime * payRate / 2;
      }
    },

    WEEKEND {
      @Override
      double overtimePay(double hoursWorked, double payRate) {
        return hoursWorked * payRate / 2;
      }
    };

    private static final int HOURS_PER_SHIFT = 8;
    abstract double overtimePay(double hoursWorked, double payRate);

    double pay(double hoursWorked, double payRate) {
      double basePay = hoursWorked * payRate;
      return basePay + overtimePay(hoursWorked, payRate);
    }
  }
}

用例項域代替序數

這個其實就是上面的Database例子,以自己定的順序來確定,而不是用自帶的ordinal。

/**
 * @author ligz
 */
public enum Ensemble {
	SOLO, DUET, TRIO, QUARTET, QUINTET;
	
	public int numberOfMusicians() {
		return ordinal() + 1;
	}
}

用EnumSet代替位域

EnumSet底層是用位向量實現的

如果一個列舉型別的元素主要用在集合中,一般就使用int列舉模式,將2的不同倍數賦予每個常量

// Bit field enumeration constants - OBSOLETE!

public class Test {

public static final int STYLE_BOLD = 1 << 0; // 1

public static final int STYLE_ITALIC  = 1 << 1; // 2

public static final int STYLE_UNDERLINE = 1 << 2; // 4

public static final int STYLE_STRIKETHROUGH  = 1 << 3;  // 8

// Parameter is bitwise OR of zero or more SYTLE_ constants

public void applyStyles(int styles) { ... }

}

這種表示法讓你用OR位運算將幾個常量合併到一個集合中,稱作位域(bit field)。

text.applyStyles(STYLE_BOLD | STYLE_ITALIC);

位域表示法也允許利用位操作,有效地執行像union(聯合)和intersection(交集)這樣的集合操作。但位域有著int列舉常量的所有缺點,甚至更多。當位域以數字形式列印時,翻譯位域比翻譯簡單的int列舉常量要困難得多。甚至,要遍歷位域表示的所有元素也沒有很容易的方法。

有些程式設計師優先使用列舉而非int常量,他們在需要傳遞多組常量集時,仍然傾向於使用位域。其實沒有理由這麼做,因為還有更好地替代方法。java.util包提供了EnumSet類來有效的表示從單個列舉型別中提取的多個值得多個集合。這個類實現Set介面,提供了豐富的功能、型別安全性,以及可以從任何其他Set實現中得到的互用性。但是在內部具體的實現上,每個EnumSet就是用單個long來表示,因此它的效能比得上位域的效能。批處理,如removeAll何retainAll,都是利用位演算法來實現的,就像手工替位域實現的那樣。但是可以避免手工位操作時容易出現的錯誤以及不大雅觀的程式碼,因為EnumSet替你完成了這項艱鉅的工作。

下面是前一個範例改成用列舉代替位域後的程式碼,他更加簡短、更加清楚,也更加安全:

// EnumSet - a modern replacement for bit fields

public class Text {

public enum Style {BOLD , ITALIC , UNDERLINE , STRIKETHROUGH}

// Any Set could be passed in , but EnumSet is clearly best

public void applyStyles(Set<Style> styles) { ... }

}

下面是將EnumSet例項傳遞給applyStyles方法的客戶端程式碼。EnumSet提供了豐富的靜態工廠來輕鬆建立集合,其中一個如這個程式碼所示:

text.applyStyles(EnumSet.of(Style.BOLD , Style.ITALIC));

注意applyStyles方法採用的是Set<Style>而非EnumSet<Style>,雖然看起來好像所有的客戶端都可以將EnumSet傳到這個方法,但是最好還是接受介面型別而非接受實現型別。這是考慮到可能會有特殊的客戶端要傳遞一些其他的Set實現,並且沒有什麼明顯的缺點。

總而言之,正是因為列舉型別要用在集合(Set)中,所以沒有理由用位域來表示他。EnumSet類集位域的簡潔和效能優勢及列舉型別的所有優點於一身。實際上EnumSet有個缺點,即截止Java 1.6發行版本,他都無法建立不可變的EnumSet,但是這一點很可能在即將出現的版本中得到修正。同時,可以用Collections.unmodifiableSet將EnumSet封裝起來,但是間接性和效能會受到影響。

用EnumMap代替序數索引

// 將集合放到一個按照型別的序數進行索引的陣列中來實現  替換
    Herb[] garden = { new Herb("1", Type.ANNUAL), new Herb("2", Type.BIENNIAL), new Herb("3", Type.PERENNTAL) };

    Set<Herb>[] herbsByType = (Set<Herb>[])new Set[Herb.Type.values().length];
    for(int i = 0;i<herbsByType.length;i++){
        herbsByType[i] = new HashSet<Herb>();
    }
    for(Herb h:garden){
        herbsByType[h.type.ordinal()].add(h);
    }

變為

Herb[] garden = { new Herb("1", Type.ANNUAL), new Herb("2", Type.BIENNIAL), new Herb("3", Type.PERENNTAL) };
    Map<Herb.Type, Set<Herb>> herbsByType = new EnumMap<Herb.Type,Set<Herb>>(Herb.Type.class);
    for(Herb.Type t:Herb.Type.values()){
        herbsByType.put(t, new HashSet<Herb>());
    }
    for(Herb h:garden)
        herbsByType.get(h.type).add(h); 
    System.out.println(herbsByType);

用介面模擬可伸縮的列舉

/**
 * 雖然列舉型別是不能擴充套件的 , 但是可以通過介面類表示API中的操作的介面型別 . 
 * 你可以定義一個功能完全不同的列舉型別來實現這個介面 .  
 */
public interface Operation {
    double apply(double x,double y);
}
//它的一個實現類
public enum BasicOperation implements Operation {   
    PLUS("+"){      
        public double apply(double x, double y) {           
            return x + y;
        }
    },
    MINUS("-"){ 
        public double apply(double x, double y) {           
            return x - y;
        }
    };  
    private final String symbol;
    BasicOperation(String symbol) {
        this.symbol = symbol;
    }   
    @Override
    public String toString(){
        return symbol;
    }
}
//他的另一個實現類
public enum ExtendedOperation implements Operation{
    Exp("^"){
        public double apply(double x,double y){
            //次冪計算
            return Math.pow(x, y);
        }
    },
    REMAINDER("%"){
        public double apply(double x,double y){
            return x % y;
        }
    };

    private final String symbol;
    ExtendedOperation(String symbol) {
        this.symbol = symbol;
    }
    @Override
    public String toString(){
        return symbol;
    }
}