別扯那些沒用的系列之:forEach循環

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

1、對經常使用集合的遍歷

JDK8中的forEach不只能夠對集合元素進行遍歷,也能根據集合元素的值搞些事情,好比作判斷,好比過濾。咱們拿經常使用的List和Map來舉例。java

對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也無妨啊~
複製代碼

2、JDK8 中雙冒號的使用

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

好比,上面的List<String>的打印,咱們能夠這樣寫:性能優化

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

執行結果也是同樣同樣的:bash

買買買
剁剁剁
複製代碼

在 JDK8 中,接口Iterable 8默認實現了forEach方法,調用了 JDK8 中增長的接口Consumer內的accept方法,執行傳入的方法參數。 JDK源碼以下:less

/** * 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);
        }
    }
複製代碼

3、對自定義類型的組裝

這個用法我以爲是比較實用也比較經常使用的。咱們先定義兩個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":"臺上臺下"}]
複製代碼

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

4、原理

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 遵循 "作什麼,而不是怎麼去作" 的原則。在咱們的示例中,描述了須要作什麼,好比得到長單詞並對它們的個數進行統計。咱們沒有指定按照什麼順序,或者在哪一個線程中作。相反,循環在一開始就須要指定如何進行計算。

5、爲何要用它?

網上許多人說:JDK8 的 Lambda 表達式的性能不如傳統書寫方式的性能。那爲什麼還要出現呢?JDK的新api和新語法有時並非爲了性能而去作極致優化的。從理論上來講,面向對象編程,性能相對面向過程確定是下降的,可是可維護性或清晰度有了很大的提高。

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

6、總結

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

(完)
相關文章
相關標籤/搜索