Java8 Lambda 场景示例

本贴最后更新于 2316 天前,其中的信息可能已经东海扬尘

所有代码及完整测试用例在 https://github.com/wangyuheng/java8-lambda-sample

收集整理了一些实际业务场景下, 对 Java8 函数的使用.

预警: 代码较多, 比较无趣

阅读前提

  1. 了解 java8 的一些新特性, 如: Optional、 Stream
  2. 对函数式编程感兴趣

背景

一切的一切,源于不喜欢在代码中写太多的 if else.

scene & solution

1. Optional

Optional 应该可以算作对 null 的一种优雅封装, 比如如下场景
我有一个枚举, 并且提供了一个 parse 方法, 可以根据 key 返回对应的枚举值.

1.1 enum parse

enum BooleanEnum { // 封装boolean TRUE(1, "是"), FALSE(0, "否"); private Integer key; private String label; BooleanEnum(Integer key, String label) { this.key = key; this.label = label; } BooleanEnum parse(Integer key) { ... return ... } }

但事实可能没那么美好, 如果这个 parse 方法传入了一个非法的 key, 比如"-999", 那么应该如何处理?

  1. 返回 null
  2. 返回 default
  3. 抛出 IllegalException

可能性多了, 在调用不同方法时, 就需要考虑这个方法会选择哪种方案. 比如通过方法是否声明了 IllegalException 异常进行判断、对每个结果进行 null!= result 判断. 这时, 方法的提供者就应该考虑, 返回一个 Optional 对象, 方便调用方进行下一步的判断处理.

public static Optional<BooleanEnum> parse(Integer key) { return Arrays.stream(values()).filter(i -> i.getKey().equals(key)).findAny(); }

1.2 nested isPresent()

基于上述思考, 我们会将方法的返回值通过 Optional 进行封装, 可能就出现了如下代码

private Optional<User> login_check_by_if(String username, String password) { Optional<UserPO> optionalUserPO = findByUsername(username); if (optionalUserPO.isPresent()) { UserPO po = optionalUserPO.get(); if (password.equals(po.getPassword())) { return Optional.of(convert(po)); } else { return Optional.empty(); } } else { return Optional.empty(); } }

这说明使用者知道了 Optional 可以方便进行非空判断, 但是远不止于此. Optional 已经为我们开启了一个流, 所以基于惰性求值的原则, 我们可以将上述代码简化.

private Optional<User> login_check_by_map(String username, String password) { return findByUsername(username) .filter(po -> Objects.equals(po.getPassword(), password)) .map(this::convert); }

1.3 .get().get().get()

我们在使用 ServerWebExchange 获取 session 值的时候, 为了保证健壮性, 需要针对每一级进行非空判断, 如

private String getTreasureIf(Exchange exchange) { if (null != exchange) { Session session = exchange.getSession(); if (null != session) { Request request = session.getRequest(); if (null != request) { return request.getTreasure(); } else { return null; } } else { return null; } } else { return null; } }

基于上述原则, 同样可以通过 Optional 进行简化

private Optional<String> getTreasureOptionalMap(Exchange exchange) { return Optional.ofNullable(exchange) .map(Exchange::getSession) .map(Session::getRequest) .map(Request::getTreasure); }

去掉了 if else 之后, 世界是不是清爽了很多?

2. Distinct field

mysql 中, 针对 field 去重, 我们可以直接使用 distinct. 那么在 java 中如何对一个 list 进行去重呢? 可能是这样

public static <T> List<T> removeDuplication(List<T> list, String... keys) { if (list == null || list.isEmpty()) { System.err.println("List is empty."); return list; } if (keys == null || keys.length < 1) { System.err.println("Missing parameters."); return list; } for (String key : keys) { if (StringUtils.isBlank(key)) { System.err.println("Key is empty."); return list; } } List<T> newList = new ArrayList<T>(); Set<String> keySet = new HashSet<String>(); for (T t : list) { StringBuffer logicKey = new StringBuffer(); for (String keyField : keys) { try { logicKey.append(BeanUtils.getProperty(t, keyField)); logicKey.append(SEPARATOR); } catch (Exception e) { e.printStackTrace(); return list; } } if (!keySet.contains(logicKey.toString())) { keySet.add(logicKey.toString()); newList.add(t); } else { System.err.println(logicKey + " has duplicated."); } } return newList; }

其实我们只是借助了 Set 值不可重复的特性, 为了工具类的通用性, 我们根据 key 和反射进行匹配.
听起来和看起来都很复杂, 我们尝试进行优化, java8 已经允许我们使用函数作为入参, 所以我们可以把获取 field 的方法传入

@Test public void should_return_2_because_distinct_by_age() { userList = userList.stream() .filter(distinctByKey(User::getName)) .collect(Collectors.toList()); userList.forEach(System.out::println); assertEquals(2, userList.size()); } private static <T, R> Predicate<T> distinctByKey(Function<T, R> keyExtractor) { Set<R> seen = ConcurrentHashMap.newKeySet(); return t -> seen.add(keyExtractor.apply(t)); }

我们传入一个 Function, 返回一个 Predicate. 而 Predicate 正好是 Stream#filter 的入参.

3. Predicate union predicate

如果需要一个工具类,用来判断 url 是否符合设定的 pattern.

private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); private boolean includedPathSelector(String antPattern, String urls) { if(StringUtils.isEmpty(urls) || StringUtils.isEmpty(antPattern)) { return false; } else { for (String url : urls.split(", ")) { if (Objects.nonNull(url)) { if (ANT_PATH_MATCHER.match(antPattern, url.trim())) { return true; } } } } return false; }

