1. 程式人生 > >別扯那些沒用的系列之:forEach迴圈

別扯那些沒用的系列之:forEach迴圈

寫Java程式碼的程式設計師,集合的遍歷是常有的事,用慣了for迴圈、while迴圈、do while迴圈,我們來點別的,JDK8 使用了新的forEach機制,結合streams,讓你的程式碼看上去更加簡潔、更加高階,便於後續的維護和閱讀。好,不說了,"talk is cheap, show me the code",我們直接上程式碼,秉承一貫以來的風格。skr~skr~

一、對常用集合的遍歷

JDK8中的forEach不僅可以對集合元素進行遍歷,也能根據集合元素的值搞些事情,比如做判斷,比如過濾。我們拿常用的List和Map來舉例。

對Map集合的遍歷:

/**
 * 對Map的遍歷
 */
Map<String, Integer> map = Maps.newHashMap(); map.put("天貓雙11", 1024); map.put("京東雙11", 1024); // ①簡寫式 map.forEach((k, v) -> System.out.println("key:" + k + ", value:" + v)); // ②判斷式 map.forEach((k, v) -> { System.out.println("key:" + k + ", value:" + v); if (StringUtils.contains(k, "京東"
)) { System.out.println("skr~"); } }); 複製程式碼

執行結果:

key:京東雙11, value:1024
key:天貓雙11, value:1024
key:京東雙11, value:1024
skr~
key:天貓雙11, value:1024
複製程式碼

對List集合的遍歷:

/**
 * 對List的遍歷
 */
List<String> list = Lists.newArrayList();
list.add("買買買");
list.add("剁剁剁");
// ①簡寫式
list.forEach(item -> System.out.println(item));
// ②判斷式
list.forEach(item -> { if (StringUtils.contains(item, "買")) { System.out.println("不如再用兩個腎換個iPhone XS Max Pro Plus也無妨啊~"); } }); 複製程式碼

執行結果如下:

買買買
剁剁剁
不如再用兩個腎換個iPhone XS Max Pro Plus也無妨啊~
複製程式碼

二、JDK8 中雙冒號的使用

JDK8中有雙冒號的用法,就是把方法當做引數傳到stream內部,使stream的每個元素都傳入到該方法裡面執行一下。

比如,上面的List<String>的列印,我們可以這樣寫:

// JDK8 雙冒號的用法
list.forEach(System.out::println);
複製程式碼

執行結果也是一樣一樣的:

買買買
剁剁剁
複製程式碼

在 JDK8 中,介面Iterable 8預設實現了forEach方法,呼叫了 JDK8 中增加的介面Consumer內的accept方法,執行傳入的方法引數。 JDK原始碼如下:

/**
     * Performs the given action for each element of the {@code Iterable}
     * until all elements have been processed or the action throws an
     * exception.  Unless otherwise specified by the implementing class,
     * actions are performed in the order of iteration (if an iteration order
     * is specified).  Exceptions thrown by the action are relayed to the
     * caller.
     *
     * @implSpec
     * <p>The default implementation behaves as if:
     * <pre>{@code
     *     for (T t : this)
     *         action.accept(t);
     * }</pre>
     *
     * @param action The action to be performed for each element
     * @throws NullPointerException if the specified action is null
     * @since 1.8
     */
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
複製程式碼

三、對自定義型別的組裝

這個用法我覺得是比較實用也比較常用的。我們先定義兩個POJO,一個叫Track,是一個Entity,和我們的資料庫表結構進行對映;另一個叫TrackVo,是一個Vo,在介面層返回給前端展示用。這裡為了簡化程式碼量,我們使用了lombok外掛。好,先將它們定義出來:

Track.java

/**
 * @author huangzx
 * @date 2018/11/13
 */
@AllArgsConstructor
@Data
@Builder
public class Track {
    private Long id;
    private String name;
    private String anchor;
}
複製程式碼

TrackVo.java

/**
 * @author huangzx
 * @date 2018/11/13
 */
@AllArgsConstructor
@Data
@Builder
public class TrackVo {
    private Long trackId;
    private String trackName;
}
複製程式碼

經常遇到的場景就是:我通過一個Dao層將資料fetch出來,是一個List<Track>,但前端需要的是List<TrackVo>,但Track和TrackVo的欄位又不一樣。按照之前的做法,可能是直接用一個for迴圈或while迴圈將List<Track>遍歷把裡面的Entity賦值到TrackVo,你飛快地敲擊鍵盤,伴隨著螢幕的震動,十來行程式碼頓時被創造了出來,長舒一口氣,大功告成!

