?
三十、用enum代替int常量:
?? ?? 枚舉類型是指由一組固定的常量組成合法值的類型,該特征是在Java 1.5 中開始被支持的,之前的Java代碼都是通過“公有靜態(tài)常量域字段”的方法來簡單模擬枚舉的,如:
? ? ? 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;
? ? ? 這樣的寫法是比較脆弱的。首先是沒有提供相應(yīng)的類型安全性,如兩個(gè)邏輯上不相關(guān)的常量值之間可以進(jìn)行比較或運(yùn)算(APPLE_FUJI - ORANGE_TEMPLE),再有就是常量int是編譯時(shí)常量,被直接編譯到使用他們的客戶端中。如果與該常量關(guān)聯(lián)的int發(fā)生了變化,客戶端就必須重新編譯。如果沒有重新編譯,程序還是可以執(zhí)行,但是他們的行為將不確定。
? ? ? 下面我們來看一下Java 1.5 中提供的枚舉的聲明方式:
? ? ? public enum Apple { FUJI, PIPPIN, GRANNY_SMITH }
? ? ? public enum Orange { NAVEL, TEMPLE, BLOOD }
? ? ? 和“公有靜態(tài)常量域字段”不同的是,如果函數(shù)的參數(shù)是枚舉類型,如Apple,那么他的實(shí)際值只能來自于該枚舉所聲明的枚舉值,即FUJI, PIPPIN, GRANNY_SMITH。如果試圖將Apple和Orange中的枚舉值進(jìn)行比較,將會(huì)導(dǎo)致編譯錯(cuò)誤。
? ? ? 和C/C++中提供的枚舉不同的是,Java中允許在枚舉中添加任意的方法和域,并實(shí)現(xiàn)任意的接口。下面先給出一個(gè)帶有域方法和域字段的枚舉聲明:
1
public
enum
Planet {
2
MERCURY(3.302e+23,2.439e6),
3
VENUS(4.869e+24,6.052e6),
4
EARTH(5.975e+24,6.378e6),
5
MARS(6.419e+23,3.393e6),
6
JUPITER(1.899e+27,7.149e7),
7
SATURN(5.685e+26,6.027e7),
8
URANUS(8.683e+25,2.556e7),
9
NEPTUNE(1.024e+26,2.477e7);
10
private
final
double
mass;
//
千克
11
private
final
double
radius;
//
米
12
private
final
double
surfaceGravity;
13
private
static
final
double
G = 6.67300E-11;
14
Planet(
double
mass,
double
radius) {
15
this
.mass = mass;
16
this
.radius = radius;
17
surfaceGravity = G * mass / (radius * radius);
18
}
19
public
double
mass() {
20
return
mass;
21
}
22
public
double
radius() {
23
return
radius;
24
}
25
public
double
surfaceGravity() {
26
return
surfaceGravity;
27
}
28
public
double
surfaceWeight(
double
mass) {
29
return
mass * surfaceGravity;
30
}
31
}
? ? ? 在上面的枚舉示例代碼中,已經(jīng)將數(shù)據(jù)和枚舉常量關(guān)聯(lián)起來了,因此需要聲明實(shí)例域字段,同時(shí)編寫一個(gè)帶有數(shù)據(jù)并將數(shù)據(jù)保存在域中的構(gòu)造器。枚舉天生就是不可變的,因此所有的域字段都應(yīng)該為final的。下面看一下該枚舉的應(yīng)用示例:
1
public
class
WeightTable {
2
public
static
void
main(String[] args) {
3
double
earthWeight = Double.parseDouble(args[0]);
4
double
mass = earthWeight/Planet.EARTH.surfaceGravity();
5
for
(Planet p : Planet.values())
6
System.out.printf("Weight on %s is %f%n",p,p.surfaceWeight(mass));
7
}
8
}
9
//
Weight on MERCURY is 66.133672
10
//
Weight on VENUS is 158.383926
11
//
Weight on EARTH is 175.000000
12
//
Weight on MARS is 66.430699
13
//
Weight on JUPITER is 442.693902
14
//
Weight on SATURN is 186.464970
15
//
Weight on URANUS is 158.349709
16
//
Weight on NEPTUNE is 198.846116
?? ?? 枚舉的靜態(tài)方法values()將按照聲明順序返回他的值數(shù)組。枚舉的toString方法返回每個(gè)枚舉值的聲明名稱。
? ? ? 在實(shí)際的編程中,我們常常需要針對不同的枚舉常量提供不同的數(shù)據(jù)操作行為,見如下代碼:
1
public
enum
Operation {
2
PLUS,MINUS,TIMES,DIVIDE;
3
double
apply(
double
x,
double
y) {
4
switch
(
this
) {
5
case
PLUS:
return
x + y;
6
case
MINUS:
return
x - y;
7
case
TIMES:
return
x * y;
8
case
DIVIDE:
return
x / y;
9
}
10
throw
new
AssertionError("Unknown op: " +
this
);
11
}
12
}
?? ?? 上面的代碼已經(jīng)表達(dá)出這種根據(jù)不同的枚舉值,執(zhí)行不同的操作。但是上面的代碼在設(shè)計(jì)方面確實(shí)存在一定的缺陷,或者說漏洞,如果我們新增枚舉值的時(shí)候,所有和apply類似的域函數(shù),都需要進(jìn)行相應(yīng)的修改,如有遺漏將會(huì)導(dǎo)致異常的拋出。幸運(yùn)的是,Java的枚舉提供了一種更好的方法可以將不同的行為與每個(gè)枚舉常量關(guān)聯(lián)起來:在枚舉類型中聲明一個(gè)抽象的apply方法,并在特定于常量的類主體中,用具體的方法覆蓋每個(gè)常量的抽象apply方法,如:
1
public
enum
Operation {
2
PLUS {
double
apply(
double
x,
double
y) {
return
x + y;} },
3
MINUS {
double
apply(
double
x,
double
y) {
return
x - y;} },
4
TIMES {
double
apply(
double
x,
double
y) {
return
x * y;} },
5
DIVIDE {
double
apply(
double
x,
double
y) {
return
x / y;} };
6
abstract
double
apply(
double
x,
double
y);
7
}
? ? ? 這樣在添加新枚舉常量時(shí)就不會(huì)輕易忘記提供相應(yīng)的apply方法了。我們在進(jìn)一步看一下如何將枚舉常量和特定的數(shù)據(jù)進(jìn)行關(guān)聯(lián),見如下代碼:
1
public
enum
Operation {
2
PLUS("+") {
double
apply(
double
x,
double
y) {
return
x + y;} },
3
MINUS("-") {
double
apply(
double
x,
double
y) {
return
x - y;} },
4
TIMES("*") {
double
apply(
double
x,
double
y) {
return
x * y;} },
5
DIVIDE("/") {
double
apply(
double
x,
double
y) {
return
x / y;} };
6
private
final
String symbol;
7
Operation(String symbol) {
8
this
.symbol = symbol;
9
}
10
@Override
public
String toString() {
11
return
symbol;
12
}
13
abstract
double
apply(
double
x,
double
y);
14
}
? ? ? 下面給出以上代碼的應(yīng)用示例:
1
public
static
void
main(String[] args) {
2
double
x = Double.parseDouble(args[0]);
3
double
y = Double.parseDouble(args[1]);
4
for
(Operation op : Operation.values())
5
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
6
}
7
}
8
//
2.000000 + 4.000000 = 6.000000
9
//
2.000000 - 4.000000 = -2.000000
10
//
2.000000 * 4.000000 = 8.000000
11
//
2.000000 / 4.000000 = 0.500000
?? ?? 沒有類型有一個(gè)自動(dòng)產(chǎn)生的valueOf(String)方法,他將常量的名字轉(zhuǎn)變?yōu)槊杜e常量本身,如果在枚舉中覆蓋了toString方法(如上例),就需要考慮編寫一個(gè)fromString方法,將定制的字符串表示法變回相應(yīng)的枚舉,見如下代碼:
1
public
enum
Operation {
2
PLUS("+") {
double
apply(
double
x,
double
y) {
return
x + y;} },
3
MINUS("-") {
double
apply(
double
x,
double
y) {
return
x - y;} },
4
TIMES("*") {
double
apply(
double
x,
double
y) {
return
x * y;} },
5
DIVIDE("/") {
double
apply(
double
x,
double
y) {
return
x / y;} };
6
private
final
String symbol;
7
Operation(String symbol) {
8
this
.symbol = symbol;
9
}
10
@Override
public
String toString() {
11
return
symbol;
12
}
13
abstract
double
apply(
double
x,
double
y);
14
//
新增代碼
15
private
static
final
Map<String,Operation> stringToEnum =
new
HashMap<String,Operation>();
16
static
{
17
for
(Operation op : values())
18
stringToEnum.put(op.toString(),op);
19
}
20
public
static
Operation fromString(String symbol) {
21
return
stringToEnum.get(symbol);
22
}
23
}
? ? ? 需要注意的是,我們無法在枚舉常量構(gòu)造的時(shí)候?qū)⒆陨矸湃氲組ap中,這樣會(huì)導(dǎo)致編譯錯(cuò)誤。與此同時(shí),枚舉構(gòu)造器不可以訪問枚舉的靜態(tài)域,除了編譯時(shí)的常量域之外。
?? ?
三十一、用實(shí)例域代替序數(shù):
?? ?? Java中的枚舉提供了ordinal()方法,他返回每個(gè)枚舉常量在類型中的數(shù)字位置,如:
1
public
enum
Color {
2
WHITE,RED,GREEN,BLUE,ORANGE,BLACK;
3
public
int
indexOfColor() {
4
return
ordinal() + 1;
5
}
6
}
? ? ? 上面的枚舉中提供了一個(gè)獲取顏色索引的方法(indexOfColor),該方法將返回顏色值在枚舉類型中的聲明位置,如果我們的外部程序依賴了該順序值,那么這將會(huì)是非常危險(xiǎn)和脆弱的,因?yàn)橐坏┻@些枚舉值的位置出現(xiàn)變化,或者在已有枚舉值的中間加入新的枚舉值時(shí),都將導(dǎo)致該索引值的變化。該條目推薦使用實(shí)例域的方式來代替枚舉提供的序數(shù)值,見如下修改后的代碼:
1
public
enum
Color {
2
WHITE(1),RED(2),GREEN(3),ORANGE(4),BLACK(5);
3
private
final
int
indexOfColor;
4
Color(
int
index) {
5
this
.indexOfColor = index;
6
}
7
public
int
indexOfColor() {
8
return
indexOfColor;
9
}
10
}
? ? ? Enum規(guī)范中談到ordinal時(shí)這么寫道:“大多數(shù)程序員都不需要這個(gè)方法。它是設(shè)計(jì)成用于像EnumSet和EnumMap這種基于枚舉的通用數(shù)據(jù)結(jié)構(gòu)的。”除非你在編寫的是這種數(shù)據(jù)結(jié)構(gòu),否則最好避免使用ordinal()方法。
?? ?
三十二、用EnumSet代替位域:
? ? ? 下面的代碼給出了位域的實(shí)現(xiàn)方式:
1
public
class
Text {
2
public
static
final
int
STYLE_BOLD = 1 << 0;
3
public
static
final
int
STYLE_ITALIC = 1 << 1;
4
public
static
final
int
STYLE_UNDERLINE = 1 << 2;
5
public
static
final
int
STYLE_STRIKETHROUGH = 1 << 3;
6
public
void
applyStyles(
int
styles) { ... }
7
}
? ? ? 這種表示法讓你用OR位運(yùn)算將幾個(gè)常量合并到一個(gè)集合中,使用方式如下:
? ? ? text.applyStyles(Text.STYLE_BOLD | Text.STYLE_ITALIC);
? ? ? Java中提供了EnumSet類,該類繼承自Set接口,同時(shí)也提供了豐富的功能,類型安全性,以及可以從任何其他Set實(shí)現(xiàn)中得到的互用性。但是在內(nèi)部具體實(shí)現(xiàn)上,沒有EnumSet內(nèi)容都表示為位矢量。如果底層的枚舉類型有64個(gè)或者更少的元素,整個(gè)EnumSet就用單個(gè)long來表示,因此他的性能也是可以比肩位域的。與此同時(shí),他提供了大量的操作方法,其實(shí)現(xiàn)也是基于位操作的,但是相比于手工位操作,由于EnumSet替我們承擔(dān)了這部分的開發(fā),從而也避免了一些容易出現(xiàn)的低級錯(cuò)誤,代碼的美觀程度也會(huì)有所提升,見如下修改的代碼:
1
public
class
Text {
2
public
enum
Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }
3
public
void
applyStyles(Set<Style> styles) { ... }
4
}
? ? ? 新的使用方式如下:
? ? ? text.applyStyles(EnumSet.of(Style.BOLD,Style.ITALIC));
? ? ? 需要說明的是,EnumSet提供了豐富的靜態(tài)工廠來輕松創(chuàng)建集合。
?
三十三、用EnumMap代替序數(shù)索引:
?? ?? 前面的條目已經(jīng)給出了盡量不要直接使用枚舉的ordinal()方法的原因,這里就不在做過多的贅述了。在這個(gè)條目中,只是再一次給出了ordinal()的典型用法,與此同時(shí)也再一次提供了一個(gè)更為合理的解決方案用于替換ordinal()方法,從而進(jìn)一步證明我們在編碼過程中應(yīng)該盡可能減少對枚舉中ordinal()函數(shù)的依賴。見如下代碼:
1
public
class
Herb {
2
public
enum
Type { ANNUAL, PERENNIAL, BIENNIAL }
3
private
final
String name;
4
private
final
Type type;
5
Herb(String name, Type type) {
6
this
.name = name;
7
this
.type = type;
8
}
9
@Override
public
String toString() {
10
return
name;
11
}
12
}
13
public
static
void
main(String[] args) {
14
Herb[] garden = getAllHerbsFromGarden();
15
Set<Herb> herbsByType = (Set<Herb>[])
new
Set[Herb.Type.values().length];
16
for
(
int
i = 0; i < herbsByType.length; ++i) {
17
herbsByType[i] =
new
HashSet<Herb>();
18
}
19
for
(Herb h : garden) {
20
herbsByType[h.type.ordinal()].add(h);
21
}
22
for
(
int
i = 0; i < herbsByType.length; ++i) {
23
System.out.printf("%s: %s%n",Herb.Type.values()[i],herbByType[i]);
24
}
25
}
?? ?? 這里我需要簡單描述一下上面代碼的應(yīng)用場景:在一個(gè)花園里面有很多的植物,它們被分成3類,分別是一年生(ANNUAL)、多年生(PERENNIAL)和兩年生(BIENNIAL),正好對應(yīng)著Herb.Type中的枚舉值。現(xiàn)在我們需要做的是遍歷花園中的每一個(gè)植物,并將這些植物分為3類,最后再將分類后的植物分類打印出來。下面將提供另外一種方法,即通過EnumMap來實(shí)現(xiàn)和上面代碼相同的邏輯:
1
public
static
void
main(String[] args) {
2
Herb[] garden = getAllHerbsFromGarden();
3
Map<Herb.Type,Set<Herb>> herbsByType =
4
new
EnumMap<Herb.Type,Set<Herb>>(Herb.Type.
class
);
5
for
(Herb.Type t : Herb.Type.values()) {
6
herbssByType.put(t,
new
HashSet<Herb>());
7
}
8
for
(Herb h : garden) {
9
herbsByType.get(h.type).add(h);
10
}
11
System.out.println(herbsByType);
12
}
?? ?? 和之前的代碼相比,這段代碼更加清晰,也更加安全,運(yùn)行效率方面也是可以與使用ordinal()的方式想媲美的。
三十四、用接口模擬可伸縮的枚舉:
?? ?? 枚舉是無法被擴(kuò)展(extends)的,這是一個(gè)無法回避的事實(shí)。如果我們的操作中存在一些基礎(chǔ)操作,如計(jì)算器中的基本運(yùn)算類型(加減乘除)。然而對于有些用戶來講,他們也可以使用更高級的操作,如求冪和求余等。針對這樣的需求,該條目提出了一種非常巧妙的設(shè)計(jì)方案,即利用枚舉可以實(shí)現(xiàn)接口這一事實(shí),我們將API的參數(shù)定義為該接口,而不是具體的枚舉類型,見如下代碼:
1
public
interface
Operation {
2
double
apply(
double
x,
double
y);
3
}
4
public
enum
BasicOperation
implements
Operation {
5
PLUS("+") {
6
public
double
apply(
double
x,
double
y) {
return
x + y; }
7
},
8
MINUS("-") {
9
public
double
apply(
double
x,
double
y) {
return
x - y; }
10
},
11
TIMES("*") {
12
public
double
apply(
double
x,
double
y) {
return
x * y; }
13
},
14
DIVIDE("/") {
15
public
double
apply(
double
x,
double
y) {
return
x / y; }
16
};
17
private
final
String symbol;
18
BasicOperation(String symbol) {
19
this
.symbol = symbol;
20
}
21
@Override
public
String toString() {
22
return
symbol;
23
}
24
}
25
public
enum
ExtendedOperation
implements
Operation {
26
EXP("^") {
27
public
double
apply(
double
x,
double
y) {
28
return
Math.pow(x,y);
29
}
30
},
31
REMAINDER("%") {
32
public
double
apply(
double
x,
double
y) {
33
return
x % y;
34
}
35
};
36
private
final
String symbol;
37
ExtendedOperation(String symbol) {
38
this
.symbol = symbol;
39
}
40
@Override
public
String toString() {
41
return
symbol;
42
}
43
}
????? 通過以上的代碼可以看出,在任何可以使用BasicOperation的地方,我們也同樣可以使用ExtendedOperation,只要我們的API是基于Operation接口的,而非BasicOperation或ExtendedOperation。下面為以上代碼的應(yīng)用示例:
1
public
static
void
main(String[] args) {
2
double
x = Double.parseDouble(args[0]);
3
double
y = Double.parseDouble(args[1]);
4
test(ExtendedOperation.
class
,x,y);
5
}
6
private
static
<T
extends
Enum<T> & Operation>
void
test(
7
Class<T> opSet,
double
x,
double
y) {
8
for
(Operation op : opSet.getEnumConstants()) {
9
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
10
}
11
}
????? 注意,參數(shù)Class<T> opSet將推演出類型參數(shù)的實(shí)際類型,即上例中的ExtendedOperation。與此同時(shí),test函數(shù)的參數(shù)類型限定確保了類型參數(shù)既是枚舉類型又是Operation的實(shí)現(xiàn)類,這正是遍歷元素和執(zhí)行每個(gè)元素相關(guān)聯(lián)的操作所必須的。
?
更多文章、技術(shù)交流、商務(wù)合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動(dòng)力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長非常感激您!手機(jī)微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