如果 antPattern 也是一个数组集合, 那么就需要进行嵌套循环进行判断. 复杂到不想实现.
所以我们通过 reduce&Predicate#or 进行判断

private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher(); private Predicate<String> includedPathSelector(String urls) { if(StringUtils.isEmpty(urls)) { return x -> false; } return Pattern.compile(", ").splitAsStream(urls) .filter(Objects::nonNull) .map(String::trim) .map(this::ant) .reduce(p -> false, Predicate::or); } private Predicate<String> ant(final String antPattern) { if (null == antPattern) { return input -> false; } return input -> ANT_PATH_MATCHER.match(antPattern, input); }

这里希望说明的是返回 Predicate 对比直接返回 boolean 有一个好处是可以通过语义更明确的链接进行组合, 如 andor

@Test public void should_return_true_when_one_of_split_item_match_predicate_or() { Predicate<String> predicate1 = this.includedPathSelector("/permission"); Predicate<String> predicate2 = this.includedPathSelector("/config"); assertTrue(predicate1.or(predicate2).test("/config")); assertTrue(predicate1.or(predicate2).test("/permission")); } @Test public void should_return_false_when_one_of_split_item_match_predicate_and() { Predicate<String> predicate1 = this.includedPathSelector("/permission"); Predicate<String> predicate2 = this.includedPathSelector("/config"); assertFalse(predicate1.and(predicate2).test("/config")); assertFalse(predicate1.and(predicate2).test("/permission")); }

4. map & group

Stream 提供了丰富的分组、聚合功能. 比如业务中需要把 List<Item> 转换为一个 Map<String, Item>, key 为 item 的 id, value 为 item 本身, 或者 item 中的某个属性. 又或者基于某个属性字段进行分组, 得到一个 Map<String, List<Item>>

@Test public void map_item_arr_by_uid() { Map<String, List<Item>> uidMap = new HashMap<>(); for (Item item : list) { if (uidMap.containsKey(item.getUid())) { uidMap.get(item.getUid()).add(item); } else { List<Item> itemList = new ArrayList<>(); itemList.add(item); uidMap.put(item.getUid(), itemList); } } uidMap.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v))); }

直接看如何利用 Stream 实现

@Data @AllArgsConstructor private static class Item { private Long id; private String uid; } private List<Item> list; @Before public void setUp() { long id = 0L; list = Arrays.asList(new Item(++id, "a"), new Item(++id, "a"), new Item(++id, "b"), new Item(++id, "b"), new Item(++id, "c"), new Item(++id, "d") ); System.out.println("--------------------"); } @Test public void map_item_by_id() { Map<Long, Item> idMap = list.stream() .collect(Collectors.toMap(Item::getId, Function.identity())); idMap.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v))); } @Test public void map_item_arr_by_uid() { Map<String, List<Item>> uidMap = list.stream() .collect(Collectors.groupingBy(Item::getUid, Collectors.toList())); uidMap.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v))); } @Test public void map_uid_arr_by_uid() { Map<String, List<Long>> uidMapString = list.stream() .collect(Collectors.groupingBy(Item::getUid, Collectors.mapping(Item::getId, Collectors.toList()))); uidMapString.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v))); } @Test public void map_item_count_by_uid() { Map<String, Long> uidMapCount = list.stream() .collect(Collectors.groupingBy(Item::getUid, Collectors.mapping(Function.identity(), Collectors.counting()))); uidMapCount.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v))); }

5. concurrent & paged

另一个业务中常见的操作是因为原数据比较大, 所以我们将数据拆分为多页, 并设计多个线程并发的进行相应的业务处理.
大致思路应该是

  1. 设定每页 period, 计算页数
  2. 设定 CountDownLatch
  3. 创建线程池
  4. 遍历 list 并通过 subList 拆分
  5. 线程处理对应的 subList

java8 允许将函数作为入参, 所以第 5 步的操作, 我们可以把处理逻辑作为入参传递. 但是上面 4 步, 如何操作呢?

