知行

总结之后必有收获 开始使用

优雅你的代码 --Java8 Lambda 场景示例

阅读前提

  1. 了解 java8 的一些新特性, 如: Optional、 Stream

背景

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

scene & solution

1. Optional

方法 描述
of 把指定的值封装为 Optional 对象,如果指定的值为 null,则抛出 NullPointerException
empty 创建一个空的 Optional 对象
ofNullable 把指定的值封装为 Optional 对象,如果指定的值为 null,则创建一个空的 Optional 对象
get 如果创建的 Optional 中有值存在,则返回此值,否则抛出 NoSuchElementException
orElse 如果创建的 Optional 中有值存在,则返回此值(默认方法依旧执行),否则返回一个默认值
orElseGet 如果创建的 Optional 中有值存在,则返回此值,否则返回一个由 Supplier 接口生成的值
orElseThrow 如果创建的 Optional 中有值存在,则返回此值,否则抛出一个由指定的 Supplier 接口生成的异常
filter 如果创建的 Optional 中的值满足 filter 中的条件,则返回包含该值的 Optional 对象,否则返回一个空的 Optional 对象
map 如果创建的 Optional 中的值存在,对该值执行提供的 Function 函数调用
flagMap 如果创建的 Optional 中的值存在,就对该值执行提供的 Function 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象
isPresent 如果创建的 Optional 中的值存在,返回 true,否则返回 false
ifPresent 如果创建的 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

参考
作者:crick77
链接:https://hacpai.com/article/1545321970124

评论
留下你的脚步