关注公众号 JavaStorm 学习更多精彩java
Java 8 带来一大新特性 Lambda 表达式流(Stream),当流与 Lambda 表达式结合使用,代码将变得至关骚气与简洁。数据库
假若有一个需求,须要对数据库查询的发票信息进行处理:编程
发票 Model数组
@Builder
@Data
public class Invoice implements Serializable {
/** * 销方名称 */
private String saleName;
/** * 是否做废 */
private Boolean cancelFlag;
/** * 开票金额 */
private BigDecimal amount;
/** * 发票类型 */
private Integer type;
/** * 明细条数 */
private Integer detailSize;
}
复制代码
咱们使用传统的方式实现,在以前咱们初始化测试数据数据结构
public class StreamTest {
private List<Invoice> invoiceList;
@Before
public void initData() {
Invoice invoice = Invoice.builder().amount(BigDecimal.valueOf(100.02)).cancelFlag(false).detailSize(10)
.saleName("广西制药").type(1).build();
Invoice invoice2 = Invoice.builder().amount(BigDecimal.valueOf(89032478.9)).cancelFlag(false).detailSize(2)
.saleName("深圳电子科技").type(1).build();
Invoice invoice3 = Invoice.builder().amount(BigDecimal.valueOf(2077777889)).cancelFlag(true).detailSize(6)
.saleName("宇宙心空").type(1).build();
Invoice invoice4 = Invoice.builder().amount(BigDecimal.valueOf(356.8)).cancelFlag(false).detailSize(10)
.saleName("孟达餐厅").type(2).build();
Invoice invoice5 = Invoice.builder().amount(BigDecimal.valueOf(998.88)).cancelFlag(false).detailSize(0)
.saleName("网红餐厅").type(2).build();
Invoice invoice6 = Invoice.builder().amount(BigDecimal.valueOf(9009884.09)).cancelFlag(false).detailSize(1)
.saleName("机动车").type(3).build();
invoiceList = Stream.of(invoice, invoice2, invoice3, invoice4, invoice5, invoice6).collect(Collectors.toList());
System.out.println("原始数据:" + invoiceList.toString());
}
复制代码
Java8 以前的实现方式dom
/** * 筛选出金额小于 10000 的发票,根据金额排序,获取排序后的销方名称列表 */
@Test
public void testJava7() {
ArrayList<Invoice> lowInvoiceList = new ArrayList<>();
//筛选出 金额小于 10000 的发票
for (Invoice invoice: invoiceList) {
if (invoice.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0) {
lowInvoiceList.add(invoice);
}
}
// 对筛选出的发票排序
lowInvoiceList.sort(new Comparator<Invoice>() {
@Override
public int compare(Invoice o1, Invoice o2) {
return o1.getAmount().compareTo(o2.getAmount());
}
});
// 获取排序后的销方名字
ArrayList<String> nameList = new ArrayList<>();
for (Invoice invoice : lowInvoiceList) {
nameList.add(invoice.getSaleName());
}
}
复制代码
Java8 以后的骚气操做,一鼓作气。不再用加班写又臭又长的代码了ide
@Test
public void testJava8() {
List<String> nameList = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)// 过滤数据
.sorted(Comparator.comparing(Invoice::getAmount))// 对金额升序排序
.map(Invoice::getSaleName)//提取名称
.collect(Collectors.toList());//转换成list
}
复制代码
一套龙服务的感受,一鼓作气送你上青天。大大减小了代码量。 函数
如今又来一个需求性能
对查询出来的发票数据进行分类,返回一个 Map<Integer, List> 的数据。学习
回顾下 Java7 的写法,有没有一种我擦,这也太麻烦了。还能不能早点下班回去抱女友。
@Test
public void testGroupByTypeJava7() {
HashMap<Integer, List<Invoice>> groupMap = new HashMap<>();
for (Invoice invoice : invoiceList) {
//存在则追加
if (groupMap.containsKey(invoice.getType())) {
groupMap.get(invoice.getType()).add(invoice);
} else {
// 不存在则初始化添加
ArrayList<Invoice> invoices = new ArrayList<>();
invoices.add(invoice);
groupMap.put(invoice.getType(), invoices);
}
}
System.out.println(groupMap.toString());
}
复制代码
接着就是咱们利用 stream 的骚操做代码实现上面的需求
groupingBy 分组
@Test
public void testGroupByTypeJava8() {
Map<Integer, List<Invoice>> groupByTypeMap = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType));
}
复制代码
就是这么简单粗暴,一行代码直捣黄龙。
Stream(流)是一个来自数据源的元素队列并支持聚合操做,它不是数据结构并不保存数据,主要目的是在于计算。
元素是特定类型的对象,造成一个队列。 Java中的Stream并不会存储元素,而是按需计算。 数据源流的来源。能够是集合,数组,I/O channel, 产生器 generator 等。 聚合操做相似SQL语句同样的操做,好比filter, map, reduce, find, match, sorted等。 和之前的Collection操做不一样,Stream操做还有两个基础的特征:
主要有五种方式
Collection<String> collection = Arrays.asList("a", "b", "c");
Stream<String> streamOfCollection = collection.stream();
复制代码
int[] intArr = new int[]{1, 2, 3, 4, 5};
IntStream stream = Arrays.stream(intArr);
复制代码
经过Arrays.stream方法生成流,而且该方法生成的流是数值流【即IntStream】而不是Stream<Integer>
。补充一点使用数值流能够避免计算过程当中拆箱装箱,提升性能。
Stream API提供了mapToInt、mapToDouble、mapToLong三种方式将对象流【即Stream】转换成对应的数值流,同时提供了boxed方法将数值流转换为对象流
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
复制代码
经过Stream的of方法生成流,经过Stream的empty方法能够生成一个空流
Stream<String> lines = Files.lines(Paths.get("data.txt"), Charset.defaultCharset());
复制代码
经过Files.line方法获得一个流,而且获得的每一个流是给定文件中的一行
iterator: iterate方法接受两个参数,第一个为初始化值,第二个为进行的函数操做,由于iterator生成的流为无限流,经过limit方法对流进行了截断,只生成5个偶数
Stream<Integer> stream = Stream.iterate(0, n -> n + 2).limit(5);
复制代码
generator: 接受一个参数,方法参数类型为Supplier,由它为流提供值。generate生成的流也是无限流,所以经过limit对流进行了截断
Stream<Double> stream = Stream.generate(Math::random).limit(5);
复制代码
主要分为两种类型
一个流能够后面跟随零个或多个中间操做。其目的主要是打开流,作出某种程度的数据映射/过滤,而后返回一个新的流,交给下一个操做使用。
这类操做都是惰性化的,仅仅调用到这类方法,并无真正开始流的遍历,真正的遍历需等到终端操做时,常见的中间操做有下面即将介绍的filter、map等
一个流有且只能有一个终端操做,当这个操做执行后,流就被关闭了,没法再被操做,所以一个流只能被遍历一次,若想在遍历须要经过源数据在生成流。终端操做的执行,才会真正开始流的遍历。以下面即将介绍的 count、collect 等。
filter筛选
Stream<Invoice> invoiceStream = invoiceList.stream().filter(invoice -> invoice.getDetailSize() < 10);
复制代码
distinct去除重复元素
List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream().distinct();
复制代码
limit返回指定流个数
Stream<Invoice> invoiceStream = invoiceList.stream().limit(3);
复制代码
经过limit方法指定返回流的个数,limit的参数值必须>=0,不然将会抛出异常
skip跳过流中的元素
List<Integer> integerList = Arrays.asList(1, 1, 2, 3, 4, 5);
Stream<Integer> stream = integerList.stream().skip(2);
复制代码
经过skip方法跳过流中的元素,上述例子跳过前两个元素,因此打印结果为2,3,4,5,skip的参数值必须>=0,不然将会抛出异常。
map流映射
所谓流映射就是将接受的元素映射成另一个元素
List<String> stringList = Arrays.asList("Java 8", "Lambdas", "In", "Action");
Stream<Integer> stream = stringList.stream().map(String::length);
复制代码
经过 map 方法能够完成映射,该例子完成中String -> Integer的映射,以前上面的例子经过 map 方法完成了 Invoice -> String 的映射
flatMap流转换
将一个流中的每一个值都转换为另外一个流
List<String> wordList = Arrays.asList("Hello", "World");
List<String> strList = wordList.stream()
.map(w -> w.split(""))// 将元素根据 空格分隔字符的Stream<String[]>
.flatMap(Arrays::stream)// 将Stream<String[]> 转换成 Stream<String>
.distinct() //去重
.collect(Collectors.toList());
System.out.println(strList.toString());
复制代码
map(w -> w.split(" "))的返回值为Stream<String[]>
,咱们想获取Stream<String>
,能够经过flatMap方法完成Stream ->Stream的转换。因此最后打印的结果是 [H, e, l, o, W, r, d]
元素匹配
if (invoiceList.stream().allMatch(Invoice::getCancelFlag)) {
System.out.println("发票全是做废");
}
复制代码
存在做废发票则打印
if (invoiceList.stream().anyMatch(Invoice::getCancelFlag)) {
System.out.println("存在做废发票");
}
复制代码
等同于
for (Invoice invoice : invoiceList) {
if (invoice.getCancelFlag()) {
System.out.println("存在做废发票");
break;
}
}
复制代码
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
if (integerList.stream().noneMatch(i -> i > 3)) {
System.out.println("值都小于3");
}
复制代码
统计流中元素个数
long count = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.count();
复制代码
long count = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.collect(Collectors.counting());
复制代码
最后一种统计元素个数的方法在与collect联合使用的时候特别有用
查找
Optional<Invoice> first = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.findFirst();
复制代码
经过 findFirst 找到金额小于 10000 的第一个元素
Optional<Invoice> any = invoiceList.stream()
.filter(item -> item.getAmount().compareTo(BigDecimal.valueOf(10000)) < 0)
.findAny();
复制代码
经过findAny方法查找到其中一个小于 10000 的元素并打印,由于内部进行优化的缘由,当找到第一个知足大于三的元素时就结束,该方法结果和findFirst方法结果同样。提供findAny方法是为了更好的利用并行流,findFirst方法在并行上限制更多【本篇文章将不介绍并行流】
reduce将流中的元素组合起来
假设咱们对一个集合中的值进行求和
jdk8 以前
int sum = 0;
for (int i : integerList) {
sum += i;
}
复制代码
jdk8以后经过reduce进行处理
int sum = integerList.stream().reduce(0, (a, b) -> (a + b));
//还能够用方法引用写
int sum = integerList.stream().reduce(0, Integer::sum);
复制代码
好比统计发票金额求和
BigDecimal reduce = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal.ZERO, (a, b) -> (a.add(b)));
复制代码
继续使用方法引用简化
BigDecimal reduce = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
复制代码
reduce 接受两个参数,一个初始值这里是0,一个BinaryOperator<T> accumulator
来将两个元素结合起来产生一个新值,
另外reduce方法还有一个没有初始化值的重载方法
获取流中最小最大值
经过min/max获取最小最大值
Optional<BigDecimal> min = invoiceList.stream().map(Invoice::getAmount).min(BigDecimal::compareTo);
Optional<BigDecimal> max = invoiceList.stream().map(Invoice::getAmount).max(BigDecimal::compareTo);
复制代码
也能够写成
OptionalInt min1 = invoiceList.stream().mapToInt(Invoice::getDetailSize).min();
OptionalInt max1 = invoiceList.stream().mapToInt(Invoice::getDetailSize).max();
复制代码
min获取流中最小值,max获取流中最大值,方法参数为Comparator<? super T> comparator
经过minBy/maxBy获取最小最大值
invoiceList.stream().map(Invoice::getAmount).collect(Collectors.minBy(BigDecimal::compareTo)).get();
复制代码
经过reduce获取最小最大值
Optional<BigDecimal> max = invoiceList.stream().map(Invoice::getAmount).reduce(BigDecimal::max);
复制代码
求和
经过summingInt
Integer sum = invoiceList.stream().collect(Collectors.summingInt(Invoice::getDetailSize));
复制代码
若是数据类型为double、long,则经过summingDouble、summingLong方法进行求和
经过reduce
Integer sum = invoiceList.stream().map(Invoice::getDetailSize).reduce(0, Integer::sum);
复制代码
经过sum,最佳写法
//推荐写成
Integer sum = invoiceList.stream().mapToInt(Invoice::getDetailSize).sum();
复制代码
在上面求和、求最大值、最小值的时候,对于相同操做有不一样的方法能够选择执行。能够选择collect、reduce、min/max/sum方法,推荐使用min、max、sum方法。由于它最简洁易读,同时经过mapToInt将对象流转换为数值流,避免了装箱和拆箱操做
经过averagingInt求平均值
Double avg = invoiceList.stream().collect(Collectors.averagingInt(Invoice::getDetailSize));
复制代码
若是数据类型为double、long,则经过averagingDouble、averagingLong方法进行求平均
对于BigDecimal 则须要先求和再除以总条数
List<BigDecimal> sumList = invoiceList.stream().map(Invoice::getAmount).collect(Collectors.toList());
BigDecimal average = average(sumList, RoundingMode.HALF_UP);
// 求平均值
public BigDecimal average(List<BigDecimal> bigDecimals, RoundingMode roundingMode) {
BigDecimal sum = bigDecimals.stream()
.map(Objects::requireNonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
return sum.divide(new BigDecimal(bigDecimals.size()), roundingMode);
}
复制代码
经过summarizingInt同时求总和、平均值、最大值、最小值
IntSummaryStatistics statistics = invoiceList.stream().collect(Collectors.summarizingInt(Invoice::getDetailSize));
double average1 = statistics.getAverage();
int max1 = statistics.getMax();
int min1 = statistics.getMin();
long sum = statistics.getSum();
复制代码
经过foreach进行元素遍历
invoiceList.forEach(item -> {
System.out.println(item.getAmount());
});
复制代码
经过joining拼接流中的元素
String result = invoiceList.stream().map(Invoice::getSaleName).collect(Collectors.joining(", "));
复制代码
经过groupingBy进行分组
Map<Integer, List<Invoice>> groupByTypeMap = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType));
复制代码
在collect方法中传入groupingBy进行分组,其中groupingBy的方法参数为分类函数。还能够经过嵌套使用groupingBy进行多级分类
Map<String, Map<String, List<RzInvoice>>> = invoiceList.stream().collect(Collectors.groupingBy(Invoice::getType, Collectors.groupingBy(invoice -> {
if (invoice.getAmount().compareTo(BigDecimal.valueOf(10000)) <= 0) {
return "low";
} else if (invoice.getAmount().compareTo(BigDecimal.valueOf(80000)) <= 0) {
return "mi";
} else {
return "high";
}
})));
复制代码
首先根据 发票类型分组,再根据开票金额大小分组,返回的数据类型是 Map<String, Map<String, List>>
进阶经过partitioningBy进行分区
特殊的分组,它分类依据是true和false,因此返回的结果最多能够分为两组
Map<Boolean, List<Dish>> = invoiceList.stream().collect(Collectors.partitioningBy(RzInvoice::getCancelFlag));
复制代码
等同于
Map<Boolean, List<Dish>> = invoiceList.stream().collect(Collectors.groupingBy(RzInvoice::getCancelFlag));
复制代码
这个例子可能并不能看出分区和分类的区别,甚至以为分区根本没有必要,换个明显一点的例子:
List<Integer> integerList = Arrays.asList(1, 2, 3, 4, 5);
Map<Boolean, List<Integer>> result = integerList.stream().collect(partitioningBy(i -> i < 3));
复制代码
返回值的键仍然是布尔类型,可是它的分类是根据范围进行分类的,分区比较适合处理根据范围进行分类
来一个本人在工做中遇到的样例
// 过滤T-1至T-12 近12月数据,根据省份分组求和开票金额,使用金额进行倒序,产生LinkedHashMap
LinkedHashMap<String, BigDecimal> areaSortByAmountMaps =
invoiceStatisticsList.stream().filter(FilterSaleInvoiceUtil.filterSaleInvoiceWithRange(1, 12, analysisDate)) //根据时间过滤数据
.collect(Collectors.groupingBy(FkSalesInvoiceStatisticsDO::getBuyerAdministrativeAreaCode
, Collectors.reducing(BigDecimal.ZERO, FkSalesInvoiceStatisticsDO::getInvoiceAmount, BigDecimal::add)))// 根据开票地区分组,并同时将每一个分组数据的开票金额求和
.entrySet().stream().sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed()) // 根据金额大小倒序
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (e1, e2) -> e1, LinkedHashMap::new)); //收集数据生成LinkedHashMap
复制代码
经过使用Stream API能够简化代码,同时提升了代码可读性,赶忙在项目里用起来。讲道理在没学Stream API以前,谁要是给我在应用里写不少Lambda,Stream API,飞起就想给他一脚。
我想,我如今可能爱上他了【嘻嘻】。同时使用的时候注意不要将声明式和命令式编程混合使用。