🍊 Java學習:Java從入門到精通總結
🍊 Spring系列推薦:Spring原始碼決議
📆 最近更新:2021年12月16日
🍊 個人簡介:通信工程本碩💪、朝著優質博主努力🌕,我寫的很慢,但敢保證每一篇都是用心寫的,絕對不無聊,歡迎關注我共飲一杯雞湯~
🍊 點贊 👍 收藏 ?留言 📝 都是我最大的動力!
豆瓣評分9.8的圖書《Effective Java》,是當今世界頂尖高手Josh Bloch的著作,在我之前的文章里我也提到過,編程就像練武,既需要外在的武功招式(編程語言、工具、中間件等等),也需要修煉心法(設計模式、原始碼等等)學霸、學神OR開掛,
我也始終有一個觀點:看視頻跟著敲代碼永遠只是入門,從書籍里學到了多少東西才決定了你的上限,

我個人在Java領域也已經學習了近5年,在修煉“內功”的方面也通過各種途徑接觸到了一些編程規約,例如阿里巴巴的泰山版規約,在此基礎下讀這本書的時候仍是讓我受到了很大的沖激,學習到了很多約定背后的細節問題,還有一些讓我欣賞此書的點是,書中對于編程規約的解釋讓我感到十分受用,并愿意將他們應用在我的作業中,也提醒了我要把閱讀JDK原始碼的任務提上日程,
最后想分享一下我個人目前的看法,內功修煉不像學習一個新的工具那么簡單,其主旨在于踏實,深入探索底層原理的程序很緩慢并且是艱辛的,但一旦開悟,修為一定會突破瓶頸,達到更高的境界,這遠遠不是我通過一兩篇博客就能學到的東西,
接下來就針對此書列舉一下我的識訓與思考,
不過還是要吐槽一下的是翻譯版屬實讓人一言難盡,有些地方會有誤導的效果,你比如java語言里extends是繼承的關鍵字,書本中全部翻譯成了擴展 就完全不是原來的意思了,所以建議有問題的地方對照英文原版進行語意上的理解,
沒有時間讀原作的同學可以參考我這篇文章,
文章目錄
- 57 最小化區域變數的作用域
- 58 for-each回圈優先于傳統的for回圈
- 59 了解并使用類別庫
- 60 若需要精確答案就應避免使用float 和double 型別
- 61 基本型別優先于包裝基本型別
- 62 如果其他型別更合適,盡量避免使用字串
- 63 當心字串連接的性能問題
- 64 通過介面參考物件
- 65 介面優于反射
- 66 謹慎使用本地方法
- 67 謹慎地進行優化
- 68 遵守被廣泛認可的命名約定
57 最小化區域變數的作用域
要使區域變數的作用域最小化,最好的方法是在首次使用的地方宣告它,過早地宣告區域變數可能導致其作用域不僅過早開始而且結束太晚,
每個區域變數宣告都應該包含一個初始化運算式,這個規則的一個例外是try-catch陳述句,如果該值必須在try塊之外使用,那么它必須在try塊之前宣告,此時它還不能被「合理地初始化」,
回圈允許宣告回圈變數,將其作用域限制在需要它們的確切區域,如果回圈終止后不需要回圈變數的內容,那么優先選擇for回圈而不是while回圈,
for (Element e : c) {
... // Do Something with e
}
如果需要訪問迭代器,也許是為了呼叫它的remove方法,首選的習慣用法,使用傳統的for回圈代替for-each回圈:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
要了解為什么這些for回圈優于while回圈,考慮以下代碼片段:
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // BUG!
doSomethingElse(i2.next());
}
第二個回圈包含一個復制粘貼錯誤:它初始化一個新的回圈變數i2,但是使用舊的變數i,不幸的是,它仍在作用域范圍內,生成的代碼編譯時沒有錯誤,并且在不拋出例外的情況下運行,但是它的邏輯已經錯了,
如果將類似的復制粘貼錯誤與for回圈(for-each回圈或傳統回圈)結合使用,則生成的代碼就無法編譯,
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e and i
}
...
// Compile-time error - cannot find symbol i
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
Element e2 = i2.next();
... // Do something with e2 and i2
}
for回圈比while回圈還有一個優點:它更短,增強了可讀性,
下面是另一種對區域變數的作用域最小化的回圈做法:
for (int i = 0, n = expensiveComputation(); i < n; i++) {
... // Do something with i;
}
它有兩個回圈變數,i和n,它們都具有完全相同的作用域,第二個變數n用于存盤第一個變數的限定值,從而避免了每次迭代中冗余計算的代價,
最后一種“最小化區域變數作用域”的最終技術是保持方法小而集中,如果把兩個操作(activities)合并到同一個方法中,與其中一個操作相關的區域變數就有可能會出現在執行另一個操作的代碼范圍之內,為了防止這種情況發生,只需將方法分為兩個:每個操作用一個方法完成,
58 for-each回圈優先于傳統的for回圈
下面是一個傳統的for回圈來遍歷一個集合:
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e
}
下面是陣列的傳統for回圈的實體:
for (int i = 0; i < a.length; i++) {
... // Do something with a[i]
}
它們并不完美,迭代器和索引變數都很混亂,好在for-each回圈解決了所有這些問題,它通過隱藏迭代器或索引變數來消除
混亂和出錯的機會:
for (Element e : elements) {
... // Do something with e
}
此外,使用for-each回圈也不會降低性能
當涉及到嵌套迭代時,for-each回圈相對于傳統for回圈的優勢甚至更大,下面是人們在進行嵌套迭代時經
常犯的一個錯誤:
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(i.next(), j.next()));
問題是,對于外部集合(suit),i.next會被呼叫很多次
下面的代碼本意是要列印一對骰子的所有可能的組合:
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = EnumSet.allOf(Face.class);
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
System.out.println(i.next() + " " + j.next());
該程式不會拋出例外,但它只列印6個重復的組合(從“ONE ONE”到“SIX SIX”),而不是預期的36個組合,
要修復例子中的錯誤,必須在外部回圈的作用域內添加一個變數來保存外部元素:
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
Suit suit = i.next();
for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
deck.add(new Card(suit, j.next()));
}
如果使用嵌套for-each回圈,問題就會消失,生成的代碼也盡可能地簡潔:
for (Suit suit : suits)
for (Rank rank : ranks)
deck.add(new Card(suit, rank));
有三種常?的情況是不能分別使用for-each回圈的:
- 解構過濾
如果需要遍歷集合,并洗掉指定選元素,則需要使用顯式迭代器,以便可以呼叫其remove方法,通常可以使用在Java 8中添加的Collection類中的removeIf方法,來避免顯式遍歷,
- 轉換
如果需要遍歷一個串列或陣列并替換其元素的部分或全部值,那么需要迭代器或陣列索引來替換元素的值,
- 并行迭代
如果需要并行地遍歷多個集合,那么需要顯式地控制迭代器或索引變數,以便所有迭代器或索引變數都可以同步進行
for-each回圈還允許遍歷實作Iterable介面的任何物件
public interface Iterable<E> {
// Returns an iterator over the elements in this iterable
Iterator<E> iterator();
}
59 了解并使用類別庫
假設你想要生成0到某個上界之間的隨機整數,許多程式員會撰寫一個類似這樣的小方法:
static Random rnd = new Random();
static int random(int n) {
return Math.abs(rnd.nextInt()) % n;
}
這個方法有三個缺點:
1. 如果n是較小的2的平方數,亂數序列會在短的時間內重復
2. 如果n不是2的冪,平均而言,一些數字將比其他數字更高概率回傳
public static void main(String[] args) {
int n = 2 * (Integer.MAX_VALUE / 3);
int low = 0;
for (int i = 0; i < 1000000; i++)
if (random(n) < n/2)
low++;
System.out.println(low);
}
如果運行它,你將發現它輸出一個接近666666的數字,隨機方法生成的數字中有三分之二落在其范圍的前半部分
3. 在極少數情況下會回傳超出指定范圍的數字,這是災難性的結果
如果nextInt()回傳Integer.MIN_VALUE、Math.abs也會因為越界而回傳Integer.MIN_VALUE,假設n不是2的冪,那么求模運算子 (%) 將回傳一個負數,
幸運的是,已經有現成的成果可以直接使用:Random.nextInt(int)
通過使用標準庫,你可以利用撰寫它的專家的知識和以前使用它的人的經驗,
從Java 7開始,就不應該再使用Random,而是用ThreadLocalRandom,原因有以下幾點:
-
它能產生更高質量的亂數,而且速度非常快
-
不必浪費時間為那些與你的作業無關的問題撰寫專?的解決方案
-
隨著時間的推移,它們的性能會不斷提高
-
隨著時間的推移,它們往往會獲得新功能
-
可以讓自己的代碼融入主流,這樣的代碼更容易被開發人員閱讀、維護和重用
在每個主要版本中,都會向庫中添加許多特性,了解這些新增特性是值得的
每個程式員都應該熟悉java.lang、java.util和java.io的基礎知識及其子包
如果你在Java平臺庫中找不到你需要的東西,你的下一個選擇應該是尋找高質量的第三方庫,比如谷歌的優秀的開源Guava庫
60 若需要精確答案就應避免使用float 和double 型別
float和double型別特別不適合進行貨幣計算,因為不可能將0.1(或10的任意負次冪)精確地表示為float或double,
例如,假設你口袋里有1.03美元,你消費了42美分,你還剩下多少錢?
System.out.println(1.03 - 0.42);
你可能認為,只需在列印之前將結果四舍五入就可以解決這個問題,但不幸的是,這種方法并不總是有效,例如,假設你口袋里有一美元,你看到一個架子上有一排好吃的糖果,它們的價格僅僅是10美分,20美分,30美分,以此類推,直到1美元,你每買一顆糖,從10美分的那顆開始,直到你買不起貨架上的下一顆糖,
public static void main(String[] args) {
double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought +"items bought.");
System.out.println("Change: $" + funds);
}
如果你運行這個程式,你會發現你可以買得起三塊糖,你還有0.399999999999999999美元,這是錯誤的
答案,解決這個問題的正確方法是使用BigDecimal、int或long進行貨幣計算,
這里是前一個程式的一個簡單改版,使用BigDecimal型別代替double,注意,使用BigDecimal的String建構式而不是它的double建構式,這是為了避免在計算中引入不準確的值
public static void main(String[] args) {
final BigDecimal TEN_CENTS = new BigDecimal(".10");
int itemsBought = 0;
BigDecimal funds = new BigDecimal("1.00");
for (BigDecimal price = TEN_CENTS;funds.compareTo(price) >= 0;price = price.add(TEN_CENTS)) {
funds = funds.subtract(price);
itemsBought++;
}
System.out.println(itemsBought +"items bought.");
System.out.println("Money left over: $" + funds);
}
使用BigDecimal有兩個缺點:
-
與原始算術型別相比很不方便
-
速度慢得多
除了使用BigDecimal,另一種方法是使用int或long,在這個例子中,最明顯的方法是用美分而不是美元來計算
public static void main(String[] args) {
int itemsBought = 0;
int funds = 100;
for (int price = 10; funds >= price; price += 10) {
funds -= price;
itemsBought++;
}
System.out.println(itemsBought +"items bought.");
System.out.println("Cash left over: " + funds + " cents");
}
使用BigDecimal的另一個好處是,它可以完全控制舍入,當執行需要舍入的操作時,可以從8種舍入模式中進行選擇,
61 基本型別優先于包裝基本型別
自動裝箱減少了使用包裝型別的繁瑣性,但沒有減少它的風險,
Java 每個基本型別都有一個對應的參考型別,稱為包裝型別,與int、double和boolean對應的包裝類是Integer、Double和Boolean,
基本型別和包裝型別之間有三個主要區別:
- 兩個包裝型別實體可以具有相同的值,但這兩個實體卻是不一樣的
- 包裝型別可以是
null - 基本型別比包裝型別更節省時間和空間
考慮下面的比較器,它的設計目的是表示Integer值上的升序數字排序,
Comparator<Integer> naturalOrder =(i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
這個比較器存在嚴重缺陷,對于
naturalOrder.compare(new Integer(42), new Integer(42))
兩個Integer實體都表示相同的值(42),所以這個運算式的值應該是0,但它是1,這表明第一個Integer值大于第二個,
i==j運算式對兩個物件參考執行比較,如果i和j參考表示相同int值的不同Integer實體,這個比較將回傳false,所以將==運算子應用于包裝型別幾乎都是錯誤的,
可以通過添加兩個區域變數來存盤基本型別int值,并對這些變數執行所有的比較,從而修復比較器中的問題:
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
int i = iBoxed, j = jBoxed; // Auto-unboxing
return i < j ? -1 : (i == j ? 0 : 1);
};
接下來考慮另外一段代碼:
public class Unbelievable {
static Integer i;
public static void main(String[] args) {
if (i == 42)
System.out.println("Unbelievable");
}
}
它在計算運算式 i==42 時拋出NullPointerException,原因是在操作中混合使用基本型別和包裝型別時,包裝型別就會自動拆箱,如果一個空物件參考自動拆箱,那么你將得到一個NullPointerException,
修復這個問題非常簡單,只需將i宣告為int:
最后在考慮第6條里曾經出現過的代碼:
public static void main(String[] args) {
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
sum += i;
}
System.out.println(sum);
}
這個程式比它預期的速度慢得多,因為它意外地宣告了一個區域變數(sum),它是包裝型別Long,變數被反復裝箱和拆箱,導致產生明顯的性能下降,
什么時候應該使用包裝型別呢?
- 作為集合中的元素、鍵和值
不能將基本型別放在集合中,因此必須使用包裝型別
- 在引數化型別和方法中,必須使用包裝型別作為型別引數
例如,不能將變數宣告為
ThreadLocal<int>型別,因此必須使用ThreadLocal<Integer>
- 在進行反射方法呼叫時,必須使用包裝型別
62 如果其他型別更合適,盡量避免使用字串
本條目討論了一些不應該使用字串的場景:
1. 字串不適合替代其他值型別
當一段資料從檔案、網路或鍵盤輸入到程式時,它通常是字串形式的,但是這種傾向只有在資料本質上是文本的情況下才合理,
2. 字串不適合替代列舉型別
如第34條,列舉型別比字串更適合表示列舉型別的常量,
3. 字串不適合替代聚合型別
如果一個物體有多個組件,將其表示為單個字串通常是很不好的,例如:
String compoundKey = className + "#" + i.next();
這種方法有很多缺點,如果用于分隔欄位的字符出現在其中一個欄位中,可能會導致混亂,要訪問各個欄位,必須決議字串,這是緩慢的、冗?的、容易出錯的程序,
更好的方法是撰寫一個類來表示聚合,通常是一個私有靜態成員類,
4. 字串不能很好地替代capabilities
例如,考慮執行緒區域變數機制的設計,這樣的機制提供的變數在每個執行緒中都有自己的值,許多年前,當面臨設計這樣一個機制的任務時,有人提出了相同的設計,其中客戶端提供的字串鍵,用于標識每個執行緒本地變數:
public class ThreadLocal {
private ThreadLocal() { } // Noninstantiable
// Sets the current thread's value for the named variable.
public static void set(String key, Object value);
// Returns the current thread's value for the named variable.
public static Object get(String key);
}
這種方法的問題在于,為了使這種方法有效,客戶端提供的字串鍵必須是惟一的:如果兩個客戶端各自決定為它們的執行緒本地變數使用相同的名稱,它們無意中就會共享一個變數,
這個API可以通過用一個不可偽造的鍵(有時稱為capability)替換字串來修復:
public class ThreadLocal {
private ThreadLocal() { } // Noninstantiable
public static class Key { // (Capability)
Key() { }
}
// Generates a unique, unforgeable key
public static Key getKey() {
return new Key();
}
public static void set(Key key, Object value);
public static Object get(Key key);
}
雖然這解決了API中基于字串的兩個問題,但是你可以做得更好,
public final class ThreadLocal {
public ThreadLocal();
public void set(Object value);
public Object get();
}
通過將ThreadLocal類泛型化,使這個API變成型別安全的:
public final class ThreadLocal<T> {
public ThreadLocal();
public void set(T value);
public T get();
}
63 當心字串連接的性能問題
不要使用字串連接運算子合并多個字串
字串連接運算子(+) 是將幾個字串組合成一個字串的簡便方法,
為了連接n個字符而重復地使用字串連接運算子,需要O(n^2)的時間,因為字串是不可變的,當兩個字串被連接在一起時,它們的內容都要被拷貝,
例如,下面代碼將每個賬單專案重復連接到一行來構造賬單陳述句的字串表示:
public String statement() {
String result = "";
for (int i = 0; i < numItems(); i++)
result += lineForItem(i); // String concatenation
return result;
}
如果項的數量很大,則該方法的執行時間就難以估算,要獲得更好的性能,可以使用StringBuilder代替String
public String statement() {
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
for (int i = 0; i < numItems(); i++)
b.append(lineForItem(i));
return b.toString();
}
第二個方法預先分配了一個足夠大的StringBuilder來保存整個結果,從而消除了自動增?的需要,即使使用默認大小的StringBuilder,它仍然比第一個方法快很多,
64 通過介面參考物件
如果存在合適的介面型別,那么應該使用介面型別宣告引數、回傳值、變數和欄位,惟一真正需要參考物件的類的時候是使用建構式創建它的時候,考慮LinkedHashSet的情況,它是Set介面的一個實作:
Set<Son> sonSet = new LinkedHashSet<>();
而不是這樣:
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
如果你養成了使用介面作為型別的習慣,那么你的程式將更加靈活,如果你決定要切換實作,只需在建構式中更改類名(或使用不同的靜態工廠),例如,第一個宣告可以改為:
Set<Son> sonSet = new HashSet<>();
所有的代碼都會繼續作業,周圍的代碼不知道舊的實作型別,所以它不會在意更改,
有一點值得注意:如果原實作提供了介面的通用約定不需要的一些特殊功能,并且代碼依賴于該功能,那么新實作提供相同的功能就非常重要,例如,如果圍繞第一個宣告的代碼依賴于LinkedHashSet的排序策略,那么在宣告中將HashSet替換為LinkedHashSet將是不正確的,因為HashSet不保證迭代順序,
為什么要更改實作型別呢?
因為第二個實作比原來的實作提供了更好的性能,或者因為它提供了原來的實作所缺乏的理想功能,例如,假設一個欄位包含一個HashMap實體,將其更改為EnumMap將為迭代提供更好的性能和與鍵的自然順序,
將HashMap更改為LinkedHashMap將提供可預測的迭代順序,性能與HashMap相當,而不需要對鍵型別作出任何特殊要求,
如果沒有合適的介面存在,那么用類參考物件也可以,如String和BigInteger,
值類很少在撰寫時考慮到多個實作,它們通常是final的,很少有相應的介面,使用這樣的值類作為引數、變數、欄位或回傳型別非常合適,
沒有合適介面型別的第二種情況是屬于框架的物件,框架的基本型別是類而不是介面,如果一個物件屬于這樣一個基于類的框架,那么最好使用相關的基類來參考它,這通常是抽象的,而不是使用它的實作類,在java.io類中許多諸OutputStream之類的就屬于這種情況,
沒有合適介面型別的最后一種情況是,實作介面但同時提供介面中不存在的額外方法的類,例如,PriorityQueue有一個在Queue介面上不存在的comparator方法,
如果沒有合適的介面,就使用類層次結構中提供必要功能的最小具體類來參考物件~~
65 介面優于反射
核心反射機制java.lang.reflect提供對任意類的編程訪問,給定一個Class物件,你可以獲得Constructor、Method和Field實體,分別代表了該Class實體所表示的類的構造器、方法和欄位,
通過呼叫Constructor、Method和Field實體上的方法,可以構造底層類的實體、呼叫底層類的方法,并訪問底層類中的欄位,
然而,這種能力是有代價的:
1. 失去了編譯時型別檢查的優勢
如果一個程式試圖反射性地呼叫一個不存在的或不可訪問的方法,它將在運行時失敗
2. 執行反射訪問所需的代碼既笨拙又冗?
3. 性能降低
反射方法呼叫比普通方法呼叫慢得多
如果是非常有限的形式使用反射,則可以獲得反射的許多好處,同時花費的代價很少,如果代碼必須用到在編譯時無法獲取到的類,卻在編譯時存在一個適當的介面或超類來參考該類,就可以用反射方式創建實體,并通過它們的介面或超類正常地訪問它們,
例如,這是一個創建Set<String>實體的程式,類由第一個命令列引數指定,程式將剩余的命令列引數插入到集合中并列印出來,注意,列印這些引數的順序取決于第一個引數中指定的類,如果你指定java.util.HashSet,它們顯然是隨機排列的;如果指定了java.util.TreeSet,則是按字?順序列印的
public static void main(String[] args) {
// Translate the class name into a Class object
Class<? extends Set<String>> cl = null;
try {
cl = (Class<? extends Set<String>>) // Unchecked cast!
Class.forName(args[0]);
} catch (ClassNotFoundException e) {
fatalError("Class not found.");
}
// Get the constructor
Constructor<? extends Set<String>> cons = null;
try {
cons = cl.getDeclaredConstructor();
} catch (NoSuchMethodException e) {
fatalError("No parameterless constructor");
}
// Instantiate the set
Set<String> s = null;
try {
s = cons.newInstance();
} catch (IllegalAccessException e) {
fatalError("Constructor not accessible");
} catch (InstantiationException e) {
fatalError("Class not instantiable.");
} catch (InvocationTargetException e) {
fatalError("Constructor threw " + e.getCause());
} catch (ClassCastException e) {
fatalError("Class doesn't implement Set");
}
// Exercise the set
s.addAll(Arrays.asList(args).subList(1, args.length));
System.out.println(s);
}
private static void fatalError(String msg) {
System.err.println(msg);
System.exit(1);
}
這個例子反映了反射的兩個缺點:
1. 該示例可以在運行時生成6個不同的例外,如果不使用反射的方式實體化,所有這些例外都將是編譯時錯誤
2. 根據類的名稱生成類的實體需要25行冗?的代碼,而建構式呼叫只需要一行
通過捕獲
ReflectiveOperationException(Java 7中引入的各種反射例外的超類),可以減少代碼的?度
如果要變寫一個包,它運行時必須依賴其他某個包的多個版本,這種做法可能非常有用,具體做法就是,在支持包所需要最老版本下對它進行編譯,然后以反射的方式訪問任何更加新的類或方法,
66 謹慎使用本地方法
Java本地介面(JNI)允許Java程式呼叫本地方法,它們提供了“訪問特定于平臺的機制”,比如注冊表,本地方法還可以通過本地語言,撰寫應用程式中注重性能的部分,以提高性能,
使用本地方法訪問特定于平臺的機制是合法的,但是很少有必要:隨著Java平臺的成熟,它提供了對許多以前只能在宿主平臺中上找到的特性,
使用本地方法來提高性能的做法也不值得提倡,原因是JVM變得越來越快了,對于大多數任務,現在可以在Java中獲得類似本地方法的性能,
在Java 1.1中添加了
java.math,里面的BigInteger是在一個用C撰寫的快速多精度運算庫的基礎上實作的,在當時,為了獲得足夠的性能這樣做是必要的,在Java 3中,BigInteger則完全用Java重寫了,并且進行了性能調優,新的版本比原來的版本更快,
使用本地方法有嚴重的缺點:
1. 本地語言是不安全的,所以使用本地方法的應用程式可能會受到記憶體毀壞的影響
2. 本地語言比Java更依賴于平臺,因此使用本地方法的程式的可移植性較差
3. 使用本地方法的代碼更難除錯
4. 本地方法可能會降低性能,
因為垃圾收集器無法自動跟蹤本地記憶體使用情況
5. 進出本地代碼時會產生額外的開銷
6. 本地方法需要「粘合代碼」,很難閱讀,并且撰寫起來很乏味
67 謹慎地進行優化
有三條關于優化的格言是每個人都應該知道的:
比起其他任何單一的原因(包括盲目的愚蠢),很多計算上的過失都被歸昝于效率(不一定能實作),
—William A. Wulf
不要去計較效率上的一些小小的得失,在97%的情況下,不成熟的優化才是一切問題的根源,
—Donald E. Knuth
在優化方面,我們應該遵守兩條規則:
規則1:不要進行優化,
規則2(僅針對專家):還是不要進行優化,也就是說,在你還沒有絕對清晰的未優化方案之前,請不要進行優化,
—M. A. Jackson
它們告訴我們關于優化的一個深刻的事實:很容易弊大于利,尤其是不成熟的優化,
我們應該努力撰寫好的程式,而不是快速的程式,如果一個好的程式不夠快,它的架構將允許它被優化,好的程式體現了資訊隱藏的原則:只要有可能,它們就會把設計決策集中在單個模塊中,因此可以在不影響系統其余部分的情況下更改單個決策,
這并不意味著在程式完成之前可以忽略性能問題,而是必須在設計程序中考慮性能,
盡量避免限制性能的設計決策:設計中最難以更改的組件是那些指定組件之間以及與外部世界的互動的組件,最主要的是API、互動層協議和永久資料格式,
要考慮API設計決策的性能結果:
- 使
public類成為可變的,可能需要大量不必要的保護性拷貝 - 在適合復合模式的
public類里使用繼承,會把該類永遠系結到它的超類,這會人為地限制子類的性能 - 在API中使用實作類而不是介面,會將你束縛在一個具體的實作上,即使將來可能會撰寫更快的實作也無法使用
以java.awt.Component中的getSize方法為例,這個注重性能的方法將回傳Dimension實體,Dimension實體是可變的,這就使得該方法的任何實作在每次呼叫時分配一個新的Dimension實體,
存在幾種API設計的替代方案:
Dimension應該是不可變的- 用兩個方法替換
getSize,他們分別回傳Dimension物件的單個基本組件
為了獲得良好的性能而改變API是一個非常糟糕的想法,因為導致你改變API的性能問題,可能在平臺或其他底層軟體的未來版本中消失,但是改變的API和隨之而來的問題將永遠伴隨著你,
在每次嘗試優化前后都要測驗性能,性能分析工具可以幫助你決定將優化作業的重點放在哪里,這些工具可以提供運行時資訊;另一類工具是jmh,它是一個微基準測驗框架,提供了對Java代碼性能詳情的預測性,
68 遵守被廣泛認可的命名約定
Java平臺有一組完善的命名約定,其中許多約定包含在了《The Java Language Specification》中,這些命名大致分為兩類:字面的和語法的,
1. 包名和模塊名應該是分層的,組件之間用句點分隔
每個部分都包括小寫字?,很少使用數字,包名就是公司或組織的域名顛倒,例如:edu.nju、com.google、org.eff,
包名的其余部分應該由描述包的一個或多個組件組成,通常不超過8個字符,鼓勵使用有意義的縮寫,例如util(utilities),縮寫詞也行,例如awt,
2. 類、介面、列舉、注釋名稱,應該由一個或多個單詞組成,每個單詞的首字?大寫
例如List或FutureTask,對于首字母縮寫,到底應該全部大寫還是只有首字母大寫,沒有統一的說法,但還是強烈推薦只有首字母大寫,比如HttpUrl就比HTTPURL清晰,
3. 方法和欄位名的第一個字?應該小寫
例如remove或ensureCapacity,如果首字母縮寫組成的單詞是一個方法或欄位名的第一個單詞,它應該是小寫的,
4. 常量欄位的名稱應該由一個或多個大寫單詞組成,由下劃線分隔
例如VALUES或NEGATIVE_INFINITY,
5. 區域變數名允許使用縮寫,也允許使用單個字符和短字符序列,它們的含義取決于它們出現的背景關系
例如i、denom、houseNum
6. 型別引數名通常由單個字?組成
最常?的是以下五種型別之一:T表示任意型別,E表示集合的元素型別,K和V表示Map的鍵和值型別,X表示例外,函式的回傳型別通常為R,任意型別的序列可以是T、U、V或T1、T2、T3,
上面所有的討論可以總結如下表:

