《Effective Java》筆記(下)
Enums and Annotations
- Item 30: Use enums instead of int constants
- 類型安全
- 可以為常量提供數(shù)據(jù)和方法的綁定
- 可以遍歷
- 實現(xiàn)建議
- 如果是通用的,應(yīng)該定義為top level enum蛹疯,否則應(yīng)定義為內(nèi)部類
- constant-specific method implementations
```java
// Enum type with constant-specific method implementations
public enum Operation {
PLUS { double apply(double x, double y){return x + y;} },
MINUS { double apply(double x, double y){return x - y;} },
TIMES { double apply(double x, double y){return x * y;} },
DIVIDE { double apply(double x, double y){return x / y;} };
abstract double apply(double x, double y);
}
```
+ 結(jié)合constant-specific data
```java
// Enum type with constant-specific class bodies and data
public enum Operation {
PLUS("+") {
double apply(double x, double y) { return x + y; }
},
MINUS("-") {
double apply(double x, double y) { return x - y; }
},
TIMES("*") {
double apply(double x, double y) { return x * y; }
},
DIVIDE("/") {
double apply(double x, double y) { return x / y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
@Override public String toString() { return symbol; }
abstract double apply(double x, double y);
}
```
+ If switch statements on enums are not a good choice for implementing con- stant-specific behavior on enums, what are they good for? Switches on enums are good for augmenting external enum types with constant-specific behavior.
- A minor performance disadvantage of enums over int constants is that there is a space and time cost to load and initialize enum types.
- 所以吗伤,在安卓設(shè)備(手機诈唬、平板)上,應(yīng)該避免使用enum伶丐,減小空間和時間的開銷
- Item 31: Use instance fields instead of ordinals
- 每個enum的常量都有一個
ordinal()
方法獲取其在該enum類型中的位置明吩,但該方法只應(yīng)該在實現(xiàn)EnumSet
,EnumMap
等類型的時候被使用皆串,其他情形都不應(yīng)該被使用 - 如果需要為每一個常量綁定一個數(shù)據(jù)入偷,可以使用instance field實現(xiàn)追驴,如果需要綁定方法,則可以用constant-specific method implementations疏之,參考上一個item
- Item 32: Use EnumSet instead of bit fields
- bit fields的方式不優(yōu)雅殿雪、容易出錯、沒有類型安全性
- EnumSet則沒有這些缺點锋爪,而且對于大多數(shù)enum類型來說丙曙,其性能都和bit field相當(dāng)
- 通用建議:聲明變量時,不要用實現(xiàn)類型其骄,應(yīng)該用接口類型亏镰,例如,應(yīng)該用
List<Integer>
而不是ArrayList<Integer>
- EnumSet并非immutable的拯爽,可以通過
Conllections.unmodifiableSet
來封裝為immutable索抓,但是代碼簡潔性與性能都將受到影響 - Item 33: Use EnumMap instead of ordinal indexing
- 同前文所述,應(yīng)該避免使用ordinal毯炮。當(dāng)需要用enum作為下標(biāo)從數(shù)組獲取數(shù)據(jù)時逼肯,可以換個角度思考,以enum作為key從map里面獲取數(shù)據(jù)桃煎。
- 數(shù)組和泛型不兼容篮幢,因此使用數(shù)組也會導(dǎo)致編譯警告;而且ordinal的值本來就不是表達index含義的为迈,極易導(dǎo)致隱蔽錯誤
- EnumMap內(nèi)部使用數(shù)組實現(xiàn)三椿,因此性能和數(shù)組相當(dāng)
- 使用數(shù)組也會導(dǎo)致程序可擴展性下降,考慮以下兩種實現(xiàn)
// Using ordinal() to index array of arrays - DON'T DO THIS!
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// Rows indexed by src-ordinal, cols by dst-ordinal
private static final Transition[][] TRANSITIONS = {
{ null, MELT, SUBLIME },
{ FREEZE, null, BOIL },
{ DEPOSIT, CONDENSE, null }
};
// Returns the phase transition from one phase to another
public static Transition from(Phase src, Phase dst) {
return TRANSITIONS[src.ordinal()][dst.ordinal()];
}
}
}
// Using a nested EnumMap to associate data with enum pairs
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
final Phase src;
final Phase dst;
Transition(Phase src, Phase dst) {
this.src = src;
this.dst = dst;
}
// Initialize the phase transition map
private static final Map<Phase, Map<Phase,Transition>> m =
new EnumMap<Phase, Map<Phase,Transition>>(Phase.class);
static {
for (Phase p : Phase.values())
m.put(p,new EnumMap<Phase,Transition>(Phase.class));
for (Transition trans : Transition.values())
m.get(trans.src).put(trans.dst, trans);
}
public static Transition from(Phase src, Phase dst) {
return m.get(src).get(dst);
}
}
}
當(dāng)需要增加Phase
時曲尸,前者需要謹(jǐn)慎地修改TRANSITIONS
數(shù)組的內(nèi)容(這一步驟容易出錯)赋续,而后者則只需要增加相應(yīng)Transition
即可,from
函數(shù)的邏輯完全不受影響另患。
- Item 34: Emulate extensible enums with interfaces
- 當(dāng)enum遇到可擴展性時,總是一個糟糕的問題蛾绎;擴展類是基礎(chǔ)類的實例昆箕,但反過來不是,這一點很讓人困惑租冠;想要枚舉所有基礎(chǔ)類和擴展類的enum對象時鹏倘,并沒有一個很好地辦法;
- 而對于可擴展性的需求顽爹,是真實存在的纤泵,例如:operation codes (opcodes)
- 實現(xiàn)方式是通過定義一個接口,enum類型(基礎(chǔ)與擴展)均實現(xiàn)該接口镜粤,而在使用enum的地方捏题,接收這個接口作為參數(shù)
- enum類型是不可擴展的玻褪,但是interface具備可擴展性,如果API使用接口而非實現(xiàn)去代表operation公荧,API就有了可擴展性
- 泛型高級用法:
<T extends Enum<T> & Operation> ... Class<T>
带射,T類型是enum類型,且是Operation
子類 - 這一方式的不足:enum類型對接口的實現(xiàn)是不能繼承的
- Item 35: Prefer annotations to naming patterns
- 在1.5之前循狰,naming patterns很常見窟社,在JUnit中都是這樣,例如要求測例方法一
test
開頭 - naming patterns有很多問題
- 拼寫錯誤不能及時發(fā)現(xiàn)
- 無法保證naming patterns只在正確的場景使用绪钥,例如可能有人以
test
開頭命名測例類灿里,方法卻沒有,JUnit則不會運行測例 - 沒有值/類型信息程腹,編譯器無法提前發(fā)現(xiàn)問題
- 使用annotations可以很好的解決這些問題匣吊,但是annotations的功能也是有限的
-
@Retention(RetentionPolicy.RUNTIME)
能限定其保留時期 -
@Target(ElementType.METHOD)
能限定其應(yīng)用的程序元素 - 還有其他meta-annotations,如
@IntDef
- annotations接收的參數(shù)如果是數(shù)組跪楞,為其賦值一個單獨的元素也是合法的
- Item 36: Consistently use the Override annotation
-
@Override
會使得重寫的準(zhǔn)確性得到檢查 - 重載和重寫的區(qū)別:一個只是函數(shù)名一樣缀去,通過參數(shù)列表決定執(zhí)行哪個版本,是編譯時多態(tài)甸祭;一個是通過虛函數(shù)機制實現(xiàn)缕碎,是運行時多態(tài);
- Item 37: Use marker interfaces to define types
- 定義一個空的接口池户,表明某個類型的屬性咏雌,例如
Serializable
- 另一種方式是使用annotation,表明者其具有某種屬性
- marker interface的優(yōu)點
- 定義了一個類型校焦,可以進行instanceof判斷赊抖,可以聲明參數(shù)類型
- 比annotation更簡潔
- marker annotation的優(yōu)點
- 當(dāng)一個類型(通過interface或者annotation)被聲明后,如果想要加入更多的信息寨典,annotation更方便氛雪,即annotation對修改是開放的,因為它的屬性可以有默認(rèn)值耸成,而interface則不行报亩,定義了方法就必須實現(xiàn)
- annotation可以被應(yīng)用到更多代碼的元素中,不僅僅是類型
- 實現(xiàn)建議
- 如果僅僅只應(yīng)用于類型井氢,則應(yīng)該優(yōu)先考慮annotation
- 如果希望mark的對象被限定于某個接口的實例(即為一個接口增加另外一種語義弦追,卻不改變其API),可以考慮使用marker interface
函數(shù)
- Item 38: Check parameters for validity
- 一個函數(shù)(包括構(gòu)造函數(shù))首先要做的事情就是驗證參數(shù)合法性花竞,如果不合法則應(yīng)該拋出相應(yīng)異常劲件,這是對“盡早發(fā)現(xiàn)錯誤盡早拋出”原則的遵循,否則等到錯誤發(fā)生時將可能難以判斷錯誤的根源所在,甚至程序不會顯式報錯零远,而是執(zhí)行了錯誤的行為苗分,導(dǎo)致更嚴(yán)重的后果
- 不由被調(diào)用函數(shù)使用,而是存起來留作后用的參數(shù)遍烦,更加要檢查其合法性
- Javadoc里面應(yīng)該注明
@throw
項俭嘁,并說明原因 - 非公開的API(private或package private),則不應(yīng)該通過拋異常來報錯服猪,應(yīng)該采用
assert
供填,assert可以通過配置虛擬機參數(shù)開啟或關(guān)閉,如果關(guān)閉則不會被執(zhí)行 - 靈活運用罢猪,設(shè)計API時近她,就應(yīng)該盡量設(shè)計得通用一些,即可以接受更大范圍的參數(shù)膳帕,畢竟檢查參數(shù)也是有開銷的
- 另外可以考慮拋出
RuntimeException
的子類粘捎,因為這樣的異常不用放到函數(shù)的異常表中,函數(shù)的使用者也不用必須try-catch
或者throw
危彩,但doc一定要寫明 - Item 39: Make defensive copies when needed
- 編碼一大原則:永遠不要信任用戶(調(diào)用方)輸入的數(shù)據(jù)攒磨,也不要信任它們不會篡改返回的數(shù)據(jù),因此defensive copy很有必要
- 編寫一個類時汤徽,如果成員變量是mutable的娩缰,那么就需要在構(gòu)造函數(shù)(或者setter)中進行深拷貝,并且谒府,先拷貝拼坎,再驗證已拷貝數(shù)據(jù)的合法性(既不是先驗證,也不是驗證傳入的數(shù)據(jù)完疫,避免TOCTOU attack)
- 另外深拷貝時泰鸡,傳入對象的類如果不是final的,就不能用clone方法進行拷貝壳鹤,因為不能保證clone方法返回的就正好是這個類的實例(有可能會是惡意的子類)
- 為mutable成員提供getter方法時盛龄,返回前也要進行深拷貝,但此時可以用clone方法芳誓,因為我們確定成員就是我們想要的類的對象
- java內(nèi)建的Map, Set等容器讯嫂,實現(xiàn)上是沒有進行深拷貝的,因為是泛型兆沙,所以put進去或者get出來的時候,編譯期都不知道具體是什么類型莉掂,是無法調(diào)用構(gòu)造函數(shù)的葛圃,如果想要測試這一問題,需要確定key和value的類型都是mutable的,如果測
Map<String, Integer>
库正,那結(jié)果肯定是錯誤的曲楚,但如果測Map<StringBuilder, Date>
,就可以知道確實如此褥符;所以如果要把用戶傳入的數(shù)據(jù)放入Map龙誊,且key/value是mutable的,那么就需要在put之前進行深拷貝喷楣,否則可能會被用戶attack - 長度非零的數(shù)組都是mutable的
- 盡量使用immutable的成員就可以省去深拷貝帶來的性能開銷
- 如果確實信任用戶趟大,就可以把深拷貝省去,但一定要在文檔內(nèi)說明铣焊,例如:wrapper模式逊朽,如果用戶惡意,那損害的也就僅僅是其自身曲伊;或者用戶都是自己的代碼叽讳,可以確信安全。
- Item 40: Design method signatures carefully
- 命名要合理坟募,可理解:清除表達函數(shù)的功能岛蚤;符合常識;保持風(fēng)格一致懈糯;
- 類/接口的成員方法數(shù)量不要太多涤妒,否則會令人難以理解,而且不利于測試昂利、維護
- 不要隨便提供helper方法届腐,只有當(dāng)很有必要時才提供
- 避免過長參數(shù)列表(不多于4個),尤其是參數(shù)類型相同蜂奸,否則既難記(倒還好)犁苏,又可能引起隱晦的bug(傳入?yún)?shù)順序錯了,編譯不報錯扩所,運行時行為確是錯的)
- 可以通過把參數(shù)列表過長的方法拆分為幾個方法围详,但要避免導(dǎo)致方法過多
- 創(chuàng)建helper類,容納作用相關(guān)聯(lián)的的參數(shù)
- 類似于構(gòu)造對象的Builder模式祖屏,為函數(shù)的調(diào)用創(chuàng)建一個builder
- 參數(shù)類型助赞,使用interface,而不是實現(xiàn)類
- 對于起控制作用的參數(shù)袁勺,使用二值enum雹食,而不是boolean,便于擴展期丰;對于安卓來說群叶,可以通過
@IntDef
輔助定義int常量吃挑,模擬enum - Item 41: Use overloading judiciously
- 慎用重載,重載(overload)與重寫(override)的區(qū)別可以見上文街立,簡言之舶衬,前者編譯時多態(tài),后者運行時多態(tài)
- 重載是編譯時多態(tài)赎离,版本選擇在編譯期完成逛犹,根據(jù)編譯期參數(shù)的類型信息來進行決策
- 建議不要用參數(shù)類型來設(shè)計不同的重載版本,應(yīng)該通過參數(shù)列表長度梁剔,或者沒有父子類關(guān)系的不同參數(shù)類型虽画,例如接受int和float的類型渊啰,后者也還是可能會有問題
- Item 42: Use varargs judiciously
- varargs的原理是調(diào)用時首先創(chuàng)建一個數(shù)組盼樟,然后把傳入的參數(shù)放入數(shù)組,數(shù)組長度為參數(shù)個數(shù)
- 一個方法需要0或多個同類型數(shù)據(jù)這個需求很常見乐设,然而也有另一個很常見的需求:需要一個或多個同類型數(shù)據(jù)众雷,此時單純用varargs不太優(yōu)雅灸拍,可以讓方法先接受一個數(shù)據(jù),在接受一個varargs
- varargs最初是為了printf和反射設(shè)計的
- 可以通過把傳入?yún)?shù)從一個數(shù)組改為varargs砾省,來改良該方法(變得更靈活)鸡岗,而且對已有代碼“無影響”,
Arrays.asList
便是一個例子编兄,但接受varargs最初是為了打印數(shù)組內(nèi)容設(shè)計的轩性,而不是為了把多個數(shù)據(jù)變成一個List - Don’t retrofit every method that has a final array parameter; use varargs only when a call really operates on a variable-length sequence of values.
- 以下兩種函數(shù)聲明都可能會產(chǎn)生問題:
```java
ReturnType1 suspect1(Object... args) { }
<T> ReturnType2 suspect2(T... args) { }
```
如果傳入一個基本類型的數(shù)組進去(例如int[]),那么這兩個方法接受的都是一個int[][]狠鸳,即相當(dāng)于接受了一個只有一個元素的數(shù)組揣苏,而這個數(shù)組的數(shù)據(jù)類型是int[]!而如果傳入一個對象的數(shù)組件舵,則相當(dāng)于傳入了數(shù)組長度個數(shù)的varargs卸察。`Arrays.asList`方法就存在這個問題!
- varargs也存在性能影響铅祸,因為每次調(diào)用都會創(chuàng)建坑质、初始化一個數(shù)組。如果為了不失API靈活性临梗,同時大部分調(diào)用的參數(shù)個數(shù)都是有限個涡扼,例如03個,那么可以聲明5個重載版本盟庞,分別接受03個參數(shù)吃沪,另外加一個3個參數(shù)+varargs的版本
- Item 43: Return empty arrays or collections, not nulls
- 可能有人認(rèn)為返回null能減小內(nèi)存開銷,然:
- 永遠不要過度考慮性能問題什猖,只有當(dāng)profiling顯示瓶頸就是這里的時候巷波,再考慮性能優(yōu)化與代碼優(yōu)雅性的犧牲萎津,當(dāng)然,無副作用的優(yōu)化肯定盡早采納
- 可以每次需要返回空數(shù)組/集合時抹镊,返回同一個空數(shù)組/集合,這樣就只需要一次內(nèi)存分配
- Collection的
<T> T[] toArray(T[] a)
方法荤傲,可以每次調(diào)用時傳入一個空數(shù)組垮耳,因為該方法保證如果集合元素可以被放入提供的參數(shù)數(shù)組中,將不會分配新內(nèi)存遂黍,當(dāng)放不下時才會分配 - 下面實現(xiàn)返回集合的值的方式也是值得借鑒的
// The right way to return a copy of a collection
public List<Cheese> getCheeseList() {
if (cheesesInStock.isEmpty())
return Collections.emptyList(); // Always returns same list
else
return new ArrayList<Cheese>(cheesesInStock);
}
- Item 44: Write doc comments for all exposed API elements
- 對于API暴露的部分(類终佛、接口、方法雾家、成員等)铃彰,都應(yīng)該先寫好文檔;為了提高代碼的可維護性芯咧,未暴露的部分也應(yīng)該寫好文檔牙捉;
- 每個方法的文檔的內(nèi)容,應(yīng)該是描述該方法與調(diào)用者之間的約定敬飒,不必是實現(xiàn)細節(jié)邪铲,細節(jié)可以看代碼,約定則是使用者關(guān)心的東西无拗;設(shè)計為被繼承的類带到,方法文檔應(yīng)該描述該方法做了什么,而不是怎么做的英染;
- 方法的文檔中揽惹,應(yīng)該描述約定的前提條件,執(zhí)行后產(chǎn)生的影響四康,尤其是對于“系統(tǒng)”(或者說這個對象)狀態(tài)的影響搪搏;不符合前提條件的情形將拋出異常;
- 更多細節(jié)
-
@param
,@return
,@throws
描述不要句號結(jié)尾 -
@throws
的描述應(yīng)該以if開頭箭养,其他都應(yīng)該是名詞描述 -
@{code}
與@{literal}
- 有泛型時慕嚷,需要說明每個類型參數(shù)
- enum類型要為每個常量注釋含義
- annotation的定義,要為每個成員/參數(shù)注釋含義
- 線程安全性說明毕泌,可見性說明喝检,序列化說明
編程通用
- Item 45: Minimize the scope of local variables
- 在變量第一次使用的時候進行聲明,聲明時盡量就進行初始化
- 因此也更傾向于使用for-loop撼泛,而不是while-loop挠说,因為后者需要使用while-loop外定義的控制變量
- for-loop的終結(jié)條件變量n,也應(yīng)該在循環(huán)變量i初始化時計算愿题,避免重復(fù)計算
- 保持方法簡短损俭,一個方法只做一件事
- Item 46: Prefer for-each loops to traditional for loops
- 優(yōu)點之一:可以避免一些容易犯的bug
```java
// Can you spot the bug?
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
...
Collection<Suit> suits = Arrays.asList(Suit.values());
Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<Card>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
```
`i.next()`在內(nèi)層循環(huán)被調(diào)用了多次蛙奖。以下寫法則直觀且不易出錯:
```java
// Preferred idiom for nested iteration on collections and arrays
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
```
- 此種寫法不僅可用于集合和數(shù)組,任何實現(xiàn)
Iterable
接口的類都可以用于冒號后面的部分 - 缺點
- 有性能代價杆兵!一定會創(chuàng)建Iterator雁仲,對于安卓開發(fā),不建議如此琐脏。
- 不能在for-each語法中進行remove攒砖,用Iterator遍歷時,能remove
- 遍歷過程中替換原有元素
- Parallel iteration
- Item 47: Know and use the libraries
- don't reinvent the wheel
- 視野日裙!
- Item 48: Avoid float and double if exact answers are required
- float和double設(shè)計為用于科學(xué)計算吹艇,“精確近似”,需要確切結(jié)果的昂拂,不要使用受神,例如:貨幣相關(guān)!應(yīng)該使用BigDecimal, int, 或者long格侯。
- BigDecimal使用有些不方便鼻听,性能也比primitive類型低
- Item 49: Prefer primitive types to boxed primitives
- 兩種類型的區(qū)別
- boxed類型,除了包含數(shù)值外养交,還有不同的唯一標(biāo)示精算,即值一樣,對象可以不一樣碎连,這一點很重要灰羽!
- boxed類型,比primitive類型多一個值鱼辙,null
- boxed類型廉嚼,時間、空間效率均低一些
- caveats
- 有些操作會auto-unbox倒戏,例如:加減乘除怠噪,大小比較,但判等(
==
)不會杜跷! - Applying the
==
operator to boxed primitives is almost always wrong. - boxed類型傍念,值為null時,會unbox為什么呢葛闷?會拋出
NullPointerException
- 當(dāng)boxed和primitive出現(xiàn)在同一個運算中憋槐,boxed類型會auto-unbox(包括判等)
- 大量重復(fù)的box/unbox會導(dǎo)致性能大幅下降
- 使用場景與注意事項
- 放到標(biāo)準(zhǔn)集合里面,必須是boxed類型
- 作為類型參數(shù)(泛型)淑趾,必須是boxed類型
- auto-box是安全的阳仔,也能省去繁瑣的代碼,但是auto-unbox則可能引起隱蔽的錯誤
- Item 50: Avoid strings where other types are more appropriate
- Strings are poor substitutes for other value types. 只有當(dāng)數(shù)據(jù)確實就是文本時扣泊,才適合用String近范。
- Strings are poor substitutes for enum types.
- Strings are poor substitutes for aggregate types. 把一系列數(shù)據(jù)轉(zhuǎn)化為一個String(序列化)嘶摊,然后再反序列化,也應(yīng)該用Json评矩,如果自定義分隔符叶堆,既不優(yōu)雅,也不安全稚照。
- Strings are poor substitutes for capabilities. capability是一種稱呼蹂空,通常就是說不同的對象,憑借一個key去同一個地方保存果录、獲取數(shù)據(jù);如果用String咐熙,那么如果內(nèi)容相同弱恒,那key就會沖突,不安全棋恼;ThreadLocal的發(fā)展史*返弹。
- Item 51: Beware the performance of string concatenation
- 用
+
連接n個String,時間復(fù)雜度為O(n^2)
爪飘,因為String是immutable的义起,所以每次拼接都會拷貝兩者的內(nèi)容 - 使用
StringBuilder
進行拼接操作;不過對于安卓開發(fā)來說师崎,基本沒什么影響默终,因為在打包的過程中,這一優(yōu)化會自動完成犁罩; - Item 52: Refer to objects by their interfaces
- 如果有接口齐蔽,那么函數(shù)參數(shù)、返回值床估、成員變量含滴、局部變量,都應(yīng)該使用接口來保持對象的引用丐巫,只有在通過構(gòu)造函數(shù)創(chuàng)建對象時才應(yīng)該引用具體的實現(xiàn)類型谈况;面向接口編程更廣義的實踐;
- 面向接口編程使得程序更加靈活递胧,切換實現(xiàn)類非常簡單碑韵;但如果代碼功能/正確性依賴于實現(xiàn)類特有的特性,那么切換時就需要仔細考慮一下谓着;
- 當(dāng)然泼诱,如果對應(yīng)功能的接口不存在,那直接引用該類當(dāng)然是可以的赊锚;value type; class-based framework; 或者實現(xiàn)類提供了接口不存在的功能
- Item 53: Prefer interfaces to reflection
- 反射可以訪問私有成員
- 反射可以調(diào)用編譯時不存在的類的方法治筒,當(dāng)然需要運行時已經(jīng)加載
- 但是反射也是有代價的
- 編譯期的類型檢查完全失效屉栓,類型安全性喪失
- 反射代碼繁瑣且易出錯,當(dāng)然這一點有一些好的框架可以避免耸袜,例如JOOR
- 性能下降友多,反射調(diào)用性能會低很多
- 反射常用的場景
- class browsers, object inspectors, code analysis tools, and interpretive embedded systems, remote procedure call (RPC) systems
- 反射功能強大,也有一些不足堤框,如果合適利用域滥,還是非常方便的
- 例如編譯期有些類尚未獲得,但是如果有其父類/接口蜈抓,則可以聲明為父類/接口启绰,只通過反射創(chuàng)建實例,其余代碼都無需反射
- Item 54: Use native methods judiciously
- 設(shè)計之初的三大用途
- 訪問平臺相關(guān)的功能沟使,例如registries and file locks
- 訪問老的C/C++版本的庫委可,訪問老的數(shù)據(jù)
- 追求性能
- 近年來JVM/Java的發(fā)展,性能已有很大改善腊嗡,追求性能而使用JNI通常來說都已經(jīng)沒必要了
- JNI的劣勢
- 不安全着倾,內(nèi)存管理不受JVM控制了,溢出等問題都有可能發(fā)生了
- 平臺相關(guān)
- 難以調(diào)試
- Java和native層的交互是有開銷的
- native代碼比Java代碼更難懂
- 對于安卓應(yīng)用開發(fā)來說燕少,JNI還有一點就是隱藏實現(xiàn)卡者,Java代碼反編譯非常容易,而native代碼則難一些
- Item 55: Optimize judiciously
- 只有當(dāng)確實需要時客们,才考慮性能優(yōu)化崇决,當(dāng)然一些常見的范式,初次編碼時就應(yīng)該遵循
- Strive to write good programs rather than fast ones; speed will follow.
- Strive to avoid design decisions that limit performance.
- Consider the performance consequences of your API design decisions.
- It is a very bad idea to warp an API to achieve good performance.
- 當(dāng)確實需要優(yōu)化性能時:measure performance before and after each attempted optimization.
- 找到原因后镶摘,首先考慮的是算法的優(yōu)化嗽桩,然后是上層的優(yōu)化
- 在進行優(yōu)化前,對程序進行profiling凄敢,確定瓶頸碌冶,否則可能浪費精力反而性能下降
- Item 56: Adhere to generally accepted naming conventions
- 包名要體現(xiàn)出組件的層次結(jié)構(gòu),全小寫
- 公布到外部的涝缝,包名以公司/組織的域名開頭扑庞,例如:edu.cmu, com.sun
- ...
異常處理
- Item 57: Use exceptions only for exceptional conditions
- exceptions are, as their name implies, to be used only for exceptional conditions; they should never be used for ordinary control flow.
- A well-designed API must not force its clients to use exceptions for ordinary control flow.
- 如果一個類的某個方法,依賴于該類當(dāng)前處于某個特定狀態(tài)拒逮,則應(yīng)該提供一個單獨的狀態(tài)檢查方法罐氨,例如Iterator的next和hasNext方法
- 另外如果不提供狀態(tài)檢查方法,也可以讓方法在異常狀態(tài)下滩援,返回一個特定的非法值
- 如果該類被并發(fā)訪問栅隐,且訪問時未進行互斥處理,則必須使用返回非法值的方式;另外考慮到性能因素租悄,也更傾向于返回非法值谨究;其他情況下,都應(yīng)該使用狀態(tài)檢查方法泣棋,可讀性更好胶哲,更容易檢查錯誤;
- Item 58: Use checked exceptions for recoverable conditions and runtime exceptions for programming errors
- use checked exceptions for conditions from which the caller can reasonably be expected to recover.
- unchecked exception:
RuntimeException
,Error
通常都不需要潭辈、也不應(yīng)該catch - Use runtime exceptions to indicate programming errors. 通常用于表示程序運行的狀態(tài)違背了前提條件鸯屿,違背了API的約定
- all of the unchecked throwables you implement should subclass RuntimeException
- Item 59: Avoid unnecessary use of checked exceptions
- 如果即便合理的調(diào)用了API也會遇到異常情形,并且捕獲異常之后能夠進行一些有意義的操作把敢,才應(yīng)該使用checked exception寄摆,其他情況下都應(yīng)該使用RuntimeException
- 通常,如果一個方法會拋出checked exception修赞,都可以將其拆分為兩個方法冰肴,一個用于判斷是否會拋出異常,另一部分用于處理正常情況榔组,如果不符合約定,就拋出RuntimeException联逻,這樣使得API更易用搓扯,也更靈活;但是要考慮狀態(tài)檢查和執(zhí)行之間包归,是否可能從外部其他線程修改對象的狀態(tài)锨推;
- Item 60: Favor the use of standard exceptions
- IllegalArgumentException, IllegalStateException, NullPointerException, IndexOutOfBoundsException, ConcurrentModificationException, UnsupportedOperationException
- Item 61: Throw exceptions appropriate to the abstraction
- exception translation: higher layers should catch lower-level exceptions and, in their place, throw exceptions that can be explained in terms of the higher-level abstraction.
```java
// Exception Translation
try {
// Use lower-level abstraction to do our bidding
...
} catch(LowerLevelException e) {
throw new HigherLevelException(...);
}
```
- While exception translation is superior to mindless propagation of excep- tions from lower layers, it should not be overused.
- Item 62: Document all exceptions thrown by each method
- Always declare checked exceptions individually, and document precisely the conditions under which each one is thrown using the Javadoc @throws tag. 不要通過聲明拋出多個異常的父類來實現(xiàn)拋出多種異常的效果。
- 要為每個方法可能拋出的unchecked exception寫文檔公壤,但是不要將這些異常放到方法聲明的異常表中去换可。便于API使用者區(qū)分checked和unchecked exception。
- 如果一個類的很多方法都拋出同一個異常厦幅,那么可以將文檔放到class doc中沾鳄,而不是method doc中。
- Item 63: Include failure-capture information in detail messages
- To capture the failure, the detail message of an exception should contain the values of all parameters and fields that “contributed to the exception.”
- 良好設(shè)計的Exception類确憨,應(yīng)該把它需要的詳細信息都作為構(gòu)造函數(shù)的參數(shù)译荞,而不是統(tǒng)一接收String參數(shù);這樣將把生成有意義的detail信息的任務(wù)集中在了Exception類本身休弃,而不是其使用者吞歼。
- checked exception可以為failure-capture information提供訪問方法,以便于使用者在程序上進行恢復(fù)處理塔猾;雖然unchecked exception通常不會在程序中進行恢復(fù)篙骡,但是提供同樣的方法也是建議的做法。
- Item 64: Strive for failure atomicity
- Generally speaking, a failed method invocation should leave the object in the state that it was in prior to the invocation. 滿足此屬性的方法稱為 failure atomic。
- immutable對象是最簡單的實現(xiàn)方法
- mutable對象要達到此效果糯俗,就需要在進行操作前尿褪,對所有的參數(shù)、field進行檢查
- 有可能無法在函數(shù)的第一部分進行檢查叶骨,但是一定要在對對象進行修改之前進行檢查
- 還有一種不太常見的方式:函數(shù)內(nèi)部捕獲異常茫多,異常發(fā)生之后先回退對象的狀態(tài),再把異常拋出去
- 還可以先創(chuàng)建一個臨時的對象忽刽,在臨時對象上進行操作天揖,成功后替換原對象的值
- 有的情況下,failure atomic是不可能的跪帝,所以也就沒必要為此做出努力了
- 有的情況下今膊,為了failure atomic,會增加很多額外的開銷伞剑、復(fù)雜度斑唬,也就不太必要了
- 當(dāng)方法不滿足failure atomic時,需要在文檔中進行說明
- Item 65: Don’t ignore exceptions
- An empty catch block defeats the purpose of exceptions
- At the very least, the catch block should contain a comment explaining why it is appropriate to ignore the exception.
- 忽略異常黎泣,可能導(dǎo)致程序在其他不相關(guān)的地方失敗/崩潰恕刘,這時將很難找到/解決根本問題
并發(fā)
- Item 66: Synchronize access to shared mutable data
-
synchronized
不僅是為了保證每個線程訪問/執(zhí)行時,看到的都是“正常狀態(tài)”的對象(所謂正常就是沒有發(fā)生多線程同時未加同步的寫同一個對象抒倚,導(dǎo)致其狀態(tài)不一致)褐着;還能保證每個線程看到的都是最新的對象; - Java語言保證了基本類型中除了long和double的訪問都是原子性的托呕,并發(fā)寫這些類型的數(shù)據(jù)而不進行同步控制含蓉,也不會有問題
- 有人建議訪問具有原子性操作屬性的對象無需進行同步控制,還能提升性能项郊,純屬一派胡言
- Java語言不會保證并發(fā)訪問時馅扣,其他線程寫的值能立即被讀的線程感知,所以同步操作不僅僅是為了互斥訪問着降,也是為了保證多線程之間看到的始終是最新的值
- 上述問題的根本原因就是Java memory model
- 一個簡單差油、常見、易錯的例子
- 如何停止后臺線程鹊碍?首先不能調(diào)用
Thread.stop
方法厌殉,這個方法會導(dǎo)致data corruption - 常用的方法就是用一個
boolean
變量,后臺線程根據(jù)其值決定是否停止侈咕,而主線程想要停止后臺線程時公罕,修改這個變量的值即可 -
boolean
的讀寫操作是原子性的,并發(fā)訪問不加同步耀销,不會導(dǎo)致data corruption楼眷,但是卻無法保證主線程對變量的修改能及時被后臺線程感知铲汪,甚至無法保證能被感知 - 指令重排,如果
done
就是個普通聲明的boolean
罐柳,以下變換在Java memory model下是允許的while (!done) i++; //==> if (!done) while (true) i++;
- 可想而知掌腰,如果未進行同步操作,后臺線程將永遠不會停止
- 解決方法有兩種
- 為
done
的讀寫訪問都加上synchronized
张吉,注意齿梁,讀寫都需要,否則沒有數(shù)據(jù)同步(communication)的效果肮蛹;由于boolean
的讀寫訪問是原子性的勺择,所以這里的synchronized
僅僅起數(shù)據(jù)同步的作用; - 聲明
done
的時候加上volatile
關(guān)鍵字伦忠,volatile
沒有互斥的作用省核,僅僅是起數(shù)據(jù)同步的作用,在這里正好滿足需求昆码;這種方式性能比上一種要好一些气忠; - 使用
volatile
需要格外謹(jǐn)慎,因為它并沒有互斥作用赋咽,如果聲明一個volatile int
旧噪,然后對其進行++
操作,那將會導(dǎo)致data corruption脓匿,因為++
不是原子性的 - 對于這種需求舌菜,可以聲明為
synchronized int
;更好的方式是使用java.util.concurrent.atomic
包下的類亦镶,安全,高效袱瓮; - 更根本的解決方式就是不要多線程共享mutable對象缤骨,而是共享immutable對象;甚至不要多線程共享數(shù)據(jù)尺借;
- 引入框架/庫時绊起,需要考慮一下它們是否會引入多線程問題
- effectively immutable:對象不是真的immutable,但是對象分享出去之后燎斩,就不會再改變了虱歪;當(dāng)然這個還是很危險的,因為并沒有強制的機制保證不會被修改栅表;
- 小結(jié):多線程訪問共享變量時笋鄙,讀和寫都需要進行同步操作
- Item 67: Avoid excessive synchronization
- 在同步代碼塊中,不要調(diào)用可能被重寫的方法怪瓶,更不要調(diào)用使用者傳入對象的方法萧落,因為這些代碼是不可控的,可能導(dǎo)致異常、死鎖找岖、data corruption
- 對于Observer模式中的observer list陨倡,Java 1.5之后有一個單獨優(yōu)化的高效并發(fā)容器:
CopyOnWriteArrayList
,每次寫(添加许布、刪除)操作都會從內(nèi)部的數(shù)組創(chuàng)建一份新的拷貝兴革,讀(遍歷)操作時完全不用加鎖,對于讀多寫少的場景性能很好 - 一個總的原則是蜜唾,在同步代碼塊中杂曲,執(zhí)行盡可能少的操作;如果有耗時操作灵妨,應(yīng)該在保證安全的前提下解阅,嘗試各種手段,將其移出同步塊泌霍;
- 過度同步的性能影響
- 喪失了多核CPU的并行性货抄,獲得鎖的開銷倒是其次
- 任何時刻都需要保證每個CPU核心之間的數(shù)據(jù)同步,這有不小的開銷
- 限制了JVM的代碼優(yōu)化空間
- 共享數(shù)據(jù)的并發(fā)訪問朱转,一定要保證線程安全蟹地;如果可以在類內(nèi)部,通過少量/高效的同步塊保證藤为,就不要把整個類的任何操作都加鎖怪与;如果做不到,那就不要進行任何同步缅疟,把這個責(zé)任交給使用者分别,給他們優(yōu)化的空間,但一定要在文檔中說明存淫;
- 如果
static
成員可以被某些方法修改耘斩,那一定要為它們加鎖,因為這種情況下使用者無法保證線程安全性 - Item 68: Prefer executors and tasks to threads
- Executor Framework
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(runnable);
executor.shutdown();
-
Executors
提供了多個工廠方法桅咆,創(chuàng)建ExecutorService
括授,還可以直接使用ThreadPoolExecutor
,對線程池做更精細的控制 - 如果程序負(fù)載輕岩饼,可以使用
Executors.newCachedThreadPool
荚虚,任務(wù)提交時如果沒有空閑線程,將創(chuàng)建新的線程籍茧;如果負(fù)載重版述,用Executors.newFixedThreadPool
更合適; - 不僅不應(yīng)該自己實現(xiàn)任務(wù)隊列寞冯,甚至都應(yīng)該避免直接使用線程院水,而是使用Executor Framework腊徙;
- 任務(wù)和機制被分別抽象了,前者為
Runnable
和Callable
檬某,后者則是executor service撬腾; -
java.util.Timer
也盡量不要用了,可以使用ScheduledThreadPoolExecutor
恢恼; - Item 69: Prefer concurrency utilities to wait and notify
- 正確使用
wait
和notify
有難度民傻,而Java又提供了更高層的抽象,何樂而不用呢场斑? -
java.util.concurrent
包主要包含三塊: - Executor Framework
- concurrent collections
- synchronizers
- concurrent collections提供了標(biāo)準(zhǔn)容器的多線程高性能版本漓踢,它們內(nèi)部進行了同步互斥操作,保證正確性漏隐;外部使用的時候喧半,無需加鎖,否則只會導(dǎo)致性能下降青责;
- concurrent collections中的每一種實現(xiàn)挺据,可能都有性能優(yōu)化的側(cè)重點,可能有的是多讀少寫高效脖隶,例如
CopyOnWriteArrayList
扁耐,所以使用時需要了解清楚其試用場景; - 除非有明確的理由产阱,否則婉称,優(yōu)先使用
ConcurrentHashMap
,而不是Collections.synchronizedMap
或者Hashtable
构蹬;也盡量避免在使用者那端進行同步操作王暗; - 有的concurrent collections提供了block操作接口,例如
BlockingQueue
庄敛,從中取數(shù)據(jù)的時候瘫筐,如果隊列為空,線程將等待铐姚,新的數(shù)據(jù)加入后,將自動喚醒等待的線程肛捍;大部分的ExecutorService
都是采用這種方式實現(xiàn)的隐绵; - Synchronizers:
CountDownLatch
,Semaphore
,CyclicBarrier
,Exchanger
-
CountDownLatch
: 多個線程等待另外一個或多個線程完成某種工作 - 注意thread starvation deadlock問題
-
Thread.currentThread().interrupt()
idiom:異常可能從其他線程拋出拙毫?用此方法回到原來的線程依许? - 計時的話,用
System.nanoTime()
而不是System.currentTimeMillis()
缀蹄,前者更準(zhǔn)確峭跳,更明確 - 如果非要用
wait
和notify
膘婶,注意以下幾點: - Always use the wait loop idiom to invoke the wait method; never invoke it outside of a loop.
- wait前的條件檢查可以保證不會死鎖,wait后的檢查可以保證安全
- 通常情況下都應(yīng)該使用
notifyAll
- Item 70: Document thread safety
- 一個方法的聲明中加了
synchronized
并不能保證它是線程安全的蛀醉,并且Javadoc也不會把這個關(guān)鍵字輸出到文檔中 - 線程安全也分好幾個層次悬襟,文檔中應(yīng)該說明類/方法做到了何種程度上的線程安全
- 線程安全的分類
- immutable,對象創(chuàng)建后不可修改拯刁,無需進行外部的同步操作(互斥訪問控制或許更恰當(dāng))脊岳;例如:
String
,Long
,BigInteger
; - unconditionally thread-safe垛玻,對象可變割捅,但是其內(nèi)部進行了正確的同步操作,無需外部進行同步帚桩;例如:
ConcurrentHashMap
亿驾; - conditionally thread-safe,和絕對線程安全類似账嚎,但是有些方法需要進行外部的同步操作莫瞬;例如:
Collections.synchronized
返回的容器,它們的iterator使用時需要進行同步醉锄; - not thread-safe乏悄,類自身沒有任何同步操作,需要使用者自己保證線程安全恳不;例如:
ArrayList
檩小; - thread-hostile,由于類的實現(xiàn)原因烟勋,使用者無論如何也無法保證線程安全规求,例如未加同步的修改static成員;例如:
System.runFinalizersOnExit
卵惦; - jsr-305引入了幾個注解:
Immutable
,ThreadSafe
,NotThreadSafe
阻肿,對應(yīng)上述前四種情形,絕對線程安全與條件線程安全都屬ThreadSafe
沮尿,對于條件線程安全還應(yīng)在文檔中說明何種情況下是需要外部進行同步的丛塌; - 如果一個類,將它用于
synchronized
的對象暴露出去了畜疾,那是很危險的赴邻,通常的做法是,內(nèi)部創(chuàng)建一個Object
實例啡捶,將其用于synchronized
姥敛,但這種方式通常只適用于unconditionally thread-safe的實現(xiàn)。
```java
// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();
public void foo() {
synchronized(lock) {
...
}
}
```
- Item 71: Use lazy initialization judiciously
- don’t do it unless you need to
- 如果使用lazy initialization瞎暑,那這個成員的訪問方法要用
synchronized
修飾 - 靜態(tài)成員實現(xiàn)lazy initialization且希望高性能彤敛,使用lazy initialization holder class idiom与帆,例如:
```java
// Lazy initialization holder class idiom for static fields
private static class FieldHolder {
static final FieldType field = computeFieldValue();
}
static FieldType getField() { return FieldHolder.field; }
```
- 實例成員要實現(xiàn)lazy initialization且希望高性能,使用double-check idiom墨榄,但是注意玄糟,double-check并非嚴(yán)格意義的線程安全,例如:
```java
// Double-check idiom for lazy initialization of instance fields
private volatile FieldType field;
FieldType getField() {
FieldType result = field;
if (result == null) { // First check (no locking)
synchronized(this) {
result = field;
if (result == null) // Second check (with locking)
field = result = computeFieldValue();
}
}
return result;
}
```
`result`這個局部變量的作用是渠概,通常情況下茶凳,`field`已經(jīng)初始化過了,這時將只會對其產(chǎn)生一次讀操作播揪,性能會有所提升
- double-check idiom還有兩個變體贮喧,各有其使用場景:single-check idiom,racy single-check idiom猪狈;前者忍受多次賦值箱沦,后者忍受多次賦值且field的操作具有原子性(primitive類型且不是long和double);
- Item 72: Don’t depend on the thread scheduler
- 依賴線程調(diào)度器的正確性雇庙、性能的程序谓形,很可能是無法移植的
- 好的多線程程序绞幌,同時運行的線程數(shù)不應(yīng)該多于CPU內(nèi)核數(shù)
- 線程無法進行有意義的工作時弄企,就不應(yīng)繼續(xù)運行,忙等是不好的實現(xiàn)方式
- 另外一個線程(task)的工作也不能太少彬坏,否則線程切換的開銷都會大于線程執(zhí)行的時間竹椒,此時性能可想而知很低
-
Thread.yield
has no testable semantics. 所以不要用Thread.yield
童太,當(dāng)程序的有些線程因為線程過多而無法獲得CPU時間時,應(yīng)該減少線程數(shù)胸完。 - 線程優(yōu)先級是Java平臺中移植性最差的部分书释,所以也不要用
- Item 73: Avoid thread groups
- 如果設(shè)計的類需要處理一些邏輯上有關(guān)聯(lián)的線程,應(yīng)該考慮 thread pool executors
Serialization
- Item 74: Implement Serializable judiciously
- 實現(xiàn)
Serializable
接口之后赊窥,一旦類發(fā)布出去爆惧,就不能隨意更改實現(xiàn)方式了,否則序列化-反序列化時可能失敗锨能,這降低了靈活性 - 序列化-反序列化的格式也是暴露的API之一扯再,而默認(rèn)的格式是和內(nèi)部具體實現(xiàn)細節(jié)綁定的,所以默認(rèn)格式把內(nèi)部實現(xiàn)細節(jié)也暴露出去了
- 自定義序列化-反序列化格式(
ObjectOutputStream.putFields
,ObjectInputStream.readFields
)址遇,可以緩解上述問題熄阻,但是這又帶來了新的實現(xiàn)復(fù)雜度 -
serialVersionUID
問題 - 會增加bug、安全漏洞的可能性傲隶,因為反序列化得到的對象,其狀態(tài)是無法保證的
- 會增加發(fā)布新版時的測試工作
- 被設(shè)計于用來被繼承的類窃页,謹(jǐn)慎實現(xiàn)
Serializable
接口跺株,同樣复濒,設(shè)計的接口也謹(jǐn)慎繼承Serializable
接口 - 內(nèi)部類不應(yīng)該實現(xiàn)
Serializable
接口 - Item 75: Consider using a custom serialized form
- Do not accept the default serialized form without first considering whether it is appropriate.
- The default serialized form is likely to be appropriate if an object’s physical representation is identical to its logical content.
- Even if you decide that the default serialized form is appropriate, you often must provide a readObject method to ensure invariants and security.
- Regardless of what serialized form you choose, declare an explicit serial version UID in every serializable class you write.
- Item 76: Write
readObject
methods defensively -
readObject
方法的功效和public的構(gòu)造函數(shù)一樣 - 反序列化的時候,
readObject
如果不進行深拷貝乒省、以及數(shù)據(jù)合法性驗證巧颈,就會導(dǎo)致生成的對象數(shù)據(jù)非法,同時袖扛,也有可能獲得反序列化后對象內(nèi)部成員的引用(rogue object reference attacks) - 不要使用
writeUnshared
和readUnshared
方法砸泛,它們并不安全 - 前文應(yīng)該提到過,非final類蛆封,構(gòu)造函數(shù)以及
readObject
方法中唇礁,不能調(diào)用可重載的方法 - Item 77: For instance control, prefer enum types to readResolve
- if you depend on readResolve for instance control, all instance fields with object reference types must be declared transient. 否則可能會無法達到實例控制的目的。
- The accessibility of readResolve is significant.
- final類惨篱,應(yīng)該置為private
- Item 78: Consider serialization proxies instead of serialized instances
- 為需要實現(xiàn)
Serializable
的類添加一個內(nèi)部類盏筐,它的構(gòu)造函數(shù)接收外部類的實例,并將其field拷貝到自身的field砸讳,并且實現(xiàn)readResolve
方法琢融,創(chuàng)建外部類實例,創(chuàng)建方法可以是構(gòu)造函數(shù)簿寂、static factory函數(shù)漾抬,在其中就可以進行實例控制了
```java
// Serialization proxy for Period class
private static class SerializationProxy implements Serializable {
private final Date start;
private final Date end;
SerializationProxy(Period p) {
this.start = p.start;
this.end = p.end;
}
// readResolve method for Period.SerializationProxy
private Object readResolve() {
return new Period(start, end); // Uses public constructor
}
private static final long serialVersionUID = 234098243823485285L; // Any number will do (Item 75)
}
```
- 外部類實現(xiàn)一個
writeReplace
方法
```java
// writeReplace method for the serialization proxy pattern
private Object writeReplace() {
return new SerializationProxy(this);
}
```
- 外部類實現(xiàn)
readObject
方法,并在其中拋出異常
```java
// readObject method for the serialization proxy pattern
private void readObject(ObjectInputStream stream) throws InvalidObjectException {
throw new InvalidObjectException("Proxy required");
}
```
摘錄來源:https://notes.piasy.com/Android-Java/EffectiveJava.html