Java8 是一个里程碑式的版本,凭借如下新特性,让人对其赞不绝口。
随着对 Java8 新特性理解的深入,会被 Lambda 表达式(包含方法引用)、流式运算的美所迷恋,不由惊叹框架设计的美。
Lambda 表达式是匿名函数,可以理解为一段可以用参数传递的代码(代码像数据一样传递)。Lambda 表达式的使用需要有函数式接口的支持。
方法引用是对特殊 Lambda 表达式的一种简化写法,当 Lambda 体中只调用一个方法,此方法满足函数式接口规范,此时可以使用::
方法引用语法。
从语法表现力角度来讲,方法引用 > Lambda表达式 > 匿名内部类
,方法引用是高阶版的 Lambda 表达式,语言表达更为简洁,强烈推荐使用。
方法引用表达式无需显示声明被调用方法的参数,根据上下文自动注入。方法引用能够提高 Lambda 表达式语言的优雅性,代码更加简洁。下面以Comparator
排序为例讲述如何借助方法引用构建优雅的代码。
普通数据类型相对较容易理解。
// 正向排序(方法引用) Stream.of(11, 15, 11, 12).sorted(Integer::compareTo).forEach(System.out::println); // 正向排序 Stream.of(11, 15, 11, 12).sorted(Comparator.naturalOrder()).forEach(System.out::println); // 逆向排序 Stream.of(11, 15, 11, 12).sorted(Comparator.reverseOrder()).forEach(System.out::println);
(1)数据完好
数据完好有两重含义,一是对象本身不为空;二是待比较对象的属性值不为空,以此为前提进行排序操作。
// 对集合按照年龄排序(正序排列) Collections.sort(userList, Comparator.comparingInt(XUser::getAge)); // 对集合按照年龄排序(逆序排列) Collections.sort(userList, Comparator.comparingInt(XUser::getAge).reversed());
此示例是以Integer
类型展开的,同理Double
类型、Long
类型等数值类型处理方式相同。其中[Comparator]是排序过程中重要的类。
(2)数据缺失
数据缺失的含义是对象本身为空或者待比较对象属性为空,如果不进行处理,上述排序会出现空指针异常。
最常见的处理方式是通过流式运算中filter
方法,过滤掉空指针数据,然后按照上述策略排序。
userList.stream().filter(e->e.getAge()!=null).collect(Collectors.toList());
少数开发者在构建实体类时,String
类型遍地开花,在需要运算或者排序的场景下,String 的缺陷逐渐暴露出来。下面讲述字符串数值
类型排序问题,即不修改数据类型的前提下完成期望的操作。
实体类
public class SUser { private Integer userId; private String UserName; // 本应该是Double类型,错误的使用为String类型 private String score; }
正序、逆序排序
// 对集合按照年龄排序(正序排列) Collections.sort(userList, Comparator.comparingDouble(e -> new Double(e.getScore())));
数据类型转换排序时,使用 JDK 内置的 API 并不流畅,推荐使用commons-collection4
包中的排序工具类。了解更多,请移步查看[ComparatorUtils]。
// 对集合按照年龄排序(逆序排列) Collections.sort(userList, ComparatorUtils.reversedComparator(Comparator.comparingDouble(e -> new Double(e.getScore()))));
小结:通过以排序为例,实现 Comparator 接口、Lambda 表达式、方法引用三种方式相比较,代码可读性逐步提高。
内置的排序器可以完成大多数场景的排序需求,当排序需求更加精细化时,适时引入第三方框架是比较好的选择。
单列排序包含正序和逆序。
// 正序 Comparator<Person> comparator = Comparator.comparing(XUser::getUserName); // 逆序 Comparator<Person> comparator = Comparator.comparing(XUser::getUserName).reversed();
多列排序是指当待比较的元素有相等的值时,如何进行下一步排序。
// 默认多列均是正序排序 Comparator<XUser> comparator = Comparator.comparing(XUser::getUserName) .thenComparing(XUser::getScore);
// 自定义正逆序 Comparator<XUser> comparator = Comparator.comparing(XUser::getUserName,Comparator.reverseOrder()) .thenComparing(XUser::getScore,Comparator.reverseOrder());
流的操作包含如下三个部分:创建流、中间流、关闭流,筛选
、去重
、映射
、排序
属于流的中间操作,收集
属于终止操作。[Stream]是流操作的基础关键类。
(1)通过集合创建流
// 通过集合创建流 List<String> lists = new ArrayList<>(); lists.stream();
(2)通过数组创建流
// 通过数组创建流 String[] strings = new String[5]; Stream.of(strings);
应用较多的是通过集合创建流,然后经过中间操作,最后终止回集合。
筛选是指从(集合)流中筛选满足条件的子集,通过 Lambda 表达式生产型接口来实现。
// 通过断言型接口实现元素的过滤 stream.filter(x->x.getSalary()>10);
非空过滤
非空过滤包含两层内容:一是当前对象是否为空或者非空;二是当前对象的某属性是否为空或者非空。
筛选非空对象,语法stream.filter(Objects::nonNull)
做非空断言。
// 非空断言 java.util.function.Predicate<Boolean> nonNull = Objects::nonNull;
查看[Objects]类了解更详细信息。
去重是指将(集合)流中重复的元素去除,通过 hashcode 和 equals 函数来判断是否是重复元素。去重操作实现了类似于 HashSet 的运算,对于对象元素流去重,需要重写 hashcode 和 equals 方法。
如果流中泛型对象使用 Lombok 插件,使用@Data
注解默认重写了 hashcode 和 equals 方法,字段相同并且属性相同,则对象相等。更多内容可查看[Lombok 使用手册]
stream.distinct();
取出流中元素的某一列,然后配合收集以形成新的集合。
stream.map(x->x.getEmpId());
filter
和map
操作通常结合使用,取出流中某行某列的数据,建议先行后列
的方式定位。
Optional<MainExportModel> model = data.stream().filter(e -> e.getResId().equals(resId)).findFirst(); if (model.isPresent()) { String itemName = model.get().getItemName(); String itemType = model.get().getItemType(); return new MainExportVo(itemId, itemName); }
传统的Collectors
类中的排序支持 List 实现类中的一部分排序,使用 stream 排序,能够覆盖所有的 List 实现类。
// 按照默认字典顺序排序 stream.sorted(); // 按照工资大小排序 stream.sorted((x,y)->Integer.compare(x.getSalary(),y.getSalary()));
(1)函数式接口排序
基于 Comparator 类中函数式方法,能够更加优雅的实现对象流的排序。
// 正向排序(默认) pendingPeriod.stream().sorted(Comparator.comparingInt(ReservoirPeriodResult::getPeriod)); // 逆向排序 pendingPeriod.stream().sorted(Comparator.comparingInt(ReservoirPeriodResult::getPeriod).reversed());
(2)LocalDate 和 LocalDateTime 排序
新日期接口相比就接口,使用体验更加,因此越来越多的被应用,基于日期排序是常见的操作。
// 准备测试数据 Stream<DateModel> stream = Stream.of(new DateModel(LocalDate.of(2020, 1, 1)), new DateModel(LocalDate.of(2019, 1, 1)), new DateModel(LocalDate.of(2021, 1, 1)));
正序、逆序排序
// 正向排序(默认) stream.sorted(Comparator.comparing(DateModel::getLocalDate)).forEach(System.out::println); // 逆向排序 stream.sorted(Comparator.comparing(DateModel::getLocalDate).reversed()).forEach(System.out::println);
对流中的元素按照一定的策略计算。终止操作的底层逻辑都是由 reduce 实现的。
收集(collect)将流中的中间(计算)结果存储到集合中,方便后续进一步使用。为了方便对收集操作的理解,方便读者掌握收集操作,将收集分为普通收集
和高级收集
。
(1)收集为List
默认返回的类型为ArrayList
,可通过Collectors.toCollection(LinkedList::new)
显示指明使用其它数据结构作为返回值容器。
List<String> collect = stream.collect(Collectors.toList());
由集合创建流的收集需注意:仅仅修改流字段中的内容,没有返回新类型,如下操作直接修改原始集合,无需处理返回值。
// 直接修改原始集合 userVos.stream().map(e -> e.setDeptName(hashMap.get(e.getDeptId()))).collect(Collectors.toList());
(2)收集为Set
默认返回类型为HashSet
,可通过Collectors.toCollection(TreeSet::new)
显示指明使用其它数据结构作为返回值容器。
Set<String> collect = stream.collect(Collectors.toSet());
(1)收集为Map
默认返回类型为HashMap
,可通过Collectors.toCollection(LinkedHashMap::new)
显示指明使用其它数据结构作为返回值容器。
收集为Map
的应用场景更为强大,下面对这个场景进行详细介绍。希望返回结果中能够建立ID
与NAME
之间的匹配关系,最常见的场景是通过ID
批量到数据库查询NAME
,返回后再将原数据集中的ID
替换成NAME
。
ID 到 NAME 映射
@Data public class ItemEntity { private Integer itemId; private String itemName; }
准备集合数据,此部分通常是从数据库查询的数据
// 模拟从数据库中查询批量的数据 List<ItemEntity> entityList = Stream.of(new ItemEntity(1,"A"), new ItemEntity(2,"B"), new ItemEntity(3,"C")).collect(Collectors.toList());
将集合数据转化成 ID 与 NAME 的 Map
// 将集合数据转化成ID与NAME的Map Map<Integer, String> hashMap = entityList.stream().collect(Collectors.toMap(ItemEntity::getItemId, ItemEntity::getItemName));
ID
与Object
类映射
@Data public class ItemEntity { private Integer itemId; private String itemName; private Boolean status; }
将集合数据转化成 ID 与实体类的 Map
// 将集合数据转化成ID与实体类的Map Map<Integer, ItemEntity> hashMap = entityList.stream().collect(Collectors.toMap(ItemEntity::getItemId, e -> e));
其中Collectors
类中的toMap
参数是函数式接口参数,能够自定义返回值。
public static <T, K, U> Collector<T, ?, Map<K,U>> toMap(Function<? super T, ? extends K> keyMapper, Function<? super T, ? extends U> valueMapper) { return toMap(keyMapper, valueMapper, throwingMerger(), HashMap::new); }
(2)分组收集
流的分组收集操作在内存层次模拟了数据库层面的group by
操作,下面演示流的分组操作。[Collectors]类提供了各种层次的分组操作支撑。
流的分组能力对应数据库中的聚合函数,目前大部分能在数据库中操作的聚合函数,都能在流中找到相应的能力。
// 默认使用List作为分组后承载容器 Map<Integer, List<XUser>> hashMap = xUsers.stream().collect(Collectors.groupingBy(XUser::getDeptId)); // 显示指明使用List作为分组后承载容器 Map<Integer, List<XUser>> hashMap = xUsers.stream().collect(Collectors.groupingBy(XUser::getDeptId, Collectors.toList()));
映射后再分组
Map<Integer, List<String>> hashMap = xUsers.stream().collect(Collectors.groupingBy(XUser::getDeptId,Collectors.mapping(XUser::getUserName,Collectors.toList())));
将对象包装成集合的形式和将集合拆解为对象的形式是常见的操作。
返回默认类型的集合实例
/** * 将单个对象转化为集合 * * @param t 对象实例 * @param <T> 对象类型 * @param <C> 集合类型 * @return 包含对象的集合实例 */ public static <T, C extends Collection<T>> Collection<T> toCollection(T t) { return toCollection(t, ArrayList::new); }
用户自定义返回的集合实例类型
/** * 将单个对象转化为集合 * * @param t 对象实例 * @param supplier 集合工厂 * @param <T> 对象类型 * @param <C> 集合类型 * @return 包含对象的集合实例 */ public static <T, C extends Collection<T>> Collection<T> toCollection(T t, Supplier<C> supplier) { return Stream.of(t).collect(Collectors.toCollection(supplier)); }
使用默认的排序规则,注意此处不是指自然顺序排序。
/** * 取出集合中第一个元素 * * @param collection 集合实例 * @param <E> 集合中元素类型 * @return 泛型类型 */ public static <E> E toObject(Collection<E> collection) { // 处理集合空指针异常 Collection<E> coll = Optional.ofNullable(collection).orElseGet(ArrayList::new); // 此处可以对流进行排序,然后取出第一个元素 return coll.stream().findFirst().orElse(null); }
上述方法巧妙的解决两个方面的异常问题:一是集合实例引用空指针异常;二是集合下标越界异常。
基于流式计算中的并行流,能够显著提高大数据下的计算效率,充分利用 CPU 核心数。
// 通过并行流实现数据累加 LongStream.rangeClosed(1,9999999999999999L).parallel().reduce(0,Long::sum);
生成指定序列的数组或者集合。
// 方式一:生成数组 int[] ints = IntStream.rangeClosed(1, 100).toArray(); // 方式二:生成集合 List<Integer> list = Arrays.stream(ints).boxed().collect(Collectors.toList());
// 获取当前日期(包含时间) LocalDateTime localDateTime = LocalDateTime.now(); // 获取当前日期 LocalDate localDate = localDateTime.toLocalDate(); // 获取当前时间 LocalTime localTime = localDateTime.toLocalTime();
日期格式化
// 月份MM需要大写、小时字母需要大写(小写表示12进制) DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") // 获取当前时间(字符串) String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); System.out.println("dateTime = " + dateTime);
Duration duration = Duration.between(Instant.now(), Instant.now()); System.out.println("duration = " + duration);
如下方式获取的是 13 位时间戳,单位是毫秒。
// 方式一 long now = Timestamp.valueOf(LocalDateTime.now()).getTime(); // 方式二 long now = Instant.now().toEpochMilli();
在[Optional]类出现之前,null
异常几乎折磨着每一位开发者,为了构建健壮的应用程序,不得不使用繁琐的if
逻辑判断来回避空指针异常。解锁Optional
类,让你编写的应用健壮性更上一层楼。
ifPresent
方法提供了先判断是否为空,后进一步使用的能力。
链式取值是指,层层嵌套对象取值,在上层对象不为空的前提下,才能读取其属性值,然后继续调用,取出最终结果值。有时候只关心链末端的结果状态,即使中间状态为空,直接返回空值。如下提供了一种无 if 判断,代码简介紧凑的实现方式:
Optional<Long> optional = Optional.ofNullable(tokenService.getLoginUser(ServletUtils.getRequest())) .map(LoginUser::getUser).map(SysUser::getUserId); // 如果存在则返回,不存在返回空 Long userId = optional.orElse(null);
传统方式下构建树形列表需要反复递归调用查询数据库,效率偏低。对于一棵结点较多的树,效率更低。这里提供一种只需调用一次数据库,通过流将列表转化为树的解决方式。
/** * 列表转树 * * @param rootList 列表的全部数据集 * @param parentId 第一级目录的父ID * @return 树形列表 */ public List<IndustryNode> getChildNode(List<Industry> rootList, String parentId) { List<IndustryNode> lists = rootList.stream() .filter(e -> e.getParentId().equals(parentId)) .map(IndustryNode::new).collect(toList()); lists.forEach(e -> e.setChilds(getChildNode(rootList, e.getId()))); return lists; }