深度掌握Java Stream 流操作,讓你的代碼高出一個逼格!

一、介紹
我們都知道,從 Java8 開始,jdk 新增加了一個 Stream 類,用來補充集合類,它的強大,相信用過它的朋友,能明顯的感受到,不用使用for循環就能對集合作出很好的操作。

Stream 使用一種類似用 SQL 語句從數據庫查詢數據的直觀方式來提供一種對 Java 集合運算和表達的高階抽象。

這種風格將要處理的元素集合看作一種流, 流在管道中傳輸, 並且可以在管道的節點上進行處理, 比如篩選, 排序,聚合等。

元素流在管道中經過中間操作(intermediate operation)的處理,最後由最終操作(terminal operation)得到前面處理的結果。

操作流程如下:

+--------------------+       +------+   +------+   +---+   +-------+ 
| stream of elements +-----> |filter+-> |sorted+-> |map+-> |collect| 
+--------------------+       +------+   +------+   +---+   +-------+ 

採用 Stream API 可以極大提高 Java 程序員的生產力,讓程序員寫出高效率、乾淨、簡潔的代碼。

下面,我們就以實際的日常開發編程風格做對比,學習完 Stream 的編程風格之後,我敢保證,你會愛上它!

二、遍歷操作
2.1、遍歷集合
日常開發中,我們經常需要需要遍歷集合對象中的元素,例如,我們會採用如下方式進行遍歷元素,然後過濾出某個字段的集合,jdk7 的操作:

/** 
 * jdk7 從集合對象中獲取用戶ID集合 
 * @param userList 
 * @return 
 */ 
public List<Long> getUserIds(List<User> userList){ 
    List<Long> userIds = new ArrayList<>(); 
    for (User user : userList) { 
        userIds.add(user.getUserId()); 
    } 
    return userIds; 
} 

當採用 Stream 編程之後,只需要通過一行代碼,即可實現:

/** 
 * jdk8 從集合對象中獲取用戶ID集合 
 * @param userList 
 * @return 
 */ 
public List<Long> getUserIds(List<User> userList){ 
    List<Long> userIds = userList.stream().map(User::getUserId).collect(Collectors.toList()); 
    return userIds; 
} 

2.2、篩選元素
篩選元素,是日常開發中經常會碰到,例如在 jdk7,我們會這麼操作:

/** 
 * jdk7 從集合對象中過濾出用戶ID不爲空的數據 
 * @param userList 
 * @return 
 */ 
public List<Long> getUserIds7(List<User> userList){ 
    List<Long> userIds = new ArrayList<>(); 
    for (User user : userList) { 
        if(user.getUserId() != null){ 
            userIds.add(user.getUserId()); 
        } 
    } 
    return userIds; 
} 

採用 Stream api,我們只需要通過filter方法來篩選出需要的數據,即可過濾出用戶ID不爲空的數據。

/** 
 * jdk8 從集合對象中篩選出用戶ID不爲空的數據 
 * @param userList 
 * @return 
 */ 
public List<Long> getUserIds8(List<User> userList){ 
    List<Long> userIds = userList.stream().filter(item -> item.getUserId() != null).map(User::getUserId).collect(Collectors.toList()); 
    return userIds; 
} 

2.3、刪除重複的內容
如果你想對返回的集合內容排除重複的數據,操作也很簡單,在合併的時候使用Collectors.toSet()即可!

/** 
 * jdk8 從集合對象中篩選出用戶ID不爲空的數據,並進行去重 
 * @param userList 
 * @return 
 */ 
public Set<Long> getUserIds(List<User> userList){ 
    Set<Long> userIds = userList.stream().filter(item -> item.getUserId() != null).map(User::getUserId).collect(Collectors.toSet()); 
    return userIds; 
} 

2.4、數據類型轉換
在實際的開發過程中,經常會出現數據類型定義不一致的問題,例如有的系統,使用String接受,有的是用Long,對於這種場景,我們需要將其轉換,操作也很簡單

/** 
 * jdk8 將Long類型數據轉換成String類型 
 * @param userIds 
 * @return 
 */ 
public List<String> getUserIds10(List<Long> userIds){ 
    List<String> userIdStrs = userIds.stream().map(x -> x.toString()).collect(Collectors.toList()); 
    return userIdStrs; 
} 

