我是一名 Java 软件工程师,也是 Goldman Sachs(高盛)的 Tech Fellow 和董事总经理。我是 GS Collections 框架的创作者。高盛于 2012 年 1 月将其开源。之前,我是一名 Smalltalk 软件工程师。
当我开始使用 Java 之后,Smalltalk 的两个特点让我念念不忘:
- Smalltalk 的 Lambda 表达式
- 丰富的 Smalltalk Collections 框架带来的各种实用功能
我想在 Java 中同时实现这两点并且保持与现有 Java Collections 接口的兼容性。大约在 2004 年左右,我意识到没有人会在 Java 里实现我想要的一切,同时我也知道在我职业生涯接下来的十到十五年的时间里,我应该都会用 Java 做开发。于是我决定开始自己开发我想要的东西。
时间快进十年。现在我几乎有了我想要在 Java 里实现的一切。随着 Java 8 对 lambda 表达式的支持,我可以将 GS Collections 与 lambda 表达式和方法引用等功能结合使用。GS Collections 可以说是目前拥有最丰富功能特性的 Java Collections 框架。
下面是一个 GS Collections, Java 8, Trove 和 Scala 的功能比较。这些可能并不包含你希望一个 Collections 框架所具有的全部功能,但是它们是我和其他高盛软件工程师 10 多年来开发工作所需要的功能。
我去年在 jClarity 的一篇采访中描述了一些令 GS Collections 引人注意的功能组合。你可以在这里阅读原文。
有人会问,既然 Java 8 已经推出并且包含了 Streams API,你为什么还想要使用 GS Collections 呢?原因在于虽然 Streams API 是在 Java Collections 基础上的一个巨大进步,但是它并不拥有你所想要的所有功能。
正如上表所示,GS Collections 有 multimaps, bags, immutable containers 和 primitive containers。GS Collections 有替代 HashSet 与 HashMap 的优化类型,并且在这些优化类型的基础上开发了 Bags 和 Multimaps。GS Collections 迭代模式是定义在集合接口之上,所以软件工程师们不用通过调用 stream() 来“进入”API 然后再通过调用 collect() 来“退出 ”API。这一功能在很多情况下可以使得代码看起来更加简洁。最后,GS Collections 后向兼容一直到 Java 5。对于函数库开发者来说,这一特性尤其重要,因为他们需要在新的 Java 版本推出之后仍然保持对旧 Java 版本的良好支持。
下面我将会给出一系列例子来阐述如何使用上面提到的这些功能和特性。这些例子是 GS Collections Kata 中一些练习的变形。GS Collections Kata 是高盛内部用来培训软件工程师如何使用 GS Collections 的一个教程,我们也已经将其开源于 Github:链接。
示例 1:过滤一个集合
你通常最想要 GS Collectons 做的一件事情也许就是去过滤一个集合。GS Collections 可以使用几种不同的方式来达到这一目的。
在 GS Collections Kata 中,我们通常会给出一个客户的列表。在其中一个练习里,我们想要从这个列表中选出住在伦敦的客户。下面一段代码显示了我如何使用“select”这一迭代模式来实现这一功能 。
import com.gs.collections.api.list.MutableList; import com.gs.collections.impl.test.Verify; @Test public void getLondonCustomers() { MutableList<Customer> customers = this.company.getCustomers(); MutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Verify.assertSize("Should be 2 London customers", 2, londonCustomers); }
MutableList 的 select 方法的返回值是 MutableList。这一方法采取及早求值策略,也就是说包括从源列表中选择匹配标准的元素以及将其加入目标列表的所有计算会在方法调用结束时全部完成。“select”这一名字传承于 Smalltalk。Smalltalk 有一系列基本的集合协议,比如 select(又称为 filter),reject(又称为 filterNot),collect(又称为 map 或 transform),detect(又称为 findOne),detectNone,injectInto(又称为 foldLeft),anySatisfy 和 allSatisfy。
如果我想要用惰性求值达到同样的效果,我可以写下面一段代码:
MutableList<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
在这个例子中,我仅仅加入了一个对 asLazy() 方法的调用。其他的代码基本上与之前相同。select 方法的返回值类型因为 asLazy() 方法的调用而有所变化。与之前的 MutableList
List<Customer> customers = this.company.getCustomers(); Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); List<Customer> londonCustomers = stream.collect(Collectors.toList()); Verify.assertSize(2, londonCustomers);
这里,stream() 方法以及对 filter() 方法的调用返回了一个 Stream
List<Customer> customers = this.company.getCustomers(); Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); Assert.assertEquals(2, stream.count());
GS Collections 的 MutableList 接口和 LazyIterable 接口都有一个共同的父接口叫做 RichIterable。事实上,我可以使用 RichIterable 来写这些程序。下面两个例子只使用了 RichIterable
RichIterable<Customer> customers = this.company.getCustomers(); RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
其次,及早求值:
RichIterable<Customer> customers = this.company.getCustomers(); RichIterable<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
正如这些例子中所展示的,RichIterable 可以替代 LazyIterable 和 MutableList 来使用,因为 RichIterable 是它们共同的根接口。
客户的列表也有可能是不可变的,如果我有一个 ImmutableList
ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); ImmutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
正如其他的 RichIterables,我们可以使用惰性求值的方法来遍历 ImmutableList
ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
ListIterable 是 MutableList 和 ImmutableList 共同的父接口。它可以用于取代其中任何一个类型来表示更加通用的类型。RichIterable 是 ListIterable 的父类。所以这一段代码可以重写为以下更通用的形式:
ListIterable<Customer> customers = this.company.getCustomers().toImmutable(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
或者甚至更加通用的:
RichIterable<Customer> customers = this.company.getCustomers().toImmutable(); RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
GS Collections 有一套基本的接口层次结构。对于每一种数据结构类型(List, Set, Bag, Map),它有一个可读的接口(ListIterable, SetIterable, Bag, MapIterable),一个可变接口(MutableList, MutableSet, MutableBag, MutableMap),和一个不可变接口(ImmutableList, ImuutableSet, ImmutableBag, ImmutableMap)
(点击图像放大)
图 1. GS Collections 的基本接口层次结构图
下面一段代码展示了如何使用 Set 来替代 List 完成和之前代码一样的功能
MutableSet<Customer> customers = this.company.getCustomers().toSet(); MutableSet<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
这是一段使用惰性求值 Set 的解决方案
MutableSet<Customer> customers = this.company.getCustomers().toSet(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
这段代码使用 Set,并且使用最通用的接口
RichIterable<Customer> customers = this.company.getCustomers().toSet(); RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
接下来,我会阐述一些可以用来转换容器类型的方法。首先,让我们通过惰性过滤将一个 List 转换为一个 Set:
MutableList<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> lazyIterable = customers.asLazy().select(c -> c.livesIn("London")); MutableSet<Customer> londonCustomers = lazyIterable.toSet(); Assert.assertEquals(2, londonCustomers.size());
由于 API 的连贯性,我们可以将这些函数调用链在一起
MutableSet<Customer> londonCustomers = this.company.getCustomers() .asLazy() .select(c -> c.livesIn("London")) .toSet(); Assert.assertEquals(2, londonCustomers.size());
我会留给读者去决定这样的代码是否影响可读性。于我个人而言,如果能够增强可读性,我倾向于将这一系列的函数调用分开,并且引入一些中间的变量。这可能会带来更多的代码量,但是这样减轻了理解代码的难度。对于不常阅读这份代码的开发者而言,这无疑是有益的。
我同样可以在 select 方法之内完成 List 到 Set 的转换,因为 select 方法有另外一个接受一个 Predicate 作为第一个参数以及一个结果集合类型作为第二个参数的重载形式。
MutableSet<Customer> londonCustomers = this.company.getCustomers() .select(c -> c.livesIn("London"), UnifiedSet.newSet()); Assert.assertEquals(2, londonCustomers.size());
我可以使用这一方法来返回任何我需要的集合类型。下面一个例子我得到了一个 MutableBag
MutableBag<Customer> londonCustomers = this.company.getCustomers() .select(c -> c.livesIn("London"), HashBag.newBag()); Assert.assertEquals(2, londonCustomers.size());
下面一个例子中,我得到了一个 CopyOnWriteArrayList 类型的返回值,而这是 JDK 中的一种数据类型。总而言之,只要某一数据类型实现了 java.utils.Collection 接口,以上方法就可以返回这一类型的变量。
CopyOnWriteArrayList<Customer> londonCustomers = this.company.getCustomers() .select(c -> c.livesIn("London"), new CopyOnWriteArrayList<>()); Assert.assertEquals(2, londonCustomers.size());
之前的例子里,我们一直在使用 lambda 表达式。实际上 select 方法接受一个 Predicate,它是一个 GS Collections 的函数式接口,其定义如下:
public interface Predicate<T> extends Serializable { boolean accept(T each); }
我之前使用的 lambda 表达式都比较简单,我现在把它赋值于一个单独的变量里来让大家可以更清楚的理解它代表了哪一部分的代码。
Predicate<Customer> predicate = c -> c.livesIn("London"); MutableList<Customer> londonCustomers = this.company.getCustomers().select(predicate); Assert.assertEquals(2, londonCustomers.size());
Customer 类中定义了一个简单的方法 livesIn() 如下:
public boolean livesIn(String city) { return city.equals(this.city); }
如果这里我们可以通过方法引用 而不是 lambda 表达式,比如引用 livesIn 方法,将会非常好。
Predicate<Customer> predicate = Customer::livesIn;
但是下面这段代码会导致编译器报错:
Error:(65, 37) java: incompatible types: invalid method reference incompatible types: com.gs.collections.kata.Customer cannot be converted to java.lang.String
这是因为方法引用需要两个参数,一个 Customer 对象和一个表示城市的字符串。这里就用到了一个 Predicate 的变化 Predicate2。
Predicate2<Customer, String> predicate = Customer::livesIn;
注意到 Predicate2 接受两个一般变量类型 Customer 和 String。有另外一种形式的 select 叫做 selectWith 可以配合 Predicate2 使用
Predicate2<Customer, String> predicate = Customer::livesIn; MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(predicate, "London"); Assert.assertEquals(2, londonCustomers.size());
使用内联函数引用可以使这一段代码更加简洁
MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(Customer::livesIn, "London"); Assert.assertEquals(2, londonCustomers.size());
字符串 “London” 是作为第二个参数传入 Predicate2 中所定义的方法,第一个参数则来自 Customer 列表中的 Customer 对象。
与 select 类似,selectWith 是定义在 RichIterable 类型上的。所以我之前展示的所有可以使用 select 方法的例子都可以使用 selectWith。这其中包含了对各类可变或者不可变接口,不同的协变类型以及惰性迭代的支持。还有另外一个形势的 selectWith 接受三个参数,与两个参数的 select 类似,selectWith 的第三个参数用来接受一个目标集合类型。
下面一段代码使用 selectWith 将 List 转换成 Set。
MutableSet<Customer> londonCustomers = this.company.getCustomers() .selectWith(Customer::livesIn, "London", UnifiedSet.newSet()); Assert.assertEquals(2, londonCustomers.size());
同样的,这段代码也可以使用惰性求值
MutableSet<Customer> londonCustomers = this.company.getCustomers() .asLazy() .selectWith(Customer::livesIn, "London") .toSet(); Assert.assertEquals(2, londonCustomers.size());
我想展示的最后一件事情就是 select 以及 selectWith 方法可以用在任意一类继承 java.lang.Iterable 的集合上。这包含了所有的 JDK 类型以及任何第三方集合库。GS Collections 中实现的第一个类是一个名为 Iterate 的工具类。下面一段代码展示了如何在一个 Iterable 上用 Iterate 来调用 select。
Iterable<Customer> customers = this.company.getCustomers(); Collection<Customer> londonCustomers = Iterate.select(customers, c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
selectWith 也可以通过同样的方法来使用
Iterable<Customer> customers = this.company.getCustomers(); Collection<Customer> londonCustomers = Iterate.selectWith(customers, Custom-er::livesIn, "London"); Assert.assertEquals(2, londonCustomers.size());
接受目标集合类型作为参数的变化形式也同时存在。Iterate 支持所有的基本迭代模式。另一个工具类 LazyIterate 涵盖了惰性迭代的特性,并且它也可以在任何扩展了 java.lang.Iterable 的容器上使用。例如:
Iterable<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = LazyIterate.select(customers, c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
另外一种更加面向对象的方式是使用一个 adapter 类,下面一个例子展示了如何对于 java.util.list 使用 ListAdapter
List<Customer> customers = this.company.getCustomers(); MutableList<Customer> londonCustomers = ListAdapter.adapt(customers).select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
这同样也可以实现为惰性求值的形式
List<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = ListAdapter.adapt(customers) .asLazy() .select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
selectWith 的惰性求值也同样适用。
List<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = ListAdapter.adapt(customers) .asLazy() .selectWith(Customer::livesIn, "London"); Assert.assertEquals(2, londonCustomers.size());
SetAdapter 可以类似的适用于任何 java.util.Set 的实现。
如果你手中的问题可以从数据层面的并行中获益,那么你可以使用下面两种方法来并行化你的解决方案。首先我们介绍如何使用 ParallelIterate 类通过及早并行的方式去解决此类问题
Iterable<Customer> customers = this.company.getCustomers(); Collection<Customer> londonCustomers = ParallelIterate.select(customers, c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
ParallelIterate 类会接受任何一个 Iterable 类型的变量作为参数,并且返回值永远是 java.util.Collection 类型。从 2005 年开始,ParallelIterate 就已经在 GS Collections 中存在。及早并行也曾经是 GS Collections 支持的唯一一种并行方式,直到 5.0 版本,我们为 RichIterable 加入了惰性并行的 API。我们暂时没有给 RichIterable 加入及早并行的 API,因为我们认为惰性并行作为一种缺省情况更为合适。我们有可能会在未来加入及早并行的 API,这取决于用户的反馈情况。
如果我想使用惰性并行 API,我可以写如下的代码:
FastList<Customer> customers = this.company.getCustomers(); ParallelIterable<Customer> londonCustomers = customers.asParallel(Executors.newFixedThreadPool(2), 100) .select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.toList().size());
现在,asParallel 方法仅在 GS Collections 的一些实体容器中存在。这一 API 还没有在 MutableList, ListIterable 或者 RichIterable 这样的接口上得到支持。asParallel() 方法接受两个参数,一个 Executor Service 和一个批量大小 。今后,我们会加入一个自动计算批量大小的 asParallel() 方法。
下面一个例子中,我选择使用一个比较特殊的类型
FastList<Customer> customers = this.company.getCustomers(); ParallelListIterable<Customer> londonCustomers = customers.asParallel(Executors.newFixedThreadPool(2), 100) .select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.toList().size());
ParallelIterable 有一系列的接口,包括 ParallelListIterable, ParallelSetIterable 和 ParallelBagIterable。
上面我展示了一些不同的使用 select() 和 selectWith() 的方法来在 GS Collections 中过滤一个集合。我也介绍了一些在 RichIterable 上使用及早求值,惰性求值,串行和并行遍历方式的组合。
在将于下个月发表的这篇教程的第二部分中,我将会涉及到一些关于 collect, groupBy, flatCollect 以及一些基本类型容器和它们丰富的 API。在第二部分的例子中,也许我不会深入到这么多细节和方法,但是需要注意的是这些方法也是存在的。
关于作者
Donald Raab在高盛的信息技术部领导 JVM Architecture 小组。Raab 是 JSR 335 专家组 (Java 编程语言的 Lambda Expressions) 的成员, 并且是高盛在 JCP (Java Community Process) 执行委员会的代表之一。他于 2001 年作为技术架构师加入高盛信息技术部的会计 & 风险分析组。他在 2007 年被授予高盛的 Technology Fellow 头衔,并在 2013 年成为董事总经理。
译者周韬,2014 年 7 月作为新工程师加入高盛信息技术部门风险控制技术组,对于 Java 以及 GS Collections 在实际工作中的应用有着浓厚的兴趣。
www.gs.com/engineering 有关于 GS Collections 和高盛信息技术部的更多信息。
披露
本文章反映的信息仅为高盛信息技术部门所有,并非高盛其他部门所持信息。其不得被依赖或被视为投资建议。除非明确标识,其表达观点并非一定为高盛所持观点。高盛公司不担保、不保证本文章的精确、完整或效用。接收者不应依赖本文章,除非在自担风险的范围内。在未刊载声明的情形下,本文章不得被转发、披露。
感谢崔康对本文的策划和审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。
评论