殊不知,JDK8 自從引入新的forEach,結合streams,可以讓這十來行程式碼濃縮為一行,實在是簡練。來瞧一瞧:

/**
 * 對自定義型別的組裝
 */
List<Track> trackList = Lists.newArrayList();
Track trackFirst = Track.builder().id(1L).name("我的理想").anchor("方清平").build();
Track trackSecond = Track.builder().id(2L).name("臺上臺下").anchor("方清平").build();
trackList.add(trackFirst);
trackList.add(trackSecond);

List<TrackVo> trackVoList = trackList.parallelStream().map(track -> TrackVo.builder().trackId(track.getId()).trackName(track.getName()).build()).collect(Collectors.toList());
System.out.println(JSON.toJSONString(trackVoList));
複製程式碼

執行結果如下:

[{"trackId":1,"trackName":"我的理想"},{"trackId":2,"trackName":"臺上臺下"}]
複製程式碼

似不似和你預期的結果一樣?

四、原理

ok,秉承程式設計師認識一件事物——“知其然必知其所以然”的原則。我們來分析一下forEach的實現原理。

首先,我們要了解一下上面用到的流 (streams) 概念,以及被動迭代器。

Java 8 最主要的新特性就是 Lambda 表示式以及與此相關的特性,如流 (streams)、方法引用 (method references)、功能介面 (functional interfaces)。正是因為這些新特性,我們能夠使用被動迭代器而不是傳統的主動迭代器,特別是 Iterable 介面提供了一個被動迭代器的預設方法叫做 forEach()。預設方法是 Java 8 的又一個新特性,是一個介面方法的預設實現,在這種情況下,forEach() 方法實際上是用類似於 Java 5 這樣的主動迭代器方式來實現的。

實現了 Iterable 介面的集合類 (如:所有列表 List、集合 map) 現在都有一個 forEach() 方法,這個方法接收一個功能介面引數,實際上傳遞給 forEach() 方法的引數是一個 Lambda 表示式。我們來編寫一段使用 streams 的程式碼:

/**
 * @author huangzx
 * @date 2018/11/13
 */
public class StreamCountsTest {
    public static void main(String[] args) {
        List<String> words = Arrays.asList("natural", "flow", "of", "water", "narrower");
        long count = words.stream().filter(w -> w.length() > 5).count();
        System.out.println(count);
    }
}
複製程式碼

上面所示程式碼使用 Java 8 方式編寫程式碼實現流管道計算,統計字元長度超過5的單詞的個數。列表 words 用於建立流,然後使用過濾器對資料集進行過濾,filter() 方法只過濾出單詞的字元長度,該方法的引數是一個 Lambda 表示式。最後,流的 count() 方法作為最終操作,得到應用結果。

我們再對自定義型別的組裝那句程式碼作個解析,如下:

圖解streams

中間操作除了 filter() 之外,還有 distinct()、sorted()、map() 等,一般是對資料集的整理,返回值一般也是資料集。我們可以大致瀏覽一下它有哪些方法,如下:

Streams提供的方法

總的來說,Stream 遵循 "做什麼,而不是怎麼去做" 的原則。在我們的示例中,描述了需要做什麼,比如獲得長單詞並對它們的個數進行統計。我們沒有指定按照什麼順序,或者在哪個執行緒中做。相反,迴圈在一開始就需要指定如何進行計算。

五、為什麼要用它?

網上許多人說:JDK8 的 Lambda 表示式的效能不如傳統書寫方式的效能。那為何還要出現呢?JDK的新api和新語法有時並不是為了效能而去做極致優化的。從理論上來說,面向物件程式設計,效能相對面向過程肯定是降低的,但是可維護性或清晰度有了很大的提升。

所以一個特性用與不用,取決於你關注什麼,當公司給你3個月時間去做功能實現的時候,顯然不會有人花1個月去做效能優化,這時候更清晰合理的程式碼就很重要了,大多數時候效能問題不是來自於演算法和api的平庸表現,而是出自各種系統的bug。

六、總結

總結一下上面講了什麼?首先是對於常見集合我們怎麼用forEach去操作,並且介紹了雙冒號的用法;然後基於一個已存在的集合怎麼利用它產生一個新的集合,以封裝成我們想要的資料;最後介紹了它的實現原理並闡釋為何要用它的的原因。好了,下課。。。(老師~再見~~)

(完)