BT

你的观点很重要! 快来参与InfoQ调研吧!

GS Collections实例教程(第一部分)

| 作者 Donald Raab 关注 0 他的粉丝 发布于 2014年12月18日. 估计阅读时间: 41 分钟 | ArchSummit社交架构图谱:Facebook、Snapchat、Tumblr等背后的核心技术

我是一名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<Customer>不同,我现在得到了一个LazyIterable<Customer>类型的变量。这与下面一段使用Java 8 Streams API的代码基本等价。

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<Customer>类型的集合。如果需要测试这一集合的大小,我需要将其按上面程序里的Stream对象转换成一个列表,或者使用Java 8中的Stream.count()方法。

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<Customer>,首先,惰性求值:

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<Customer>,下面一段代码显示了返回类型会如何变化。

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<Customer>类型的返回值

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)关注我们,并与我们的编辑和其他读者朋友交流。

评价本文

专业度
风格

您好,朋友!

您需要 注册一个InfoQ账号 或者 才能进行评论。在您完成注册后还需要进行一些设置。

获得来自InfoQ的更多体验。

告诉我们您的想法

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

GS Collections Source by deanna lin

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

允许的HTML标签: a,b,br,blockquote,i,li,pre,u,ul,p

当有人回复此评论时请E-mail通知我

1 讨论

登陆InfoQ,与你最关心的话题互动。


找回密码....

Follow

关注你最喜爱的话题和作者

快速浏览网站内你所感兴趣话题的精选内容。

Like

内容自由定制

选择想要阅读的主题和喜爱的作者定制自己的新闻源。

Notifications

获取更新

设置通知机制以获取内容更新对您而言是否重要

BT