為什麼要將區域性變數的作用域最小化?

沉默王二發表於2019-10-01

嗨,本篇文章來說說 Java 的一個小細節:為什麼要將區域性變數的作用域最小化?

明人不說暗話啊。這篇文章的靈感來源於《Effective Java》,這本書我買了有好長好長一段時間了,書頁都已經泛黃,烙下了時間的痕跡,但我仍然還沒有把這本書讀完。說來慚愧啊。

為什麼呢?總感覺這本書的中文翻譯有點拙劣,讀起來煩悶枯燥。明明感覺作者說得非常有道理,但就是提不起半點興致。

(說完這句話,總覺得有點對不住這本書的譯者,畢竟吐槽容易,分享難啊。)

為什麼要說這些廢話呢,因為怕大家覺得這是不值一提的細節,但往往細節決定成敗啊。大家不妨換一種比較輕鬆的心態來讀一讀。反正我是不怎麼喜歡高談闊論的文章,讀完後往往只能感慨一句:“說得不錯啊”,但也僅此而已。

好了,來步入正題。

String [] strs = {"洛陽","牡丹","甲天下"};
List<String> list = Arrays.asList(strs);

Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String s = (String) iterator.next();
    System.out.println(s);
}

list.add("沉默王二");
Iterator<String> iterator1 = list.iterator();
while (iterator.hasNext()) {
    String s = (String) iterator1.next();
    System.out.println(s);
}
複製程式碼

大家用“肉眼”看完上面這段程式碼後,會覺得有問題嗎?

如果不細心的話,好像真的很難發現“複製-貼上”引發的這個問題:第二個 while 迴圈的條件中使用了之前的變數 iterator,而不是它應該使用的 iterator1(貼上後遺漏了變數的修改)。這個問題將會導致程式碼在執行的時候丟擲 java.lang.UnsupportedOperationException 的錯誤。

說句實在話,在敲程式碼的這十年來,沒少複製貼上,沒少因為貼上後變數沒有修改徹底,而導致出現了各種意料之外的 bug。

假如把變數的作用域最小化的話,還真的能夠減少這種因為“複製-貼上”而導致出現的錯誤。比如說把 while 迴圈改造成 for 迴圈。

for (Iterator<String> iterator = list.iterator();iterator.hasNext();) {
    String s = (String) iterator.next();
    System.out.println(s);
}

list.add("沉默王二");
for (Iterator<String> iterator = list.iterator();iterator.hasNext();) {
    String s = (String) iterator.next();
    System.out.println(s);
}
複製程式碼

第二個 for 迴圈使用了和第一個 for 迴圈一模一樣的程式碼,連 iterator 這個變數也不需要修改了。

從另一方面來看的話,for 迴圈比 while 迴圈更簡短,可讀性更好。for 迴圈還有另外一種最常用的寫法,示例如下。

for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}
複製程式碼

但這種寫法仍有改進的地方,因為從位元組碼的角度來看,每次迴圈都要呼叫一次 size() 方法。

2: iload_1
3: aload_0
4: getfield      #4                  // Field list:Ljava/util/List;
7: invokeinterface #5,  1            // InterfaceMethod java/util/List.size:()I
12: if_icmpge     40
15: getstatic     #6                  // Field java/lang/System.out:Ljava/io/PrintStream;
18: aload_0
19: getfield      #4                  // Field list:Ljava/util/List;
22: iload_1
23: invokeinterface #7,  2            // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
28: checkcast     #8                  // class java/lang/String
31: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
34: iinc          1, 1
37: goto          2
40: return
複製程式碼

size() 方法雖然簡短,但也有消耗啊。都有什麼消耗呢?說幾個專業名詞大家感受一下,比如說:建立棧幀、呼叫方法時保護現場、呼叫方法完畢後恢復現場。

(容許我尷尬一下,在寫這篇文章之前,我一直用的上面這種 for 迴圈格式。看來寫文章還是能夠督促自己進步啊。)

怎麼改進呢,看下面這種寫法(強烈推薦啊)。

for (int i = 0, n = list.size(); i < n; i++) {
    System.out.println(list.get(i));
}
複製程式碼

在 for 迴圈內部宣告兩個變數:i 和 n,n 用來儲存 i 的極限值,這樣就減少了 size() 方法的呼叫次數(僅有一次了)。

再來看一段程式碼。

String pre_name = "沉默";
String last_name = "王二";

System.out.println(pre_name);
System.out.println(last_name);
複製程式碼

上面這段程式碼看起來挺規整的,沒什麼問題,對吧?它沒有遵守約定——將區域性變數的作用域最小化。

pre_name 變數的作用域結束的有點晚;last_name 變數的作用域開始的有點早。假如第一個 System.out.println() 出錯的話,last_name 的宣告就變得毫無意義了。

(這只是一個例子,變數的處理方法可能比 System.out.println() 複雜得多。)

好的寫法應該是下面這樣子。

String pre_name = "沉默";
System.out.println(pre_name);

String last_name = "王二";
System.out.println(last_name);
複製程式碼

有人可能覺得這不是在吹毛求疵嗎?真不是的,變數就應該是在第一次使用它的時候宣告。否則的話,變數的作用域要麼開始的太早,要麼結束的太晚。

好了,這篇文章到此就結束了,非常的簡短,但講清楚了“為什麼要將區域性變數的作用域最小化”。

為什麼要將區域性變數的作用域最小化?

PS:噓,告訴大家一個好訊息啊。關注「沉默王二」公眾號,後臺回覆關鍵字「666」即可獲取《Effective Java 中文第三版》(書本定價 119 元 )。

相關文章