Java 8 入门使用
哈喽,大家好,我是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 表达式,便称之为函数式接口。
使用举例:
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实现加法和减法
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 } }
它是怎么做到的呢?
是因为一种新的操作符”->“,该操作符被称之为箭头操作符,操作符将表达式分成了两部分:
(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 | 是否所有元素匹配 | 最后步骤 | -- | 判断流中所有元素是否符合我们要的判断 |
接下来针对每个方法,我们都举个简单的例子进行使用
@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的订单时,我们可以这样写:
@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的订单的顾客姓名时,我们可以这样写:
//先过滤、总价格大于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方法,我们可以这样写:
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总价格倒序排序,只要前五个时,我们就可以这样写:
List<Order> list = data; // sorted(排序的标准) 倒序:reversed() 只要五条:limit list.stream().sorted(comparing(Order::getTotalPrice).reversed()).limit(5).forEach(System.out::println);
我们要让它按照totalPrice总价格倒序排序,只要第三个和第四个时,我们可以这样写:
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的订单的顾客姓名,且对它进行去重时,我们可以这样写:
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类,以下是它的静态方法:
举几个collect常用案例:
一、使用collect实现字符串拼接,随机生成一定位数的字符串
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....
//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:
//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计算平均购买的商品数量
//1是平均,2是购买数量,但是需要先求购买数量后求平均值 Double collect = list.stream(). collect( Collectors.averagingInt(order -> order.getOtherList().stream() .collect(Collectors.summingInt(OrderItem::getProductQuantity))) );
解析:先通过summingInt(OrderItem::getProductQuantity)获取集合里各自的购买数量,再通过averagingInt来算平均值来达成效果
四、使用collect分组
//使用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给数据分区
//下了单的订单 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 判空逻辑。
@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 的增强。
//系统缓存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则替代掉繁杂的逻辑,以下是它具体的实现源码:
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; }
computeIfAbsent方法的逻辑其实也是很简单,跟我们原本的写法逻辑一样,只是它的代码非常的简洁,不局限于任何一个实现逻辑。它能通过Function函数对应的方法来控制缓存的条件。不需要像以前一样,一种缓存方式需要写一个方法。
3.结语
我们来回顾一下,这次我给你简单的介绍了一下Java 8 中最为重要和常用的几个功能,其中Lambda、Stream、Optional操作,这些特性可以帮助我们写出简单易懂、可读性强的代码。有些案例可能不太好理解,我建议你对着代码逐一到源码中查看这些操作的方法定义,以及 JDK 中的代码注释。然后亲手多敲几遍,修改几次参数,加深对方法的印象。代码之道,在于多写多测多练习,以强化记忆、加深理解。