1. 程式人生 > >Java中List遍歷的幾個問題

Java中List遍歷的幾個問題

之前在專案中引入Lambda表示式後,最近就把之前的程式碼改為Lambda表示式,中間遇到了一個小插曲就是List的在呼叫Stream的forEach()中使用return 、break、continue關鍵字的問題;加上最近一直關注的“碼農每一題”於是自己回顧一下List的基礎溫故而知新了;

一、List幾種遍歷方式的問題

List遍歷集中方式:
1.loop without size;
2.foreach
3.Iterator
4.Stream.forEach()
5.parallelStream().forEach();

問題1:foreach增強for迴圈中修改List中element的值操作無效;

示例程式碼:

 public static void main(String[] args) {
        int size = 1000;
        String s[] = new String[]{"qwqwe", "frsgdf", "asd", "dfsfuytrd", "qwds"};
        List<String> asList = Arrays.asList(s);
        for (String t : asList) {
            if (t.length() <= 4) {
                System.out.println(t);
t = "1122"; } } for (String tt : asList){ System.out.println("==== :"+tt); } } //程式執行結果 asd qwds ==== :qwqwe ==== :frsgdf ==== :asd ==== :dfsfuytrd ==== :qwds
  • 問題緣由:

    foreach遍歷JDK5.0增加的增強for迴圈,foreach在遍歷過程中是通過一個臨時變數,記錄遍歷到的當前List中的element,所以在foreach中操作的物件是指向臨時變數的,而不是List中的element例項物件的地址,結果自然就只是修改臨時變數的值並沒修改List中的element,所以才會出現:foreach增強for迴圈中修改List中element的值是無效的問題;

  • 解決辦法:

    改用loop without size實行;

問題2:Iterator迭代時,呼叫List集合物件remove(Object o)時,丟擲Exception;

示例程式碼:

 public static void main(String[] args) {
        List<String> asList = new ArrayList<>();
        asList.add("qwqwe");
        asList.add("frsgdf");
        asList.add("asd");
        asList.add("dfsfuytrd");
        asList.add("qwds");
        Iterator<String> iterator = asList.iterator();
        String next;
        while (iterator.hasNext()) {
             next = iterator.next();
            if(next.length()<=4){
                asList.remove(next);
            }
        }
}
//執行結果
Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at Main.main(Main.java:31)
  • 問題緣由:

    這個問題是和Iterator的實現方式有關係的,以ArrayList為例,在ArrayList中.iterator()其實是通過工廠模式在內部new出來一個Iterator物件,而且這個Iterator物件的size是按照它被建立時List的.size()大小建立的,如果在iterator()中呼叫List的remove方法,這就會導致Iterator的size大於List的size,進而發生IndexOutOfBoundsException越界異常(List中改為丟擲ConcurrentModificationException,可參考ArrayList.Itr.next()函式);

  • 解決辦法:

    1.如果list中需要刪除一個element的操作的話可以的話,刪除完成直接break;這樣也可以節約時間和減小效能開銷;
    2.呼叫Iterator的remove()方法進行刪除【在原始碼中可以看到在Iterator的remove()中同時也呼叫了List的remove(),這保持了List的size和Iterator的size一致,避免出現越界異常;】

問題3:JDK8中Stream.forEach()遍歷時return、break、continue關鍵字使用【parallelStream也存在這樣問題】;

在JDK8中引入的Stream中利用forEach()遍歷List中,發現breakcontinue兩個關鍵字IDE會直接提示語法錯誤的,所以這連個關鍵字就直接可以pass了,直接看return吧;
示例程式碼:

 public static void main(String[] args) {
  List<String> list = Arrays.asList("qwqwe", "frsgdf", "asd", "dfsfuytrd", "qwds");
        list.stream().forEach(e -> {
            if (e.length() <= 4) {
                System.out.println("----"+e);
                return;
            }
            System.out.println(e);
        });
 }
 //程式執行結果:
 qwqwe
