我們一起實(shí)戰(zhàn)學(xué)習(xí)Java8 Stream新特性
引言
大家好,我是了不起。剛剛不久Java23如期發(fā)布,但目前國(guó)內(nèi)市場(chǎng)Java8還是占據(jù)著主導(dǎo)地位。今天我將模擬實(shí)際工作中的需求,帶領(lǐng)大家學(xué)習(xí)Java8中的Stream新特性,大家可以收藏起來以防在需要的時(shí)候找不到。
實(shí)體類聲明
@Getter
@Setter
public class ComputerDTO {
/**
* 計(jì)算機(jī)編號(hào)
*/
private String computerNo;
/**
* 品牌
*/
private String brand;
/**
* 價(jià)格
*/
private BigDecimal price;
/**
* cpu核數(shù)
*/
private Integer coreQuantity;
/**
* 內(nèi)存GB
*/
private Integer memory;
/**
* 硬盤信息,包含容量和類型,如 "500GB HDD" 或 "256GB SSD"
*/
private String hardDisk;
/**
* 產(chǎn)地
*/
private String place;
}
場(chǎng)景描述
我暫且充當(dāng)一下產(chǎn)品經(jīng)理,現(xiàn)在羅列出了下列需求,基本上覆蓋了日常使用Stream流的大多場(chǎng)景,各位小伙伴可以先行看一看有沒有思路。
經(jīng)典場(chǎng)景
- 篩選出所有品牌為“abc”的電腦,并按價(jià)格降序排序。
- 計(jì)算所有電腦的價(jià)格總和。
- 找出內(nèi)存最大的電腦的信息。
- 統(tǒng)計(jì)硬盤類型為SSD的電腦數(shù)量。
- 將所有電腦的產(chǎn)地轉(zhuǎn)換成一個(gè)不重復(fù)的集合。
- 創(chuàng)建一個(gè)Map,鍵為品牌,值為該品牌的電腦列表。
- 獲取每個(gè)品牌的平均價(jià)格。
- 獲取一個(gè)Map,鍵為計(jì)算機(jī)編號(hào),值為該計(jì)算機(jī)信息。
組合應(yīng)用
- 篩選出價(jià)格低于5000元且CPU核數(shù)大于等于4的電腦。
- 找出每個(gè)品牌中最貴的電腦,并返回一個(gè)包含這些電腦的列表。
- 統(tǒng)計(jì)每個(gè)品牌的電腦數(shù)量,并按數(shù)量降序排序。
- 找出所有品牌為“abc”且內(nèi)存大于等于8GB的電腦,并按CPU核數(shù)降序排序。
- 統(tǒng)計(jì)每個(gè)品牌的平均價(jià)格,并找出平均價(jià)格最高的品牌。
- 創(chuàng)建一個(gè)Map,鍵為品牌,值為該品牌所有電腦的總價(jià)。
經(jīng)典場(chǎng)景實(shí)戰(zhàn)攻克
下面我來帶大家一道一道攻克,并在這個(gè)過程中帶大家梳理一下Stream流使用過程中的一些注意事項(xiàng)。
我們假設(shè)需要處理的數(shù)據(jù)是一個(gè)ComputerDTO的List,如下:
List<ComputerDTO> computers=getComputers();
Stream流模型的操作很豐富,我們今天將使用到一些常用的方法,這些方法可以被分成兩種。
終結(jié)方法:返回值類型不再是Stream類型的方法,不再支持鏈?zhǔn)秸{(diào)用。如count、forEach、collect方法等。
非終結(jié)方法:返回值類型仍然是Stream類型的方法,支持鏈?zhǔn)秸{(diào)用。如map、filter、sorted方法等。
場(chǎng)景1
篩選出所有品牌為“abc”的電腦,并按價(jià)格降序排序。
List<ComputerDTO> abcComputers = computers.stream()
.filter(computer -> "abc".equals(computer.getBrand()))
.sorted(Comparator.comparing(ComputerDTO::getPrice).reversed())
.collect(Collectors.toList());
首先我們將這個(gè)場(chǎng)景拆解成兩個(gè)過程,第一個(gè)過程是將列表中的所有品牌不為“abc”的電腦過濾掉,這里我們需要使用到filter方法。
filter方法的入?yún)⑹呛粋€(gè)參數(shù)返回結(jié)果為boolean類型的函數(shù)式接口,這里我們直接使用lambda表達(dá)式實(shí)現(xiàn)。
需要注意的是filter方法將會(huì)保留符合表達(dá)式的數(shù)據(jù),這里可以和集合的removeIf方法進(jìn)行對(duì)比記憶,并且我們使用stream處理數(shù)據(jù)并不會(huì)改變?cè)蟘omputers。
第二個(gè)過程是將過濾后的結(jié)果按照價(jià)格降序排序,這里我們使用sorted方法實(shí)現(xiàn)。
sorted方法的入?yún)⑹且粋€(gè)比較器Comparator,這里我們直接使用Comparator.comparing方法構(gòu)建一個(gè)根據(jù)價(jià)格排序的比較器,并使用reversed方法返回一個(gè)降序的比較器。
最后我們使用終結(jié)方法collect(Collectors.toList())將結(jié)果收集到集合當(dāng)中。
場(chǎng)景2
計(jì)算所有電腦的價(jià)格總和。
BigDecimal totalCost = computers.stream()
.map(ComputerDTO::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
這個(gè)場(chǎng)景我們需要先將集合中的ComputerDTO對(duì)象轉(zhuǎn)換為價(jià)格,因?yàn)槲覀冃枰淖罱K結(jié)果是一個(gè)BigDecimal類型,所以需要先使用map方法對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換。
map方法的入?yún)⑹且粋€(gè)Function函數(shù)式接口,下面貼出一張圖幫助大家理解map方法的作用。
圖片
map方法在工作中常常被使用,例如需要根據(jù)一個(gè)實(shí)體類集合獲取一個(gè)屬性值集合,通常先使用map方法獲取屬性值,看情況需要可以使用distinct方法去重、filter過濾、sorted方法排序,最后使用collect方法收集起來。
在當(dāng)前場(chǎng)景中我們需要計(jì)算所有電腦的價(jià)格總和,所以可以使用reduce終結(jié)方法進(jìn)行匯總。
圖片
場(chǎng)景3
找出內(nèi)存最大的電腦的信息。
Optional<ComputerDTO> maxMemoryComputer = computers.stream()
.max(Comparator.comparingInt(ComputerDTO::getMemory));
這個(gè)場(chǎng)景簡(jiǎn)單粗暴,直接將待處理數(shù)據(jù)轉(zhuǎn)成流,然后使用max方法就可以解決,不過需要注意的是max方法返回的數(shù)據(jù)使用Optional包了一層。
Optional類同樣是Java8提供的,使用isPresent方法可以判斷包含值是否為null,通過get方法可以獲取包含值,如果包含值為null會(huì)拋出一個(gè)NoSuchElementException異常,所以通常搭配isPresent方法使用。
場(chǎng)景4
統(tǒng)計(jì)硬盤類型為SSD的電腦數(shù)量。
long ssdCount = computers.stream()
.filter(computer -> computer.getHardDisk().contains("SSD"))
.count();
這個(gè)場(chǎng)景使用了一個(gè)新的終結(jié)方法count,count方法用于統(tǒng)計(jì)流中元素個(gè)數(shù),返回值類型為long類型。
場(chǎng)景5
將所有電腦的產(chǎn)地轉(zhuǎn)換成一個(gè)不重復(fù)的集合。
Set<String> places = computers.stream()
.map(ComputerDTO::getPlace)
.collect(Collectors.toSet());
這個(gè)場(chǎng)景在工作中常常會(huì)用到,也是上面提到的map的經(jīng)典用法,只不過這里將流中數(shù)據(jù)通過collect(Collectors.toSet())收集到了Set中,利用了Set的特性進(jìn)行去重,而沒有使用distinct方法進(jìn)行去重。
這里引申一下,上點(diǎn)難度,如果這里最終需要獲取的是根據(jù)產(chǎn)地去重后的ComputerDTO集合呢,使用流的方式又該怎樣實(shí)現(xiàn)。
這是工作中另外的一個(gè)經(jīng)典場(chǎng)景,List集合按照對(duì)象屬性去重,其實(shí)最終也是利用了Set的特性,在Set的構(gòu)造函數(shù)中傳入了自定義比較器!
List<ComputerDTO> newList = computers.stream().collect(Collectors
.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(ComputerDTO::getPlace)))
, ArrayList::new));
這里使用的Collectors.collectingAndThen方法只是將返回結(jié)果Set轉(zhuǎn)化為了List,核心處理就是Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(ComputerDTO::getPlace)))。
場(chǎng)景6
創(chuàng)建一個(gè)Map,鍵為品牌,值為該品牌的電腦列表。
Map<String, List<ComputerDTO>> computersByBrand = computers.stream()
.collect(Collectors.groupingBy(ComputerDTO::getBrand));
這個(gè)場(chǎng)景也是工作中常常會(huì)遇到的場(chǎng)景,對(duì)原有數(shù)據(jù)根據(jù)某一個(gè)緯度進(jìn)行分組,然后不同組的數(shù)據(jù)使用不同的邏輯進(jìn)行處理。Stream為這個(gè)需求也提供了專門的方法Collectors.groupingBy。
場(chǎng)景7
獲取每個(gè)品牌的平均價(jià)格。
Map<String, Double> averagePrices = computers.stream()
.collect(Collectors.groupingBy(ComputerDTO::getBrand, Collectors.averagingDouble(c -> c.getPrice().doubleValue())));
這個(gè)場(chǎng)景是場(chǎng)景6的進(jìn)階玩法,根據(jù)某一個(gè)緯度進(jìn)行分組,分組后再對(duì)數(shù)據(jù)進(jìn)行處理。
這里使用的是Collectors.groupingBy兩個(gè)參數(shù)的重載方法。
場(chǎng)景8
獲取一個(gè)Map,鍵為計(jì)算機(jī)編號(hào),值為該計(jì)算機(jī)信息。
Map<String, ComputerDTO> computerInfoMap = computers.stream().collect(Collectors.toMap(ComputerDTO::getComputerNo, item -> item));
Map<String, ComputerDTO> computerInfoMap = computers.stream().collect(HashMap::new, (m, v) -> m.put(v.getComputerNo(), v), HashMap::putAll);
這個(gè)場(chǎng)景在工作中出現(xiàn)的頻率很高,通常有兩種方法去實(shí)現(xiàn),其中Collectors.toMap方法有一個(gè)小坑,大家在使用時(shí)需要注意一下。
java8的Collectors.toMap的value不能為null。
如果待處理的數(shù)據(jù)中value值存在null,則會(huì)出現(xiàn)莫名其妙的空指針異常,所以我在工作中往往會(huì)使用第二種方式。
組合應(yīng)用代碼參考
通過上面經(jīng)典場(chǎng)景的講解,其實(shí)我們可以注意到,基本上絕大多數(shù)的應(yīng)用都離不開collect方法,這個(gè)方法在流的使用中極為重要,在后續(xù)的文章中我也會(huì)為大家進(jìn)一步的講解collect方法,敬請(qǐng)期待!
組合場(chǎng)景就是對(duì)經(jīng)典場(chǎng)景中的一些常用API進(jìn)行組合應(yīng)用,所以就不在這里一一贅述,僅為大家提供了參考代碼。
- 篩選出價(jià)格低于5000元且CPU核數(shù)大于等于4的電腦。
List<ComputerDTO> affordableAndPowerful = computers.stream()
.filter(computer -> computer.getPrice().compareTo(new BigDecimal("5000")) < 0 && computer.getCoreQuantity() >= 4)
.collect(Collectors.toList());
- 找出每個(gè)品牌中最貴的電腦,并返回一個(gè)包含這些電腦的列表。
Map<String, ComputerDTO> mostExpensivePerBrand = computers.stream()
.collect(Collectors.groupingBy(ComputerDTO::getBrand,
Collectors.collectingAndThen(
Collectors.maxBy(Comparator.comparing(ComputerDTO::getPrice)),
optional -> optional.orElseThrow(() -> new NoSuchElementException("No computers found for this brand"))
)
));
List<ComputerDTO> mostExpensiveComputers = new ArrayList<>(mostExpensivePerBrand.values());
- 統(tǒng)計(jì)每個(gè)品牌的電腦數(shù)量,并按數(shù)量降序排序。
Map<String, Long> brandCounts = computers.stream()
.collect(Collectors.groupingBy(ComputerDTO::getBrand, Collectors.counting()));
List<Map.Entry<String, Long>> sortedBrandCounts = brandCounts.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.collect(Collectors.toList());
- 找出所有品牌為“abc”且內(nèi)存大于等于8GB的電腦,并按CPU核數(shù)降序排序。
List<ComputerDTO> abcHighMemoryComputers = computers.stream()
.filter(computer -> "abc".equals(computer.getBrand()) && computer.getMemory() >= 8)
.sorted(Comparator.comparingInt(ComputerDTO::getCoreQuantity).reversed())
.collect(Collectors.toList());
- 統(tǒng)計(jì)每個(gè)品牌的平均價(jià)格,并找出平均價(jià)格最高的品牌。
Optional<Map.Entry<String, Double>> highestAveragePrice = computers.stream()
.collect(Collectors.groupingBy(
ComputerDTO::getBrand,
Collectors.averagingDouble(c -> c.getPrice().doubleValue())
))
.entrySet().stream()
.max(Map.Entry.comparingByValue());
String highestBrand = highestAveragePrice.map(Map.Entry::getKey).orElse(null);
double highestAverage = highestAveragePrice.map(Map.Entry::getValue).orElse(0.0);
- 創(chuàng)建一個(gè)Map,鍵為品牌,值為該品牌所有電腦的總價(jià)。
Map<String, BigDecimal> totalPricesByBrand = computers.stream()
.collect(Collectors.groupingBy(
ComputerDTO::getBrand,
Collectors.reducing(BigDecimal.ZERO, ComputerDTO::getPrice, BigDecimal::add)
));
結(jié)語
學(xué)會(huì)使用java8的Stream新特性,可以極大的減少工作中的代碼量,可以使自己的代碼看起來更整潔,同時(shí)很多框架源碼中也大量使用Stream,掌握了它也可以為我們閱讀源碼提供幫助,希望這篇文章可以給大家?guī)韼椭?/p>