Java 新特性,使用 Lambdas 表达式作为 Predicates

来源:开源中国社区 作者:oschina
  

以前我给过一个例子,查询地图的参数变成了 SOLR 的搜索字符串。在 Java 8 之前的代码使用传统的for循环条件和使用StringBuilder 逐步构建一个字符串。Java 8 代码使用 map 实体,映射(转换)每一个实体变成字符串形式"key:value" ,并最终使用了 Collector 加入这些查询片段。这是一个常见的函数式代码,一个 for 循环将一个对象集合转换成不同的对象集合,过滤也是可选的,并选择性地减少单个元素的集合。在函数风格上,map,filter,reduce 等等都是一些通用的模式。你可以总是用条件过滤代替一个 for 循环,减少 Java 8 的 map, filter 和 reduce (collect) 等操作。

而除了 stream API 之外, Java 8 也引入了一些新的 API 方法,它们可以让某些东西简单许多,很不错。例如,假设我们要有下面这样一个方法来移除一个给定的键值的集合中所有的映射项。在示例代码中,dataCache 是一个 ConcurrentMap,而deleteKeys 是我们将要从缓存中移除的键值的集合。下面是我遇到过的采用原始方法实现的代码:

1
2
3
4
5
6
7
8
9
public void deleteFromCache(Set<String> deleteKeys) {
    Iterator<Map.Entry<String, Object>> iterator = dataCache.entrySet().iterator();
    while (iterator.hasNext()) {
        Map.Entry<String, Object> entry = iterator.next();
        if (deleteKeys.contains(entry.getKey())) {
            iterator.remove();
        }
    }
}

现在,你可能会说有更好的方法来做这件事情。比方说对要删除的键值进行迭代,然后使用 Map#remove(Object key) 方法将每一个映射移除。例如:

1
2
3
4
5
public void deleteFromCache(Set<String> deleteKeys) {
    for (String deleteKey : deleteKeys) {
        dataCache.remove(deleteKey);
    }
}

尽管功能上是对等的,但代码中使用for 循环看起来似乎比在这种情形下使用Iterator 更加清晰。那我们可以做得更好吗? Java 8 引入了 removeIf  方法作为一个默认方法,没有把它放到 Map 中,而是放到了 Collection 接口类中。这一新方法 "将集合中所有满足给定谓语条件的元素移除", 该描述引用自 Javadocs 文档。这个方法会接收一个参数,也就是一个Predicate, 它是在Java8中引入的一个函数式的接口, 而其也因此能被用在 lambda 表达式中。让我们首先来将其实现_——一个普通的老的匿名内部类,甚至在 Java 8 中,你也可以这样做。代码如下:

1
2
3
4
5
6
7
8
public void deleteFromCache(Set<String> deleteKeys) {
    dataCache.entrySet().removeIf(new Predicate<Map.Entry<String, Object>>() {
        @Override
        public boolean test(Map.Entry<String, Object> entry) {
            return deleteKeys.contains(entry.getKey());
        }
    });
}

如你所见,我们首先通过 entrySet 方法取得了 map 的 entry 集合,然后在上面调用 removeIf,提供了一个Predicate 来检测 deleteKeys 的集合中是否包含这个 entry 的键值。如果检测返回的是 true,entry 就会被移除。因为 Predicate 被加上了@FunctionalInterface注解, 因此,根据 Javadoc 的说明, 它也可以被当做是一个  lambda 表达式,一个方法引用,或者是一个构造器引用。因此,首先让我们先将这个匿名内部类转换成一个 lambda 表达式:

1
2
3
4
public void deleteFromCache(Set<String> deleteKeys) {
    dataCache.entrySet().removeIf((Map.Entry<String, Object> entry) ->
        deleteKeys.contains(entry.getKey()));
}

在上述代码中,我们用一个 lambda 表达式替换了匿名类,它会接收一个Map.Entry 作为参数。不过 Java 8 可以自己对 lambda 表达式的参数类型进行推断,所以我们可以将明确(且有点啰嗦)的类型声明去掉了, 剩下的就是下面这样更加干净的代码:

1
2
3
public void deleteFromCache(Set<String> deleteKeys) {
    dataCache.entrySet().removeIf(entry -> deleteKeys.contains(entry.getKey()));
}

这段代码比原来使用 Iterator的那段看起来好了很多。但如果是把它同使用了一个简单的for 循环来遍历键值,然后调用remove 来移除每一个元素的第二段代码做比较会如何呢?代码的行与行之间真的是没啥不同,因此假定它们在功能上是对等的,那么也许区别仅仅只是风格外表上面的。看上起比较明确的 for 循环是一种传统的命令式的风格,而removeIf 则有一种功能上更加强大的意味。如果你查看 Collection 接口中 removeIf 的实际实现,就会发现它实际就是使用的Iterator,就跟本文的第一个示例一样。

所以实际上在功能方面是没有区别的。不过 removeIf 在理论上可以针对特定类型的集合执行并行操作来进行实现, 而也许只会对特定大小的集合才能显示其并行操作具备优势。不过这里的示例实际上更多的是关于关注点分离的,例如将决定一个元素是否应该被删除的业务逻辑中同集合的遍历逻辑分离。

例如,在需要将一个集合中不同位置的元素删除的时候,很可能会在不同的地方写出类似的遍历循环,其中还掺杂着删除逻辑。但是removeIf 方法需要的只是在不同的地方写删除逻辑,而且这些逻辑才是真正的业务逻辑。而且,如果将来Java的集合遍历逻辑有某种改进,比如对大集合进行并行运算时,那么所有使用这个方法的地方将会自动获得这种好处,如果使用显示的迭代器或循环的则没办法实现。

在诸如此类的情况下,我认为责任分离是函数式编程优于命令式编程的一个重要原因。责任分离会产生更好、更清晰的代码和更方便的代码精确重用,因为责任都是分别实现的,同时测试也是分离的,也就意味着不仅有更清晰的生产代码,也有更清晰的测试代码。所有这些都带来更易维护的代码,也就意味着新特性和对已有代码的改进将会完成的更快,对已有代码的破坏也会更低。直到本系列下一篇关于Java 8特性和函数式编程的文章之前,请快乐编程!

本文转自:开源中国社区 [http://www.oschina.net]
本文标题:Java 新特性,使用 Lambdas 表达式作为 Predicates
本文地址:
https://www.oschina.net/translate/towards-more-functional-java-using-lambdas-as-pred
参与翻译:
leoxu, 无若, debugging, xufuji456

英文原文:Towards More Functional Java Using Lambdas as Predicates


时间:2016-09-26 08:32 来源:开源中国社区 作者:oschina 原文链接

好文,顶一下
(0)
0%
文章真差,踩一下
(0)
0%
------分隔线----------------------------


把开源带在你的身边-精美linux小纪念品
无觅相关文章插件,快速提升流量