frsgdf
----asd
dfsfuytrd
----qwds
  • 問題緣由:

    在stream[parallelStream中也是一樣的]中關鍵字return、break、continue關鍵字使用問題是和Java8中流Stream的設計有關係的,在Java8中引入的流的目的是提高併發執行效率即:Stream 是對集合(Collection)物件功能的增強,它專注於對集合物件進行各種非常便利、高效的聚合操作(aggregate operation),或者大批量資料操作 (bulk data operation);可以理解為Stream操作的物件是Collection)集合物件自身而不是集合Collection中的element,而且Stream中的各個方法都是一旦開始執行就沒有回頭路,只能等待全部執行完成。
    1.break和continue關鍵字在Sream中失效問題,個人理解為:Stream每次執行的是對整個集合為最小操作單元,而break和continue是以集合Collection中的element為操作單元的,所以這兩關鍵字在設計上就不是一個量級的,所以它們在Stream面前就失效了;
    2 .return 在遍歷結果來看其實充當了continue的角色,同樣return在整個Java中的方法中充當了“急剎車和掉頭返回”的功能,根據上面的理解:在上述程式碼中return的物件只是Steam操作中的一個小支流element,所以return也只能對其中目標element進行剎車,並不能阻止其他element的繼續,結果就是return在Stream中充當continue成了一個既成事實;

  • 解決辦法:

    個人的觀點是跟著黨走有酒有肉,搞清楚緣由了,根據實際需求靈活選擇;

  • 小插曲

    剛開始看結果給人的感覺就是return充當了continue的角色,而且還是按照List的順序執行的,菜雞還是百度了一下結果都說Java8中的stream是併發的資料量大的話就可能是出現亂序,於是趕緊自己測試了1000個String結果任然是按順序列印的,又在CSDN中看到有人說String太簡單,於是new了一個JavaBean結果還是按照順序列印,於是越發感覺網上"那些人是胡說",再者在原始碼看到stream和parallelStream二者差別時,更加確認stream是sequential有序的,而parallelStream是parallel無序的;

二、List幾種遍歷方式的效率問題

Java一直被人詬病的就是效率問題了,所以最後咋能不簡單的對比一下呢;

  • 基礎測試前準備問題

    1.經驗告訴我們是效能越差勁的設定越能放大程式碼效率差異,所以選擇Android手機測試,況且自己就搞Android開發的
    2.測試選擇ArrayList和LinkedList兩種最長用到的List,同時也是兩種不同資料結構的List進行驗證;
    3.測試Size分別選擇size為50,500,1000,5000,10000,50000作為驗證變數;
    4.測試List遍歷的物件為JavaBean【有String.int long三種基本型別,且每次遍歷都是相同列印操作】;
    5.測試過程中所有的遍歷方式中操作完全相同;
    6.測試過程中每次測試前殺死手機其他app,完成一次測試後殺死測試app等一小會盡可能消除記憶體影響;

  • -
測試結果為:

這裡寫圖片描述

基本結論:

1.隨著Size的逐漸變大parallelStream遍歷的效率就越明顯,在Size達到5000+以後parallelStream遍歷時間基本上是其他遍歷方式的時間的一半 ;
2.根據測試結果,在JDK8之前幾種遍歷的方式中通過Size迴圈遍歷效率最差,Iterator和foreach效率基本差不多,但是foreach程式碼更簡潔;
3.在parallelStream遍歷中LinkedList的遍歷效率明顯優於ArrayList;這是和LinkedList的資料結構以及parallelStream的遍歷邏輯有關係的
4.JDK8中引入是stream在List的size在5000以下時遍歷的時間由於其他遍歷方式【parallelStream以外】這個結果不知道正確不;

測試的幾個問題:

1.在測試過程中發現同樣的Size測試幾次結果幾乎每次都有細微的差異,個人分析認為是和測試時手機狀態有關係,不同時間手機系統內部不同操作導致CPU佔用情況不相同導致的;
2.這個測試資料結果中並沒有很明顯體現出ArrayList和LinkedList相比在查詢的中的優勢:在foreach遍歷方式中二者時間基本上沒有差異;這個有點不太明白是什麼原因導致的,希望有哪位大佬答疑解惑。

【下文中已經指出問題根源和改進建議】

三、重要的補充

對這是一個重要的補充,是針對的上文中對List測試的一個重要補充。
在最近準備看面試題看到關於try catch效能影響時,看到的一篇博文try catch 對效能影響不正確測試後,於是趕緊寫程式碼測試[重現之前測試會自相矛盾的結果]驗證之前測試方式的錯誤;於是解開了筆者在上文中測試結果困惑;由於本人是半路的碼農,以目前個人的認知水平經過測試驗證後,個人十分認同try catch 對效能影響提出的問題和解決思路。
下文是在大量引用try catch 對效能影響文中的內容同時夾雜少部分個人理解,如有錯誤紕漏望諸位及時指正;

