Java-forEach增強for循環是值傳遞規則詳解

  • 2020 年 2 月 18 日
  • 筆記

1. 引入

 正如Java語法意義,變數的傳遞只有值傳遞,雖然變數分為引用變數和基本類型變數,前者更像C中的地址概念。 在學習Lambda表達式的時候,遇到了試圖在增強for循環中對原鏈表元素重新賦值失敗的問題,網路上也沒有針對此的其他博文,故開此文。

2. 數組的增強for循環

public class Test1{        public static void main(String[] args) {            int[] arr = new int[10];            for (int temp :arr){              temp++;          }            for (int temp :arr){              System.out.println(temp);          }          }  }

 控制台會打出10個0,而不是1,這表明在forEach語句中temp++操作對arr數組本身沒有任何影響,所以間接證明了,增強for循環中只是值傳遞。這也可以從原理層面解釋:增強for循環作為一個語法糖,其執行順序是:對數組第一個元素複製給臨時變數temp,然後讓temp執行循環中的語句;接著對數組第二個元素再次賦值給臨時變數temp,再次讓其執行for循環中的語句…就這般執行至數組最後一個元素。所以說,temp接受了數組元素的值,在++,這對於數組中的數字沒有任何影響。所以說如果要進行原數組的更改,更好的方式是使用普通的for循環。

3. ArrayList的增強for循環

 程式碼需求是將其list中的String類型對象從小寫轉換為大寫;

public class LowercaseToUppercase{        public static void main(String[] args) {            List<String> list = Arrays.asList("hello", "world", "hello world");            list.forEach(i -> {              i = i.toUpperCase();          });            list.forEach(System.out::println);      }  }

 控制還是輸出小寫的String類型對象,「hello」, 「world」, 「hello world」,倘若你查看forEach方法,你可以發現此原理和第一個例子的數組遍歷實現原理是一樣的,i作為一個中間變數,是臨時存放了String類型的引用變數,但是對原list沒有任何影響,如下面被調用的forEach方法的默認實現程式碼(其中t就是被定義為泛型類型T的臨時變數)。 一個易錯點:很多人認為:因為String內部是final修飾的數組,不能被重新賦值,臨時變數i只能指向新的引用對象,所以上述程式碼功能才不能被實現,這是不對的,其真正的原因是對臨時變數賦值是無法達到預期效果。正確的理解是對臨時變數進行賦值,只能使臨時變數指向新的對象,而對原String對象沒有任何作用。即使將上述程式碼中ArrayList的對象類型由String換成StringBuilder類,在這樣的情況下,雖然同一個“`StringBuilder“對象的值是可以被修改的,但是使用對臨時變數賦值的操作還是不能對原數據結構元素值造成影響。

 如果要實現,需要調用StringBuilder類對象的方法,一般是返回this對象,程式碼如下所示:

public class LowercaseToUppercase {        public static void main(String[] args) {            List<StringBuilder> list3= Arrays.asList(new StringBuilder("hello"),          new StringBuilder("world"),new StringBuilder("hello world"));            list3.forEach(i->                  {                      String str= i.toString().toUpperCase();                      i.replace(0,str.length(),str);                  }            );          list3.forEach(System.out::println);        }  }

 控制台輸出了大寫的字元串,說明我們成功將StringBuilder類型由小寫轉化為大寫,不過遍歷中的臨時變數i的賦值語句並不存在,而是調用其方法,返回this對象,才實現了轉換。

 下面這個程式碼塊是Java集合的forEach方法默認實現,一定要讀懂它:

   default void forEach(Consumer<? super T> action) {          Objects.requireNonNull(action);          for (T t : this) {              action.accept(t);          }      }

 可見Java在foreach語言的執行上保證了原數據結構的安全性,如果確定要更改原數據結構,請使用傳統的for循環。並且我們在foreach語句中可以採用複製給新數據結構的方法實現類似的作用:

		List<String> list2 = new ArrayList<>();            list.forEach(item->list2.add(item.toUpperCase()));            list2.forEach(System.out::println);

 倘若返回list2,那麼和傳統的for語句也是類似的效果。