2.5、數組轉集合
我們還會碰到,前端傳給我們的是一個數組,但是我們需要轉成集合,採用 stream api 操作也很簡單!

public static void main(String[] args) { 
        //創建一個字符串數組 
        String[] strArray = new String[]{"a","b","c"}; 
        //轉換後的List 屬於 java.util.ArrayList 能進行正常的增刪查操作 
        List<String> strList = Stream.of(strArray).collect(Collectors.toList()); 
} 

三、集合轉Map操作
在實際的開發過程中,還有一個使用最頻繁的操作就是,將集合元素中某個主鍵字段作爲key,元素作爲value,來實現集合轉map的需求,這種需求在數據組裝方面使用的非常多,尤其是在禁止連表 sql 查詢操作的公司,視圖數據的拼裝只能在代碼層面來實現。

例如下面這段代碼,角色表裏面關聯角色組ID信息,當查詢角色信息的時候,需要把角色組名稱也展示處理,採用map方式來匹配,效率會非常高。

實際代碼案例分享

//角色組ID集合 
Set<Long> roleGroupIds = new HashSet<>(); 
//查詢所有的角色信息 
List<RoleInfo> dbList = roleInfoMapper.findByPage(request); 
for (RoleInfo source : dbList) { 
    roleGroupIds.add(source.getRoleGroupId()); 
    RoleInfoDto result = new RoleInfoDto(); 
    BeanUtils.copyProperties(source, result); 
    resultList.add(result); 
} 
//查詢角色組信息 
if (CollectionUtils.isNotEmpty(roleGroupIds)) { 
    List<RoleGroupInfo> roleGroupInfoList = roleGroupInfoMapper.selectByIds(new ArrayList<>(roleGroupIds)); 
    if (CollectionUtils.isNotEmpty(roleGroupInfoList)) { 
     //將List轉換成Map,其中id主鍵作爲key,對象作爲value 
        Map<Long, RoleGroupInfo> sourceMap = new HashMap<>(); 
        for (RoleGroupInfo roleGroupInfo : roleGroupInfoList) { 
            sourceMap.put(roleGroupInfo.getId(), roleGroupInfo); 
        } 
  //封裝角色組名稱 
        for (RoleInfoDto result : resultList) { 
            if (sourceMap.containsKey(result.getRoleGroupId())) { 
                result.setRoleGroupName(sourceMap.get(result.getRoleGroupId()).getName()); 
            } 
        } 
    } 
} 

3.1、集合轉 map(不分組)
在 jdk7 中,將集合中的元素轉 map,我們通常會採用如下方式。

/** 
 * jdk7 將集合轉換成Map,其中用戶ID作爲主鍵key 
 * @param userList 
 * @return 
 */ 
public Map<Long, User> getMap(List<User> userList){ 
    Map<Long, User> userMap = new HashMap<>(); 
    for (User user : userList) { 
        userMap.put(user.getUserId(), user); 
    } 
    return userMap; 
} 

在 jdk8 中,採用 stream api的方式,我們只需要一行代碼即可實現

/** 
 * jdk8 將集合轉換成Map,其中用戶ID作爲主鍵key,如果集合對象有重複的key,以第一個匹配到的爲主 
 * @param userList 
 * @return 
 */ 
public Map<Long, User> getMap(List<User> userList){ 
    Map<Long, User> userMap = userList.stream().collect(Collectors.toMap(User::getUserId, v -> v, (k1,k2) -> k1)); 
    return userMap; 
} 

打開Collectors.toMap方法源碼,一起來看看到底是啥。

public static <T, K, U> 
Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, 
                                Function<? super T, ? extends U> valueMapper, 
                                BinaryOperator<U> mergeFunction) { 
    return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); 
} 

從參數表可以看出:

第一個參數:表示 key
第二個參數:表示 value
第三個參數:表示某種規則
上文中的Collectors.toMap(User::getUserId, v -> v, (k1,k2) -> k1),表達的意思就是將userId的內容作爲key,v -> v是表示將元素user作爲value,其中(k1,k2) -> k1表示如果存在相同的key,將第一個匹配的元素作爲內容,第二個捨棄!