1.測試中的問題

a、System.currentTimeMillis()和System.nanoTime()測測量程式執行耗時的不準確

首先System.currentTimeMillis()和System.nanoTime()得到的只是當前時間點,前後時間差只是兩次呼叫程式碼的時間差;這中間不僅僅只有函式的執行時間還有執行緒搶佔CPU資源時的等待時間,所以難以保證時間的準確性;

b、Java中JIT優化導致結果出現偏差;

在JVM中的JIT的JIT優化同樣會導致結果出現偏差;

  • JIT:在Java程式語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的位元組碼(包括需要被解釋的指令的程式)轉換成可以直接傳送給處理器的指令的程式。

  • 熱點程式碼(Hot Spot Code):當虛擬機發現某個方法或程式碼塊執行特別頻繁時,就會把這些程式碼認定為“Hot Spot Code”(熱點程式碼),為了提高熱點程式碼的執行效率,在執行時,虛擬機器將會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各層次的優化,完成這項任務的正是JIT編譯器。 在某些情況下,調整好的最佳化 JVM 效能可能超過手工的 C++ 或 C;

  • 執行過程中會被即時編譯器編譯的“Hot Spot Code”有兩類: 多次呼叫的方法、多次呼叫的迴圈體

顯然測試程式碼正是典型的:頻繁的迴圈的迴圈體,JIT也增添了更大誤差;

c、類載入時間和程式執行時間疊加

在首次run的時候類的載入時帶來的時間誤差;

2.正確的測試方式

a、不要使用System.currentTimeMillis()亦或者使用System.nanoTime() ;

這裡說明一下,可能你會看到有些建議使用System.nanoTime()來測試,但是它跟System.currentTimeMillis()區別,僅僅在於時間的基準不同和精度不同,但都表示的是逝去的時間,所以對於測試執行時間上,並沒有什麼區別。因為都無法統計CPU真正執行時間。

b、推薦使用JProfiler效能測試工具

要測試cpu真正執行時間,這裡推薦使用JProfiler效能測試工具,它可以測量出cpu真正的執行時間。具體安裝使用方法可以自行google百度。因為這不是本文最終使用的測試方法,所以就不做詳細介紹了。但是你使用它來測試上面的程式碼,至少可以排除等待CPU消耗的時間
對於後兩者,需要加入Warmup(預熱)階段。
預熱階段就是不斷執行你的測試程式碼,從而使得程式碼完成初始化工作(類載入),並足以觸發JIT編譯機制。一般來說,迴圈幾萬次就可以預熱完畢。

那是不是做到以上兩點就可以了直抵真相了?非常不幸,並沒有那麼簡單,JIT機制和JVM並沒有想象的這麼簡單,要做到以下這些點你才能得到比較真實的結果。建議參考how-do-i-write-a-correct-micro-benchmark-in-java排名第一的答案;
還可以參考Java theory and practice: Anatomy of a flawed microbenchmark
認真看完這些,你就會發現,要保證microbenchmark結果的可靠,真不是一般的難!!!

那就沒有簡單可靠的測試方法了嗎?如果你認真看完上面提到的點,你應該會注意到Rule 8,沒錯,我就是使用Rule8提到的JMH來。
JMH官方主頁:http://openjdk.java.net/projects/code-tools/jmh/

四、附上錯誤的測試程式碼和手機引數