private interface DivideHandler { default int getPeriod() { return 10; } default <T> void divideBatchHandler(List<T> dataList, Consumer<List<T>> consumer) { Optional.ofNullable(dataList).ifPresent(list -> IntStream.range(0, list.size()) .mapToObj(i -> new AbstractMap.SimpleImmutableEntry<>(i, list.get(i))) .collect(Collectors.groupingBy( e -> e.getKey() / getPeriod(), Collectors.mapping(Map.Entry::getValue, Collectors.toList()))) .values() .parallelStream() .forEach(consumer) ); } } class PrintDivideHandler implements DivideHandler { @Override public int getPeriod() { return 2; } private void batchPrint(List<String> dataList) { divideBatchHandler(dataList, System.out::println); } }

parallelStream 会调用 Fork/Join 框架进行并行处理. 如果需要在程序中显性的指定线程数, 可以通过 new ForkJoinPool(threadNum).submit(()->{}) 进行封装

6. chain return notNull & fail-fast

场景如下:
我们需要获取 name 值, 但是有多个方式可以获取, 也可能获取不到. 所以我们结合业务要求与获取效率进行排序, 并且针对结果进行判断, 非空则返回.

听起来又是一连串的 if.

针对这种 case 如何通过 Stream 优化呢? 依然是通过惰性求值以及函数入参.

我们先预设一个 Stream 逻辑, 其中顺序排列多个获取 name 值的函数, 然后统一执行.

private Optional<String> getOptionalName() { Stream<Supplier<String>> nameStream = Stream.of( this::getName1, this::getName2, this::getName3, this::getName4 ); return nameStream .map(Supplier::get) .filter(Objects::nonNull) .findAny(); }

如何复杂度再提升, 需要获取多个值构造一个 bean, 其中任意一个值为空, 那么就终止 stream, 不去进行下一个值的获取操作.

此时可以利用 flatMap 进行连接

@Deprecated private Optional<User> fillBuild() { Stream<String> nameStreamWrapper = buildStream(Arrays.asList(this::getName1, this::getName2, this::getName3, this::getName4)); Stream<Integer> ageStreamWrapper = buildStream(Arrays.asList(this::getAge1, this::getAge2)); BuildUser buildUser = (name, age) -> Stream.of(new User(name, age)); return ageStreamWrapper.flatMap(age -> nameStreamWrapper.flatMap(name -> buildUser.build(name, age) ) ).findAny(); } private <T> Stream<T> buildStream(List<Supplier<T>> suppliers) { return suppliers.stream() .map(Supplier::get) .filter(Objects::nonNull); } @FunctionalInterface private interface BuildUser { Stream<User> build(String name, Integer age); }

加上了 @Deprecated 标记是因为这么写看起来比较丑, 降低了语义和阅读性.

总结

  1. 函数式编程带来更强的语义, 但是绝不仅是语法糖.
  2. 尝试用函数式思维重构复杂逻辑, 会有意外收获

所有代码及完整测试用例在 https://github.com/wangyuheng/java8-lambda-sample

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3198 引用 • 8215 回帖 • 1 关注
  • stream
    10 引用 • 13 回帖
  • Lambda
    24 引用 • 19 回帖
  • 函数式
    9 引用 • 12 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • zonghua

    给个类似 SQL 分组统计的操作

    1 回复
  • crick77 1 赞同 via macOS
    作者

    有的

    @Test public void map_item_count_by_uid() { Map<String, Long> uidMapCount = list.stream() .collect(Collectors.groupingBy(Item::getUid, Collectors.mapping(Function.identity(), Collectors.counting()))); uidMapCount.forEach((k, v) -> System.out.println(String.format("key = %s : value = %s", k, v))); }
    2 回复
  • zwxbest

    如果有一个商品集合,需要分别筛选出价格在 3 个区间内的商品,不用 for 或者 Foreach 有好的办法吗?

    1 回复
  • crick77 via macOS
    作者

    like this?

    String less150 = "less 150"; String greater150 = "greater 150"; Map<String, List<Item>> map = list.stream().collect(Collectors.groupingBy(it->{ if (it.getPrice()>100) { return less150; } else { return greater150; } }, Collectors.toList()));
  • zwxbest

    👍 对,学习了。

  • tmedivh

    不错,不错

  • zonghua

    😂 太花哨了,我一般都不知道怎么写,只知道用字段连接做 key 然后放到 map 里循环统计

  • zonghua

    下次我把代码都重构一边

  • 88250 1

    稍微补充一下,并行流的 ForkJoinPool 用完需要关闭,可以用关闭 + 超时进行控制:

    pool.shutdown(); pool.awaitTermination(10, TimeUnit.SECONDS);
    1 回复
  • crick77 via macOS
    作者

    感谢补充,我会在 sample 代码中增加

请输入回帖内容 ...