3.2、集合轉map(分組)
在實際的操作中,有一些場景需要我們將相同的key,加入到一個集合,而不是覆蓋,哪改如何做呢?

如果是採用 jdk7,我們大概會這麼做。

/** 
 * jdk7 將集合轉換成Map,將相同的key,加入到一個集合中,實現分組 
 * @param userList 
 * @return 
 */ 
public Map<Long, List<User>> getMapGroup(List<User> userList){ 
    Map<Long, List<User>> userListMap = new HashMap<>(); 
    for (User user : userList) { 
        if(userListMap.containsKey(user.getUserId())){ 
            userListMap.get(user.getUserId()).add(user); 
        } else { 
            List<User> users = new ArrayList<>(); 
            users.add(user); 
            userListMap.put(user.getUserId(), users); 
        } 
    } 
    return userListMap; 
} 

而在 jdk8 中,採用 stream api的方式,我們只需要一行代碼即可實現

/** 
 * jdk8 將集合轉換成Map,將相同的key,加入到一個集合中,實現分組 
 * @param userList 
 * @return 
 */ 
public Map<Long, List<User>> getMapGroup(List<User> userList){ 
    Map<Long, List<User>> userMap = userList.stream().collect(Collectors.groupingBy(User::getUserId)); 
    return userMap; 
} 

四、分頁操作
stream api 的強大之處還不僅僅是對集合進行各種組合操作,還支持分頁操作。

例如,將如下的數組從小到大進行排序,排序完成之後,從第1行開始,查詢10條數據出來,操作如下:

//需要查詢的數據 
List<Integer> numbers = Arrays.asList(3, 2, 2, 3, 7, 3, 5,10, 6, 20, 30, 40, 50, 60, 100); 
List<Integer> dataList= numbers.stream().sorted((x, y) -> x.compareTo(y)).skip(0).limit(10).collect(Collectors.toList()); 
System.out.println(dataList.toString()); 

其中skip參數表示第幾行,limit表示查詢的數量,類似頁容量!

五、查找與匹配操作
stream api 還支持對集合進行查找,同時還支持正則匹配模式。

allMatch(檢查是否匹配所有元素)

List<Integer> list = Arrays.asList(10, 5, 7, 3); 
boolean allMatch = list.stream()// 
        .allMatch(x -> x > 2);//是否全部元素都大於2 
System.out.println(allMatch); 

findFirst(返回第一個元素)

List<Integer> list = Arrays.asList(10, 5, 7, 3); 
Optional<Integer> first = list.stream()// 
        .findFirst(); 
Integer val = first.get(); 
System.out.println(val);//輸出10 

reduce(可以將流中元素反覆結合起來,得到一個值)

List<Integer> list = Arrays.asList(10, 5, 7, 3); 
Integer result = list.stream()// 
    .reduce(2, Integer::sum); 
System.out.println(result);//輸出27,其實相當於2+10+5+7+3,就是一個累加 

stream api 支持的操作方法非常多,這裏只列舉了幾種類型,具體在使用的時候,可以參考官網接口文檔說明!

六、並行操作
所謂並行,指的是多個任務在同一時間點發生,並由不同的cpu進行處理,不互相搶佔資源;而併發,指的是多個任務在同一時間點內同時發生了,但由同一個cpu進行處理,互相搶佔資源。

這點上大家一定要區分清楚,別弄混了!

stream api 的並行操作和串行操作,只有一個方法區別,其他都一樣,例如下面我們使用parallelStream來輸出空字符串的數量:

List<String> strings = Arrays.asList("abc", "", "bc", "efg", "abcd","", "jkl"); 
// 採用並行計算方法,獲取空字符串的數量 

long count = strings.parallelStream().filter(string -> string.isEmpty()).count();
在實際使用的時候,並行操作不一定比串行操作快!對於簡單操作,數量非常大,同時服務器是多核的話,建議使用Stream並行!反之,採用串行操作更可靠!

七、小結
本文主要,圍繞 jdk stream api 操作,結合實際的日常開發需求,做了簡單總結和分享,鑑於筆者才疏學淺,可能也有理解不到位的地方,歡迎網友們批評指出!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章