測試Android手機:華為暢享7(SLA-AL00/2GB RAM/全網通);
測試完整程式碼:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.button_a)
    Button buttonA;
    private static int size = 1000;
    @BindView(R.id.size)
    EditText dt_size;
    @BindView(R.id.button_b)
    Button buttonB;
    @BindView(R.id.textView)
    TextView textView;
    private ArrayList<TestBean> as;
    private LinkedList<TestBean> ls;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);

    }

    @SuppressLint("NewApi")
    @OnClick({R.id.button_a, R.id.button_b})
    public void onViewClicked(View view) {
        switch (view.getId()) {
            case R.id.button_a:
                initData();
                break;
            case R.id.button_b:
                test();
                break;
        }
    }

    private void initData() {
        size = Integer.parseInt(dt_size.getText().toString());
        Log.e("TAG", "initData: ======= > SIZE :" + size);
        as = null;
        ls = null;
        as = new ArrayList<>(size);
        ls = new LinkedList<TestBean>();
        for (int i = 0; i < size; i++) {
            this.as.add(new TestBean("AS index :" + i, System.currentTimeMillis(), i));
            this.ls.add(new TestBean("LS index :" + i, System.currentTimeMillis(), i));
        }
        Log.e("TAG", "initData: ======= > init date over");
    }

    @SuppressLint("NewApi")
    private void test() {
        StringBuilder sb = new StringBuilder();
        /*------------------------------loop without size------------------------------*/
        long l1 = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            Log.i("TAG", "AS: ---- > " + as.get(i).toString());
        }

        long l = System.currentTimeMillis() - l1;
        sb.append("AS loop without :"+l).append(" _ ");
        long l2 = System.currentTimeMillis();
        for (int i = 0; i < size; i++) {
            Log.i("TAG", "LS: ---- > " + ls.get(i).toString());
        }
        long ll = (System.currentTimeMillis() - l2);
        sb.append("LS loop without :"+ll).append(" _ ");

        /*-----------------------------------foreach-------------------------*/
        long l3 = System.currentTimeMillis();
        for (TestBean bean : as) {
            Log.i("TAG", "AS: ---- > " + bean.toString());
        }
        long l_3 = (System.currentTimeMillis() - l3);
        sb.append("AS foreach  :"+l_3).append(" _ ");


        long l4 = System.currentTimeMillis();
        for (TestBean bean : ls) {
            Log.i("TAG", "LS: ---- > " + bean.toString());
        }
        long l_4 = (System.currentTimeMillis() - l4);
        sb.append("LS foreach  :"+l_4).append(" _ ");

        /*-------------------------------Iterator-----------------------------*/
        long l5 = System.currentTimeMillis();
        Iterator<TestBean> iterator = as.iterator();
        while (iterator.hasNext()) {
            Log.i("TAG", "AS: ---- > " + iterator.next().toString());
        }

        long l_5 = (System.currentTimeMillis() - l5);
        sb.append("AS Iterator  :"+l_5).append(" _ ");

        long l6 = System.currentTimeMillis();
       Iterator<TestBean> Literators = ls.iterator();
      while (Literators.hasNext()) {
            Log.i("TAG", "LS: ---- > " + Literators.next().toString());
        }
        long l_6 = (System.currentTimeMillis() - l6);
        sb.append("LS Iterator  :"+l_6).append(" _ ");

        /*----------------------------------stream--------------------------*/
        long l7 = System.currentTimeMillis();
        as.stream().forEach(e -> Log.i("TAG", "AS: ---- > " + e.toString()));
        long l_7 = (System.currentTimeMillis() - l7);
        sb.append("AS stream  :"+l_7).append(" _ ");

        long l8 = System.currentTimeMillis();
        ls.stream().forEach(e -> Log.i("TAG", "LS: ---- > " + e.toString()));
        long l_8 = (System.currentTimeMillis() - l8);
        sb.append("LS stream  :"+l_8).append(" _ ");

        /*---------------------------------parallelStream---------------------------*/

        long l9 = System.currentTimeMillis();
        as.parallelStream().forEach(e -> Log.i("TAG", "AS: ---- > " + e.toString()));
        long l_9 = (System.currentTimeMillis() - l9);
        sb.append("AS parallelStream  :"+l_9).append(" _ ");
        long l10 = System.currentTimeMillis();
        ls.parallelStream().forEach(e -> Log.i("TAG", "LS: ---- > " + e.toString()));
        long l_10 = (System.currentTimeMillis() - l10);
        sb.append("LS parallelStream  :"+l_10).append(" _ ");
        textView.setText(sb.toString());
        Log.e("TAG", "-------------------------------TEST OVER----------------------------------");
        Log.e("TAG", "---------------- RESULT : "+sb.toString());
    }

    class TestBean {

        private String str;
        private long time;
        private int index;

        public TestBean(String str, long time, int index) {
            this.str = str;
            this.time = time;
            this.index = index;
        }

        @Override
        public String toString() {
            return "TestBean{" + "str='" + str + '\'' + ", time=" + time + ", index=" + index + '}';
        }
    }
}

測試主頁View
這裡寫圖片描述