語法命名約定比排版約定更靈活,也更有爭議,
1. 可實體化的類,包括列舉型別,通常使用一個或多個名詞短語來命名
例如Thread、PriorityQueue或ChessPiece
2. 不可實體化的類通常使用復數名詞來命名
例如collector或Collections
3. 介面的名稱類似于類
例如集合或比較器,或者以able或ible結尾的形容詞,例如Runnable、Iterable或Accessible
4. 注解型別有很多的用途,所以名詞、動詞、介詞和形容詞都很常?
例如BindingAnnotation、Inject、ImplementedBy或Singleton
5. 執行某些操作的方法通常用動詞或動詞短語(包括物件)命名
例如append或drawImage
6. 回傳布林值的方法的名稱通常以單詞is或has(通常很少用)開頭,后面跟一個名詞、一個名詞短語,或者任何用作形容詞的單詞或短語
例如isDigit、isProbablePrime、isEmpty、isEnabled或hasSiblings
7. 回傳被呼叫物件的非布爾函式或屬性的方法通常使用名詞短語、以get開頭的名詞或動詞短語來命名
例如size、hashCode或getTime
8. 轉換物件型別的實體方法通常稱為toType
例如toString或toArray
9. 回傳與接收物件型別不同的視圖的方法通常稱為asType
例如asList
10. 回傳與呼叫它們的物件具有相同值的基本型別的方法通常稱為型別值
例如intValue
11. 靜態工廠的常?名稱
from、of、valueOf、instance、getInstance、newInstance、getType和newType
欄位名的語法約定沒有類、介面和方法名的語法約定建立得好,也不那么重要,因為設計良好的API包含很少的公開欄位,型別為boolean的欄位的名稱通常類似于boolean訪問器方法,省略了初始值「is」,例如initialized、composite,其他型別的欄位通常用名詞或名詞短語來命名,如height、digits和bodyStyle,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/400610.html
標籤:其他
