引言
大家好,我是了不起。刚刚不久Java23如期发布,但目前国内市场Java8还是占据着主导地位。今天我将模拟实际工作中的需求,带领大家学习Java8中的Stream新特性,大家可以收藏起来以防在需要的时候找不到。
实体类声明
@Getter
@Setter
public class ComputerDTO {
/**
* 计算机编号
*/
private String computerNo;
/**
* 品牌
*/
private String brand;
/**
* 价格
*/
private BigDecimal price;
/**
* cpu核数
*/
private Integer coreQuantity;
/**
* 内存GB
*/
private Integer memory;
/**
* 硬盘信息,包含容量和类型,如 "500GB HDD" 或 "256GB SSD"
*/
private String hardDisk;
/**
* 产地
*/
private String place;
}
场景描述
我暂且充当一下产品经理,现在罗列出了下列需求,基本上覆盖了日常使用Stream流的大多场景,各位小伙伴可以先行看一看有没有思路。
经典场景
- 筛选出所有品牌为“abc”的电脑,并按价格降序排序。
- 计算所有电脑的价格总和。
- 找出内存最大的电脑的信息。
- 统计硬盘类型为SSD的电脑数量。
- 将所有电脑的产地转换成一个不重复的集合。
- 创建一个Map,键为品牌,值为该品牌的电脑列表。
- 获取每个品牌的平均价格。
- 获取一个Map,键为计算机编号,值为该计算机信息。
组合应用
- 筛选出价格低于5000元且CPU核数大于等于4的电脑。
- 找出每个品牌中最贵的电脑,并返回一个包含这些电脑的列表。
- 统计每个品牌的电脑数量,并按数量降序排序。
- 找出所有品牌为“abc”且内存大于等于8GB的电脑,并按CPU核数降序排序。
- 统计每个品牌的平均价格,并找出平均价格最高的品牌。
- 创建一个Map,键为品牌,值为该品牌所有电脑的总价。
经典场景实战攻克
下面我来带大家一道一道攻克,并在这个过程中带大家梳理一下Stream流使用过程中的一些注意事项。
我们假设需要处理的数据是一个ComputerDTO的List,如下:
List<ComputerDTO> computers=getComputers();
Stream流模型的操作很丰富,我们今天将使用到一些常用的方法,这些方法可以被分成两种。
终结方法:返回值类型不再是Stream类型的方法,不再支持链式调用。如count、forEach、collect方法等。
非终结方法:返回值类型仍然是Stream类型的方法,支持链式调用。如map、filter、sorted方法等。
场景1
筛选出所有品牌为“abc”的电脑,并按价格降序排序。
List<ComputerDTO> abcComputers = computers.stream()
.filter(computer -> "abc".equals(computer.getBrand()))
.sorted(Comparator.comparing(ComputerDTO::getPrice).reversed())
.collect(Collectors.toList());
首先我们将这个场景拆解成两个过程,第一个过程是将列表中的所有品牌不为“abc”的电脑过滤掉,这里我们需要使用到filter方法。
filter方法的入参是含一个参数返回结果为boolean类型的函数式接口,这里我们直接使用lambda表达式实现。
需要注意的是filter方法将会保留符合表达式的数据,这里可以和集合的removeIf方法进行对比记忆,并且我们使用stream处理数据并不会改变原集合computers。
第二个过程是将过滤后的结果按照价格降序排序,这里我们使用sorted方法实现。
sorted方法的入参是一个比较器Comparator,这里我们直接使用Comparator.comparing方法构建一个根据价格排序的比较器,并使用reversed方法返回一个降序的比较器。
最后我们使用终结方法collect(Collectors.toList())将结果收集到集合当中。
场景2
计算所有电脑的价格总和。
BigDecimal totalCost = computers.stream()
.map(ComputerDTO::getPrice)
.reduce(BigDecimal.ZERO, BigDecimal::add);
这个场景我们需要先将集合中的ComputerDTO对象转换为价格,因为我们需要的最终结果是一个BigDecimal类型,所以需要先使用map方法对数据进行转换。
map方法的入参是一个Function函数式接口,下面贴出一张图帮助大家理解map方法的作用。
图片
map方法在工作中常常被使用,例如需要根据一个实体类集合获取一个属性值集合,通常先使用map方法获取属性值,看情况需要可以使用distinct方法去重、filter过滤、sorted方法排序,最后使用collect方法收集起来。
在当前场景中我们需要计算所有电脑的价格总和,所以可以使用reduce终结方法进行汇总。
图片
场景3
找出内存最大的电脑的信息。
Optional<ComputerDTO> maxMemoryComputer = computers.stream()
.max(Comparator.comparingInt(ComputerDTO::getMemory));
这个场景简单粗暴,直接将待处理数据转成流,然后使用max方法就可以解决,不过需要注意的是max方法返回的数据使用Optional包了一层。
Optional类同样是Java8提供的,使用isPresent方法可以判断包含值是否为null,通过get方法可以获取包含值,如果包含值为null会抛出一个NoSuchElementException异常,所以通常搭配isPresent方法使用。
场景4
统计硬盘类型为SSD的电脑数量。
long ssdCount = computers.stream()
.filter(computer -> computer.getHardDisk().contains("SSD"))
.count();
这个场景使用了一个新的终结方法count,count方法用于统计流中元素个数,返回值类型为long类型。
场景5
将所有电脑的产地转换成一个不重复的集合。
Set<String> places = computers.stream()
.map(ComputerDTO::getPlace)
.collect(Collectors.toSet());
这个场景在工作中常常会用到,也是上面提到的map的经典用法,只不过这里将流中数据通过collect(Collectors.toSet())收集到了Set中,利用了Set的特性进行去重,而没有使用distinct方法进行去重。
这里引申一下,上点难度,如果这里最终需要获取的是根据产地去重后的ComputerDTO集合呢,使用流的方式又该怎样实现。
这是工作中另外的一个经典场景,List集合按照对象属性去重,其实最终也是利用了Set的特性,在Set的构造函数中传入了自定义比较器!
List<ComputerDTO> newList = computers.stream().collect(Collectors
.collectingAndThen(Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(ComputerDTO::getPlace)))
, ArrayList::new));
这里使用的Collectors.collectingAndThen方法只是将返回结果Set转化为了List,核心处理就是Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(ComputerDTO::getPlace)))。
场景6
创建一个Map,键为品牌,值为该品牌的电脑列表。
Map<String, List<ComputerDTO>> computersByBrand = computers.stream()
.collect(Collectors.groupingBy(ComputerDTO::getBrand));
这个场景也是工作中常常会遇到的场景,对原有数据根据某一个纬度进行分组,然后不同组的数据使用不同的逻辑进行处理。Stream为这个需求也提供了专门的方法Collectors.groupingBy。
场景7
获取每个品牌的平均价格。
Map<String, Double> averagePrices = computers.stream()
.collect(Collectors.groupingBy(ComputerDTO::getBrand, Collectors.averagingDouble(c -> c.getPrice().doubleValue())));
这个场景是场景6的进阶玩法,根据某一个纬度进行分组,分组后再对数据进行处理。
这里使用的是Collectors.groupingBy两个参数的重载方法。
场景8
获取一个Map,键为计算机编号,值为该计算机信息。
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);
这个场景在工作中出现的频率很高,通常有两种方法去实现,其中Collectors.toMap方法有一个小坑,大家在使用时需要注意一下。
java8的Collectors.toMap的value不能为null。
如果待处理的数据中value值存在null,则会出现莫名其妙的空指针异常,所以我在工作中往往会使用第二种方式。
组合应用代码参考
通过上面经典场景的讲解,其实我们可以注意到,基本上绝大多数的应用都离不开collect方法,这个方法在流的使用中极为重要,在后续的文章中我也会为大家进一步的讲解collect方法,敬请期待!
组合场景就是对经典场景中的一些常用API进行组合应用,所以就不在这里一一赘述,仅为大家提供了参考代码。
- 筛选出价格低于5000元且CPU核数大于等于4的电脑。
List<ComputerDTO> affordableAndPowerful = computers.stream()
.filter(computer -> computer.getPrice().compareTo(new BigDecimal("5000")) < 0 && computer.getCoreQuantity() >= 4)
.collect(Collectors.toList());
- 找出每个品牌中最贵的电脑,并返回一个包含这些电脑的列表。
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());
- 统计每个品牌的电脑数量,并按数量降序排序。
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”且内存大于等于8GB的电脑,并按CPU核数降序排序。
List<ComputerDTO> abcHighMemoryComputers = computers.stream()
.filter(computer -> "abc".equals(computer.getBrand()) && computer.getMemory() >= 8)
.sorted(Comparator.comparingInt(ComputerDTO::getCoreQuantity).reversed())
.collect(Collectors.toList());
- 统计每个品牌的平均价格,并找出平均价格最高的品牌。
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);
- 创建一个Map,键为品牌,值为该品牌所有电脑的总价。
Map<String, BigDecimal> totalPricesByBrand = computers.stream()
.collect(Collectors.groupingBy(
ComputerDTO::getBrand,
Collectors.reducing(BigDecimal.ZERO, ComputerDTO::getPrice, BigDecimal::add)
));
结语
学会使用java8的Stream新特性,可以极大的减少工作中的代码量,可以使自己的代码看起来更整洁,同时很多框架源码中也大量使用Stream,掌握了它也可以为我们阅读源码提供帮助,希望这篇文章可以给大家带来帮助。