五柳先生有个习惯,“好读书,不求甚解”!看来今天也要学习学习五柳先生了,JDK中流式编程魅力实在太大,简而言之,就是太方便了!那个程序员能抵挡这种考验?
至于流式编程的底层原理,哦,这个我是没关心,仅仅是“不求甚解”,底层实现什么的,告辞,在下实在卷不动了!
废话不多说,今天就来见识见识JDK中的流式编程,或者说是lambda编程,据说最初是根据希腊字母λ命名的。虽然Java中不使用这个符号,名称还是被保留了下来。
使用流构建集合,输出规律数组
一般来说,能使用流实现的,直接使用数组集合也能够实现,不过代码量会多许多,今天的话,只是单纯地聊一聊流式编程的实现,List、Map中的操作就不赘述了哈。
咱们由俭入奢吧,先来个简单的顺序数组,
ackage com.example.demomybatis.mytest;
import com.alibaba.fastjson.JSON;
import org.assertj.core.util.Lists;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.util.stream.Collectors.*;
/**
* desc:
*
* @author 笔下天地宽
* @date 2022/8/15 15:28
*/
public class StreamStudy {
public static void main(String[] args) {
//1.简单生成一个顺序数组
List tempList = Stream.iterate(1, i -> i + 1).limit(20).collect(Collectors.toList());
//或者
List tempIntStream = IntStream.range(1, 20)
//转换成stream
.boxed()
.collect(Collectors.toList());
// tempList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
System.out.println("tempIntStream = " + tempIntStream);
}
}
/**
* desc:
*
* @author 笔下天地宽
* @date 2022/8/15 15:25
*/
@Data
public class Book {
/**
* desc :名字
*/
private String name;
/**
* desc: 价格
*/
private BigDecimal price ;
/**
* desc: 页码数量
*/
private Integer number;
public Book() {
}
public Book(String name, BigDecimal price, Integer number) {
this.name = name;
this.price = price;
this.number = number;
}
}
/**
* desc:
*
* @author 笔下天地宽
* @date 2022/8/16 10:39
*/
@Data
public class Pen implements Serializable {
/**
* desc :名字
*/
private String name;
/**
* desc: 价格
*/
private BigDecimal price ;
public Pen() {
}
public Pen(String name, BigDecimal price) {
this.name = name;
this.price = price;
}
}
我把所有的引用包都丢上去了,还有两个对象类,后续就直接上关键代码了哈,这个简单数据没啥可说的,要么迭代,要么取出范围内的数字,然后collect一下,返回一个集合就行了,需要的时候,肯定比写个foreach舒服多了!
弄一个简单数据当然容易啊,但是来个斐波那契数列呢?那当然也没问题啦! 摆上!
List fList = Stream
// 生成数组(1,1)(1,2),(2,3),(3,5),(5,8)... 因为斐波那契数列需要前一位和前两位,故而临时存一下倒数第二位
.iterate(new int[]{1, 1}, i -> new int[]{i[1], i[0] + i[1]}).limit(20)
// 保留倒数第二位,因为倒数第二位是从斐波那契数列的第一个数字开始的
.map(vo -> vo[0]).collect(Collectors.toList());
System.out.println("fList = " + fList);
//[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765]
也就是根据斐波那契数列的属性,生成一个数组,然后筛选保留,最后得到结果。只要有特定规律,一般来说都能通过lambda生成对应集合。下面咱们通过简单的随即数生成,来体会下lambda聚合的魅力所在!
简单生成5个随即数,不定制,double类型,
List doubleList = Stream.generate(Math::random).limit(5).collect(Collectors.toList());
System.out.println("doubleList = " + doubleList);
//doubleList = [0.051194606060667724, 0.32254338606020416, 0.7561004043101184, 0.5811685207466939, 0.1405678285694163]
想自己定制下?行啊,一起瞅一瞅
//想要定制的话,可以写个无参方法进行定制
List integerList = Stream.generate(StreamStudy::random).limit(10).collect(Collectors.toList());
System.out.println("integerList = " + integerList);
//integerList = [39, 42, 40, 25, 46, 41, 46, 24, 18, 17]
/**
* desc : 无参化定制
* @return
*/
public static Integer random(){
return ThreadLocalRandom.current().nextInt(1, 50);
}
还要写个方法啊?不是说流式编程很简单么?
嗨!这个不是一步一步来么?直接上硬菜吃着硌牙不是?方法先干掉!
// 或者这样也可以
List tempIntList = IntStream.generate(new IntSupplier() {
@Override
public int getAsInt() {
return ThreadLocalRandom.current().nextInt(1, 50);
}
}).limit(10).boxed().collect(Collectors.toList());
System.out.println("integerList = " + tempIntList);
//integerList = [2, 30, 31, 8, 15, 30, 39, 37, 38, 7]
// 又或者这样?
List tempStreamIntList = Stream.generate(new Supplier() {
@Override
public Integer get() {
return ThreadLocalRandom.current().nextInt(1, 50);
}
}).limit(10).collect(Collectors.toList());
lambda里面直接实现一个接口,这个好点了吧?这种看着还是蛮清晰的,当然你想彻底摒弃这种匿名内部类?可以啊,来个最终的!
List integerListOther = Stream.generate(() -> ThreadLocalRandom.current().nextInt(1, 50)).limit(10).collect(toList());
System.out.println("tempStreamIntList = " + tempStreamIntList);
直接实现流式Supplier的具体代码,然后进行数据收集。要不要练个手?使用流式编程生成个100以内的质数?动动手再往下瞅?等你哦!
O啦,上个代码,接着看!
String intString = IntStream.range(2, 100)
// 质数筛选,比如31,比较16以内的数字,能被整除就代表不是质数,毕竟不是质数的话,前面一半有质因数,后面一半才有
.filter(num -> IntStream.range(2, num).noneMatch(i -> 2 * i < num && num % i == 0))
//转换Stream,呃,转个String,不用list了
.boxed().map(String::valueOf).collect(Collectors.joining(","));
System.out.println("intString = " + intString);
// intString = 2,3,4,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97
lambda 简单运用
这个提一下吧,如果还不熟练,赶紧去看下《JDK8从入门到放弃!》,温故而知新啊!
下面的运用主要包括,过滤、类型转换、归并、累加、统计。当然累加中BigDecimal与Integer、Double是有区别的,这个在计算的时候需要注意下哈。
List bookList = getBookList();
//lambda 简单运用
Set set = bookList.stream()
// 过滤,剩余的number必须大于110
.filter(vo -> !vo.getNumber() > 110)
// 跳过第一个
.skip(1)
// 类型转换,返回自己需要的类型,这里直接获取价格,也可以新建另一个类型get/set,组装新的类型,最后返回新类型的Collection
.map(vo -> {
return vo.getName();
})
// 重新组合成set,也可以拼接成字符串(Collectors静态应用了,可以省略) .collect(joining(",")); 也可以.foreach 遍历处理数据秀
.collect(toSet());
System.out.println("set = " + set);
//set = [历史, 数学, 语文, 地理]
// 简单看下归并
BigDecimal decimal = bookList.stream()
// 类型转换,返回自己需要的类型,这里直接获取价格,也可以新建另一个类型get/set,组装新的类型,最后返回新类型的Collection
.map(vo -> {
return vo.getPrice();
})
// 也可进行累加,或者这样写.reduce(BigDecimal.ZERO,BigDecimal::add),也可以统计个数哦。使用.count()!嘿嘿just so so?;
.reduce(BigDecimal::add).get();
System.out.println("decimal = " + decimal);
//decimal = 162.0
// 集合组装
public static List getBookList(){
Book book = new Book("语文", new BigDecimal("22.8"), 140);
Book book1 = new Book("语文", new BigDecimal("25"), 160);
Book book2 = new Book("数学", new BigDecimal("21"), 110);
Book book3 = new Book("数学", new BigDecimal("22.8"), 120);
Book book4 = new Book("英语", new BigDecimal("30"), 105);
Book book5 = new Book("历史", new BigDecimal("20.6"), 120);
Book book6 = new Book("地理", new BigDecimal("19.8"), 126);
return Lists.list(book,book1,book2,book3,book4,book5,book6);
}
}
集合分组
分组这个就很关键了,如果你用不到,感觉无所谓,若是真的用到了,你就会发现,lambda真是,太TMD香了!
依然是上文中组装的那个集合,我们来根据书籍名称分个组,两行代码直接解决!如果使用foreach,七八行代码都不止!估计还要创建好几个List,然后让回收器帮忙回收。
// a.直接过滤分组
Map> nameList = bookList.stream()
// 过滤大于20的
.filter(book -> book.getPrice().compareTo(new BigDecimal("20")) > 0)
// 也可先用 .sorted(Comparator.comparing(Book::getPrice)) 排序,sort也可以自定义,sort((a,b)>{return ...;})
// 通过名称分组
.collect(groupingBy(Book::getName));
System.out.println("nameList = " + nameList);
//nameList = {历史=[Book(name=历史, price=20.6, number=120)], 数学=[Book(name=数学, price=21, number=110), Book(name=数学, price=22.8, number=120)], 语文=[Book(name=语文, price=22.8, number=140), Book(name=语文, price=25, number=160)], 英语=[Book(name=英语, price=30, number=105)]}
要不来个秀一点的?根据名称和价格,返回页码数的分组!
Map> listMap = bookList.stream().collect(groupingBy(vo -> vo.getName() + vo.getPrice(), mapping(t -> t.getNumber(), toList())));
System.out.println("listMap = " + listMap);
// listMap = {地理19.8=[126], 语文22.8=[140], 数学22.8=[120], 语文25=[160], 数学21=[110], 英语30=[105], 历史20.6=[120]}
其实这个可读性就没那么高了,估计要瞅一下,才能大概明白是什么意思。mapping(t -> t.getNumber(), toList()),就是对Map中的value进行处理组装,最终返回到流的出口。
或者你想分组后再做个转换,如转换成Pen的名字和价格?也行!摆上。
List bookList = getBookList();
Map> setPenMap = bookList.stream().collect(groupingBy(Book::getName, mapping(t -> {
return new Pen(t.getName() + "笔", t.getPrice());
}, toSet())));
System.out.println("setPenMap = " + setPenMap);
// setPenMap = {历史=[Pen(name=历史笔, price=20.6)], 数学=[Pen(name=数学笔, price=21), Pen(name=数学笔, price=22.8)], 语文=[Pen(name=语文笔, price=22.8), Pen(name=语文笔, price=25)], 英语=[Pen(name=英语笔, price=30)], 地理=[Pen(name=地理笔, price=19.8)]}
归并转Map
许多时候,可能分组之后,我们并不想要一个list,而是想要其中的一个字段作为value,或者是几个字段作为value,怎么弄呢?lambda肯定帮我们想到了!
比如想获取每种书籍的页数
//Book::getName 是key赋值,或者写成vo->vo.getName()
//Book::getNumber 是value赋值,也可以写成v->v.getNumber()
//(a,b->a) 是一个BinaryOperator,是归并原则,就是有两个key是一样,取的是第一个的value保留,如果(a, b) -> b,这样就是保留第二个value了
Map map = bookList.stream().collect(toMap(Book::getName, Book::getNumber, (a, b) -> a));
System.out.println("map = " + map);
//map = {历史=120, 数学=110, 语文=140, 英语=105, 地理=126}
要不key一样,value不淘汰了?比如我想计算每种书籍的总价格,不能直接抹掉后面的哟!
这样其实算是分组了,不过我更感觉像map处理,放到这里了,哈哈哈!摆上!分组后直接把价格进行累加,后面通过key就能直接获取总价格了。
Map decimalMap = bookList.stream().collect(groupingBy(Book::getName, reducing(BigDecimal.ZERO, book -> book.getPrice(), BigDecimal::add)));
System.out.println("decimalMap = " + decimalMap);
// decimalMap = {历史=20.6, 数学=43.8, 语文=47.8, 英语=30, 地理=19.8}
如果累加页呢,不是说BigDecimal和Integer不一样么?那更easy!
Map integerMap = bookList.stream().collect(groupingBy(Book::getName, summingInt(vo -> vo.getNumber())));
System.out.println("integerMap = " + integerMap);
// integerMap = {历史=120, 数学=230, 语文=300, 英语=105, 地理=126}
好了,我平时工作中常用的lambda操作已经全部在上面了,如果那位老铁有其他要求,可以写在评论,一起唠嗑唠嗑哈。
下面来说几个接口吧,或者说是FunctionalInterface?概念性的东西不多啰嗦,看一看例子就明白了!
我们都能发现流式编程书写起来简单许多,不过如果一个逻辑中流式编程特别多,也就是业务太复杂了。或者说,一些流式编程可以复用,能不能抽取出来,然后相当于一个方法直接调用了? 比如:t->t.getName();
当然可以!
JDK8中为我们提供了这么一个注解 @FunctionalInterface!被这个注解注释的接口,有且只能有一个接口是未被实现的,可以没有其他的接口,也可以其他接口是默认实现的,都可以。我们来看看JDK8中帮我们提供的几个关键接口。
Predicate
关键接口:
boolean test(T t);
这是一个判断的lambda表达式定义,比如你可以这样写:
public static void main(String[] args) {
Predicate predicate = vo->vo.getNumber() > 110;
Book book = new Book("PE", new BigDecimal("23"), 120);
boolean flag = predicate.test(book);
System.out.println("flag = " + flag);
//flag = true
}
vo->vo.getNumber() > 110 这个表达式肯定是不能作为入参传入方法的,一般是在方法中直接实现,但是定义了Predicate之后,这个表达式就相当于一个参数了,也就是一个判断的lambda表达式,可以直接进行传递调用。
下面再看下其他的几个,直接来几行代码,大家先体会下哈。
Function
关键接口:
R apply(T t);
T为入参,R为返回的参数,相当于类型转换。
public static void main(String[] args) {
Function function = vo->vo.getPrice();
Book book = new Book("PE", new BigDecimal("23.5"), 120);
BigDecimal price = function.apply(book);
System.out.println("price = " + price);
//price = 23.5
}
Consumer
关键接口
void accept(T t);
这个无返回,只是类型的处理。
Consumer consumer = vo->vo.setPrice(new BigDecimal("33.8"));
Book book = new Book("PE", new BigDecimal("23.5"), 120);
consumer.accept(book);
System.out.println("book = " + book);
// book 价格已经修改
// book = Book(name=PE, price=33.8, number=120)
Supplier
关键接口
T get();
无参,直接返回一个类型,这个之前代码中就有简单实现的,来个简单的例子吧。就生成一个随机数~
public static void main(String[] args) {
Supplier supplier = () -> ThreadLocalRandom.current().nextInt(1, 50);
Integer number = supplier.get();
System.out.println("number = " + number);
//number = 17
}
是不是有点眼熟?
主要的接口应该就这几个,其他的都是一些功能延伸,若果这几个简单的能看懂的话,其他的也不在话下!
个人感觉吧,这些接口作为入参,代码的可读性估计会变差一点,你不信,来列举一个Collectors里面的一个接口实现,看看你眼力如何
public staticCollector > groupingBy(Function super T, ? extends K> classifier, Collector super T, A, D> downstream) { return groupingBy(classifier, HashMap::new, downstream);} 乍一看,我都没看明白这方法入参都是啥子! 我再乍! 三乍! 好了,大概看明白了一点,要知道咋调用?对比自己的参数一个个看吧,可能还需要一点点的,嗯,小测试!就调用这种方法,没测试,我是不敢上线啊! 总的来说,流式编程简洁方便,许多功能都已经帮忙封装好了,不过就可读性而言,稍稍差了那么一丢丢,在编写代码的时候最好加上注释,不然一个方法里面来个十个八个Consumer,Function,Predicate。然后在方法中层层嵌套,最后估计都没有review的欲望了~ 好了,流式编程就扯到这里吧,有什么高见的话可以在评论区交流。 对了,都看到这里了,顺便点个赞呗! no sacrifice,no victory!