哈喽,大家好,我是Java小面。
今天主管老大静悄悄地把我叫了过去,跟我说他之前招的三年工作经验的后端来了,让我带一下.....Excuses me?为什么三年了还要我带?起初我以为只是说笑,想我帮新同事熟悉一下部门和自家产品,所以才这么说。
结果相处了两天发现,新同事在记忆力方面不错,讲起理论来朗朗上口,跟背歌词一样。但是实操方面,却是个只会捡螺丝的人,连拧螺丝都不是很顺手.....
一份数据过滤 转化 提取的过程,他硬生生的用了17行代码,三次for循环,我明明记得他的简历上写着擅长使用Java8特性来着....
等我写完这篇文章,我就甩他脸上去,顺便甩一份给主管老大,怪不得让我带他,还偷偷摸摸的吩咐我。
前言
Java8是目前最常用的JDK版本,相比Java 7,增加了很多功能,帮助开发者们简化了很多代码。比如:Lambda,Stream流操作。而Stream流操作是Java8版本针对数据集合做开发出来的特有的抽象概念。它可以按照我们编写的方式对集合进行处理,对数据进行复杂的查询、过滤、映射提取数据等操作。
说到Stream,我们往往会第一个想到I/O Stream,但是在Java8中,通过Lambda为核心的函数式编程,使得Java8有了一个新的Stream概念,用于解决当前集合库已有的弊端。
1.浅谈Lambda表达式
Lambda 表达式,也可称为闭包,我们也能把 Lambda理解为是一段可以传递的代码,与以前的method方法传参不一样的是,它所传递的是代码,是逻辑。通过函数式接口将代码像形参一样进行传递,是一种紧凑的代码风格,让我们写出的代码变得更加灵活,简洁,使Java的语言表达能力得到了提升。
Lambda表达式核心:函数式接口
什么是函数式接口呢?只有单一抽象方法的接口,使用 @FunctionalInterface 来描述,可转换成 Lambda 表达式,便称之为函数式接口。
使用举例:
代码语言:javascript复制public class LambdaTest {
//我们定义一个函数式编程
@FunctionalInterface
interface MathOperation {
//定义一个未知逻辑的抽象方法
int operation(int a, int b);
}
//定义一个内部私有、使用这个函数式的方法
private int operate(int a, int b, MathOperation mathOperation)
{
return mathOperation.operation(a, b);
}
}
接下来我们拿MathOperation实现加法和减法
代码语言:javascript复制public class LambdaTest {
//我们定义一个函数式编程
@FunctionalInterface
interface MathOperation {
//定义一个未知逻辑的抽象方法
int operation(int a, int b);
}
//定义一个内部私有、使用这个函数式的方法
private int operate(int a, int b, MathOperation mathOperation)
{
return mathOperation.operation(a, b);
}
public static void main(String[] args)
{
LambdaTest test = new LambdaTest();
//在内部 *实现* 对应的函数逻辑
//有类型声明的加法
MathOperation add = (int a, int b) -> a b;
//无类型声明的减法
MathOperation sub = (a, b) -> a - b;
//使用时传入参数和对应实现的方法
//add:传入加法的实现逻辑(int a, int b) -> a b
System.out.println("使用add传参:10 5=" test.operate(10, 5, add));
//sub:传入减法的实现逻辑(int a, int b) -> a - b
System.out.println("使用sub传参:10-5=" test.operate(10, 5, sub));
//控制台打印
//使用add传参:10 5=15
//使用sub传参:10-5=5
}
}
它是怎么做到的呢?
是因为一种新的操作符”->“,该操作符被称之为箭头操作符,操作符将表达式分成了两部分:
代码语言:javascript复制(int a, int b) -> a b
左侧:int a,int b 是Lambda表达式的参数,它对应到时候使用的MathOperation接口中抽象方法operation(int a, int b)的参数列表,而且必须顺序一致。
右侧:a b 是 Lambda表达式中所需要执行的功能,也就是operation(int a, int b)抽象方法的内部逻辑。
如果你不清楚自己定义的是否是函数式编程,可以用@FunctionalInterface来判断。
接下来,为了更好的了解Lambda表达式的实战效果,我们进入Stream操作环节。
2.Java8三个重要方面
使用Stream简化集合操作
对于开发的好处:
一、方便自己:通过使用函数式编程来对数据集合进行处理,简洁且意图明确的代码方便你后续回忆,并且使用Stream接口还能让你从此告别复杂的for循环。
二、方便他人:规范的存在拉近了开发人员之间的距离,统一使用Java8的stream,不仅大家都能理解到你的用意,还减少了双方对代码编写的分歧。
如果要拿Stream与MySQL做对比的话,以下这个表可以帮助你清晰的认识Stream。
方法 | 中文 | 操作类型 | 对比SQL | 作用 |
---|---|---|---|---|
filter | 筛选/过滤 | 中间步骤 | where | 对数据流进行过滤,过滤掉不符合传入条件 |
map | 转换/投影 | 中间步骤 | select | 根据传入的函数、对流中的每个元素进行转换 |
flatMap | 扁平化 | 中间步骤 | -- | 相当于map flat,先通过map把每个元素转换为流,再通过把所有流链接在一起扁平化展开 |
sorted | 排序 | 中间步骤 | order by | 使用传入的比较器,对流中的元素进行比较 |
distinct | 去重 | 中间步骤 | distinct | 对流中的元素进行去重,原理是Object.equals判重 |
skip&limit | 分页 | 中间步骤 | limit | 跳过部分元素以及限制元素数量 |
collect | 收集 | 最后步骤 | -- | 对流进行最后的收集操作,把流转换成我们想要的数据格式 |
forEach | 遍历 | 最后步骤 | -- | 对流中每一个元素进行遍历 |
anyMatch | 是否有元素匹配 | 最后步骤 | -- | 判断流中是否有一个元素符合我们要的判断 |
allMatch | 是否所有元素匹配 | 最后步骤 | -- | 判断流中所有元素是否符合我们要的判断 |
接下来针对每个方法,我们都举个简单的例子进行使用
代码语言:javascript复制@Data
public class Order {
private Long id;
private Long customerId;//顾客ID
private String name;//顾客姓名
private List<OrderItem> otherList;//订单商品明细
private Double totalPrice;//总价格
private LocalDateTime placedAt;//下单时间
}
@Data
public class OrderItem {
private Long productId;//商品ID
private String productName;//商品名称
private Double productPrice;//商品价格
private Integer productQuantity;//商品数量
}
filter方法
.filter()方法可以实现对数据的过滤操作,相当于SQL中的where。
它可以代替我们日常java开发中的for循环 equals匹配方法。
有个List集合,如果我们要获取顾客姓名叫Tony的,总金额大于100的订单时,我们可以这样写:
代码语言:javascript复制@Test
public void testFilter(){
List<Order> list = new ArrayList<>();
list.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
list.add(Order.builder().name("Sam").id(2L).totalPrice(91.0).build());
list.add(Order.builder().name("Tony").id(3L).totalPrice(21.0).build());
list.add(Order.builder().name("Sam").id(4L).totalPrice(131.0).build());
//先过滤name叫Tony的,再过滤总价格大于100的,然后打印出来
//以前的做法
for (Order order : list) {
Double totalPrice = order.getTotalPrice();
String name = order.getName();
if (totalPrice>100.0 && "Tony".equals(name)) {
System.out.println(order);
}
}
//Java 8的做法
list.stream().filter(e -> "Tony".equals(e.getName()))
.filter(e -> e.getTotalPrice()>100.0)
.forEach(System.out::println);
}
Java8的写法相对于以前的写法,在实现这个功能需求上,在写法上会相对的简洁很多,并且使用filter一看就知道,我打算过滤什么。而以前的做法把所有判断条件堆积在一起,if中判断条件一旦过多就会非常不美观,代码冗长且可阅读性很差。
而且使用Java 8 的话,我可以直接一行就可以完成这项功能,不像以前那样需要7行才可以解决。
map方法
.map()方法可以做转化提取,类似于SQL的select。
在以前的java开发中,我们需要先for循环遍历,然后再把需要的字段打印出来,但是使用map就可以完全替换掉它。
有个List集合,如果我们要获取总金额大于100的订单的顾客姓名时,我们可以这样写:
代码语言:javascript复制
//先过滤、总价格大于100的,再把name转化出来,然后打印出来
//以前的做法
for (Order order : list) {
Double totalPrice = order.getTotalPrice();
String name = order.getName();
if (totalPrice>100.0 ) {
System.out.println(name);
}
}
//Java 8的做法
list.stream().filter(e -> e.getTotalPrice()>100)
.map(Order::getName)
.forEach(System.out::println);
Java8抽取name,也只需要在使用filter过滤完后再用map(ClassName::getXxx)抽取即可,不需要像以前一样,for循环对每个类的金额进行判断后再get出来。
flatMap方法
.flatMpa()方法是一种扁平化操作,相当于map flat操作,常用于获取一个list中嵌套的所有orderList,然后进行流操作。
有个List集合,它嵌套了一个订单详情的otherList,我需要获取这个List集合里所有订单的总价格。
我们来分析一下这个做法,正常情况下,我们需要遍历这个List,然后拿到每个Order对象里的otherList集合对象,然后遍历这个集合对象,使用商品单价productPrice 乘以 商品数量productQuantity,把得到的值放到一个list里,加法后打印出来。
如果使用flatMap方法,我们可以这样写:
代码语言:javascript复制
List<Order> list = data;
//先拿到每个Order对象里的otherList集合对象的流,然后让每个对象的商品单价productPrice 乘以 商品数量productQuantity,
//以前的做法
double sum1 = 0.0f;
for (Order order : list) {
List<OrderItem> otherList = order.getOtherList();
for (OrderItem item : otherList) {
double i = item.getProductQuantity() * item.getProductPrice();
sum1 =i;
}
}
//现在的做法
double sum = list.stream().flatMap(order -> order.getOtherList().stream())
.mapToDouble(item -> item.getProductQuantity() * item.getProductPrice())
.sum();
System.out.println(sum);
这个方法可以用于计算集合内的集合的属性。通过以前的做法可以看到,我们需要经过两次for循环才能计算内嵌的集合的值,之后加起来才是最后的总数。但是通过flatMap,我们可以把所有的内嵌集合都汇聚在一起,然后再通过mapToDouble来计算它们的总值。这样是不是就简单多了。
sorted方法、skip方法、limit方法
.sorted() 方法可以用于集合内排序,相当于SQL中的order by。
.skip()方法可以用于跳过数据。
.limit()方法可以约束集合里的数量。
假设有个List集合
我们要让它按照totalPrice总价格倒序排序,只要前五个时,我们就可以这样写:
代码语言:javascript复制
List<Order> list = data;
// sorted(排序的标准) 倒序:reversed() 只要五条:limit
list.stream().sorted(comparing(Order::getTotalPrice).reversed()).limit(5).forEach(System.out::println);
我们要让它按照totalPrice总价格倒序排序,只要第三个和第四个时,我们可以这样写:
代码语言:javascript复制
List<Order> list = data;
//使用skip(跳过多少条),limit(取多少条)
list.stream().sorted(Comparator.comparing(Order::getTotalPrice)
.reversed())
.skip(2).limit(2)
.forEach(System.out::println);
distinct方法
.distinct()方法是数据去重,类似于SQL中的distinct。
有一个List集合,我们要获取总金额大于100的订单的顾客姓名,且对它进行去重时,我们可以这样写:
代码语言:javascript复制
List<Order> list = new ArrayList<>();
list.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
list.add(Order.builder().name("Sam").id(2L).totalPrice(91.0).build());
list.add(Order.builder().name("Tony").id(3L).totalPrice(21.0).build());
list.add(Order.builder().name("Sam").id(4L).totalPrice(131.0).build());//先过滤、总价格大于100的,再把name转化出来,然后打印出来
list.stream().filter(e -> e.getTotalPrice()>100)
.map(Order::getName).distinct()
.forEach(System.out::println);
collect方法
collect方法是收集操作,对流进行终止的操作,然后把流转换成我们需要的数据格式。也可以理解成要把数据装到一个对象里的操作,不再进行数据的其他操作了,所以使用后就没办法再使用上面提到的那些方法了,除非再使用一次.stream()方法。
collect中比较重要的就是Collectors类,以下是它的静态方法:
方法 | 返回类型 | 作用 |
---|---|---|
toList | List | 把流中的元素收集成为一个List |
toSet | Set | 把流中的元素收集成为一个Set,去重 |
toCollection | Collection | 把流中的元素收集成为指定的集合 |
counting | Long | 计算元素中的个数 |
summingInt | Integer | 对元素中某个整形属性进行求和 |
averagingInt | Double | 对元素中的某个整形属性进行求平均值 |
joining | String | 连接流中元素toString后的字符串 |
minBy | Optional | 选出最小值 |
maxBy | Optional | 选出最大值 |
collectionAndThen | 根据收集器返回 | 传入一个收集器,对其进行转化 |
groupBy | Map<K,List> | 根据指定的属性进行分组,指定的属性为key |
partitionBy | Map<Boolean,List> | 对元素中的某个值进行判断,true为一组,false为一组 |
举几个collect常用案例:
一、使用collect实现字符串拼接,随机生成一定位数的字符串
代码语言:javascript复制
Random random = new Random();
//实现逻辑是,获取随机48到122中的数字,过滤掉转成字符后是特殊字符的数字,然后取30位,使用collect方法用StringBuilder拼接。
String id = random.ints(48, 122)
.filter(i -> (i < 57 || i > 65) && (i < 90 || i > 97))
.mapToObj(i -> (char)i).limit(30)
.collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
System.out.println(id);
二、使用collect实现转List、LinkedList、Set....
代码语言:javascript复制//1使用list装数据
List<String> list1 = list.stream().map(Order::getName)
.collect(Collectors.toList());
//1使用list装数据
List<String> list2 = list.stream().map(Order::getName)
.collect(Collectors.toCollection(ArrayList::new));
//2使用Linkedlist装数据
LinkedList<String> linkedList = list.stream().map(Order::getName)
.collect(Collectors.toCollection(LinkedList::new));
//3使用set来装数据
Set<String> set = list.stream().map(Order::getName)
.collect(Collectors.toSet());
其源码类似,都是定义对应T的数据类型集合,然后addAll进去,return就结束了,这个T可以自定义传入,也可以使用定义好的比如toList:
代码语言:javascript复制//1
public static <T>Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },CH_ID);
}
//2
public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
return new CollectorImpl<>(collectionFactory, Collection<T>::add,
(r1, r2) -> { r1.addAll(r2); return r1; },CH_ID);
}
//3
public static <T> Collector<T, ?, Set<T>> toSet() {
return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
(left, right) -> { left.addAll(right); return left; },CH_UNORDERED_ID);
}
三、使用collect计算平均购买的商品数量
代码语言:javascript复制
//1是平均,2是购买数量,但是需要先求购买数量后求平均值
Double collect = list.stream().
collect(
Collectors.averagingInt(order -> order.getOtherList().stream()
.collect(Collectors.summingInt(OrderItem::getProductQuantity)))
);
解析:先通过summingInt(OrderItem::getProductQuantity)获取集合里各自的购买数量,再通过averagingInt来算平均值来达成效果
四、使用collect分组
代码语言:javascript复制
//使用groupingby以购买人名字为key,用Collectors.counting方法统计每个人下单的数量,然后comparingByValue().reversed()倒序排序
List<Map.Entry<String, Long>> collect1 = list.stream().collect(
Collectors.groupingBy(Order::getName,Collectors.counting())
).entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed()).collect(Collectors.toList());
五、使用collect partitionBy给数据分区
代码语言:javascript复制
//下了单的订单
List<Order> list = new ArrayList<>();
list.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
list.add(Order.builder().name("Sam").id(2L).totalPrice(91.0).build());
list.add(Order.builder().name("Tony").id(3L).totalPrice(21.0).build());
list.add(Order.builder().name("Sam").id(4L).totalPrice(131.0).build());//付了款的订单
List<Order> list2 = new ArrayList<>();
list2.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
//分组,把list分成true和false,true为已经付了款的,false为没付款的订单
//逻辑:list2.stream().map(Order::getId)获取付了款的id,partitioningBy用anyMatch匹配下了单的订单,形成的分组。
Map<Boolean, List<Order>> data = list.stream().collect(
Collectors.partitioningBy(order -> list2.stream().map(Order::getId).anyMatch(e -> e.equals(order.getId())))
);
System.out.println(data);
使用 Optional 简化判空逻辑
除了Optional以外,还有OptionalInt,OptionalDouble...等,各种不同基本类型的可空对象。此外Java8还定义了用于引用类型的Optional类,使用Optional,不仅可以避免数据联级内的空指针问题,它还给我们开发者提供了实用的方法避免判空逻辑。减少了我们对数据判断的代码编写,提升效率。
以下是一些例子,演示了如何使用 Optional 来避免空指针,以及如何使用它的 fluent API 简化冗长的 if-else 判空逻辑。
代码语言:javascript复制 @Test(expected = IllegalArgumentException.class)
public void testOptional() {
//通过get方法获取Optional中的值
System.out.println(Optional.of(100).get());
//通过ofNullable来初始化一个空字符串对象,通过orElse方法实现Optional中无数据的时候返回一个默认值
System.out.println(Optional.ofNullable(null).orElse(""));
//double的Optional对象,isPresent可判断OptionalDouble有无数据
System.out.println(OptionalDouble.empty().isPresent());
//通过map方法可以对Optional对象进行级联转换,不会出现空指针
System.out.println(Optional.of(100).map(Math::incrementExact).get());
//使用filter实现Optional中数据的过滤,得到一个Optional,使用orElse提供默认值
System.out.println(Optional.of(1).filter(e -> e % 2 == 0).orElse(null));
//通过orElseThrow在无数据时抛出异常
System.out.println(Optional.empty().orElseThrow(IllegalArgumentException::new));
}
下面是关于Optional的方法整理:
方法 | 作用 |
---|---|
.empty() | 返回一个空的Optional |
.orElse() | 有值则返回,否则返回默认值 |
.orElseGet() | 有值则返回,否则返回Supplier函数提供的值 |
.orElseThrow() | 有值则返回,否则返回Supplier函数生成的异常 |
.of() | 将值进行Optional包装,值为null则抛出NullPointerException异常 |
.ofNullable() | 将值进行Optional包装,值为null则生成空的Optional |
.ifPresent() | 有值则使用Consumer函数消费值 |
.ifPresent() | 判断是否有值 |
.get() | 有值则获取值,否则抛出NoSuchElementException异常 |
.map() | 如果有值,则应用传入的Function函数 |
.filter() | 如果有值且匹配传入的Predicate函数,则返回包含值的Optional,否则返回空的Optional |
.stream() | 如果有值则返回包含值的steam,否则返回空的stream |
Java 8 类对于函数式 API 的增强
除了Stream以外,Java 8中有很多类都实现了函数式的功能,比如ConcurrentHashMap。ConcurrentHashMap一般常用于系统缓存,接下来我们通过ConcurrentHashMap来看看Java 8 的增强。
代码语言:javascript复制 //系统缓存map
private Map<Long, Order> cacheMap = new ConcurrentHashMap<>();
//创建一个数据集合
List<Order> list = new ArrayList<>();
//原本获取缓存的写法
private Order beforeGetCacheMethod(Long id) {
Order order = null;
//Key存在,返回Value
if (cache.containsKey(id)) {
order = cacheMap.get(id);//获取后直接赋值,跳过else直接return
} else {
//不存在,则获取Value
//需要遍历数据源查询获得order
for (Order o : list) {
if (o.getId().equals(id)) {//判断是否存在这个id
order = o;//存在则直接赋值
break;
}
}
//判断order是否为null
if (order != null)
cacheMap.put(id, order);//加入map中去
}
return order;//返回对象
}
//Java 8 中,我们利用 ConcurrentHashMap 用可以这样实现繁琐的操作
//现在获取缓存的写法
private Order nowGetCacheMethod(Long id) {
//当Key不存在的时候提供一个Function来代表根据Key获取Value过程
return cacheMap.computeIfAbsent(id, item ->
list.stream()
.filter(p -> p.getId().equals(item)) //通过对象里的id属性过滤数据
.findFirst() //只找第一个,其他的不理会。得到Optional<Order>
.orElse(null)); //如果找不到符合要求的,则返回一个默认值,这里我们设置为null
}
computeIfAbsent则替代掉繁杂的逻辑,以下是它具体的实现源码:
代码语言:javascript复制
default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {
//判断传入的Function是否是null
Objects.requireNonNull(mappingFunction);
V v;
//给v赋值的同时顺便判断是否为null
if ((v = get(key)) == null) {
V newValue;
//执行传入的Funcation函数后判断所得值是否为null
if ((newValue = mappingFunction.apply(key)) != null) {
//不为null则存入map里,然后再返回值
put(key, newValue);
return newValue;
}
}
return v;
}