Eclipse Collections 随 Java 版本的演变

阅读数:1806 2017 年 10 月 24 日

本文要点

  • Eclipse Collections 是一个高性能的 Java 集合框架,针对 Java 8 及以上版本进行了重新设计,极大地丰富了这个 Java Collections 框架的功能。
  • 在 2012 年开源之前,该框架在 Goldman Sachs 内部已经开发了 10 年,那时称为 GS Collections。2015 年,它被迁移到 Eclipse 基金会。
  • 它使用原始的数据结构,性能优于传统的原始集合。
  • 在 Eclipse Collections 8.0 版本之前,EC 兼容 Java 5 和 7 之间的版本。8.0 及以上版本需要使用 Java 8 及以上版本,并且使用 Optional 处理潜在的 null 值。
  • 最新版本经过升级已经支持 Java 9 的模块。

30 秒简介——Eclipse Collections 是什么?

Eclipse Collections 是 Java Collections 框架的替代者。它提供 JDK 兼容的 List、Set 和 Map 实现,并且提供了丰富的 API 以及 JDK 中没有的其他类型,如 Bags、Multimaps 和 BiMaps。Eclipse Collections 还充分补充了原始容器。在 2012 年开源之前,该框架在 Goldman Sachs 内部已经开发了 10 年,那时称为 GS Collections。2015 年,它被迁移到 Eclipse 基金会。从那会开始,所有开发都是在 Eclipse Collections 名下完成的。如果你想阅读一些优质的介绍性文章,可以看下 Donald Raab 发表在 InfoQ 上的文章“GS Collections 实例教程”第一部分第二部分

领域

在讨论任何细节或代码示例之前,让我们了解下本文的代码片段来自什么领域,如下图所示:

(点击查看大图)

我们有一个人的列表(类型为 Person),每个人对应一个 Pet 列表,每只宠物都是枚举类型 PetType 中的一种。

面向 Java 8 的 Eclipse Collections 8

在 Eclipse Collections 8 发布之前,EC 兼容的 Java 版本为 5 和 7 之间的版本。开发人员也可以使用 Java 8,既使用框架提供的丰富 API,同时又充分利用 Lambda 表达式和方法引用的优势,而实际效果还不错。

但你能做的也就只有那些。Eclipse Collections 与 Java 8 兼容,但它没有使用或包含 Java 8。现在,从 Eclipse Collections 8 开始,我们已经决定兼容 Java 8 及以上版本,从而可以开始在我们的代码库中利用部分绝妙的 Java 8 新特性。

Optional

Optional 是 Java 8 中最受欢迎的新特性之一。据 Javadoc 介绍,“一个容器对象可能包含也可能不包含非空值。如果值存在,那么 isPresent() 会返回 true,而 get() 会返回那个值”。从根本上讲,Optional 强制开发人员处理潜在的 null 项,帮助他们避免 NullPointerExceptions。那么,我们可以在 Eclipse Collections 的那个地方使用这项特性呢?RichIterable.detectWith()非常适合。detectWith 接收一个 Predicate 参数,返回集合中满足那个条件的第一个元素。如果它没有找到任何元素,则返回 null。因而,在 8.0 版本中,我们引入了 detectWithOptional()。该方法不会返回一个元素或 null,它返回一个 Optional 对象,然后由用户来处理,参见下面的代码(来自我们的 kata 教程资料):

复制代码
Person person = this.people.detectWith(Person::named, "Mary Smith");
// 空指针异常
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());

在这段代码中,我们想查找 Mary Smith。当调用 detectWith 方法时,person 对象被置为 null,因为它没有找到任何满足条件的人。因此,这段代码会抛出 NullPointerException。

复制代码
Person person = this.people.detectWith(Person::named, "Mary Smith");
if (person == null)
{
person = new Person("Mary", "Smith");
}
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());

接下来,在 Java 8 之前,我们可以总是使用上面这样的 null 检查。但是,Java 8 提供了 Optional,那么我们就用它吧!

复制代码
Optional<Person> optional =
this.people.detectWithOptional(Person::named, "Mary Smith");
Person person = optional.orElseGet(() -> new Person("Mary", "Smith"));
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());

在这段代码中,detectWithOptional 没有返回 null,而是返回了一个封装了 Person 的 Optional。现在,由开发人员决定如果处理这种潜在的 null 值。在我的代码中,如果它不是 null,我就调用 orElseGet() 新建一个 Person 实例。测试通过,我们避免了任何异常!

Collectors

如果你的代码中使用了 Streams,那么你之前很可能使用过 Collector。Collector 是一种实现可变归约操作的方法。例如,Collectors.toList() 让开发人员可以将 Stream 中的数据项累加到列表中。JDK 有多个“内置”的 Collector,可以从 Collectors 类里找到。下面是一些 Java 8(不是 Eclipse Collections)的例子:

复制代码
List<String> names = this.people.stream()
.map(Person::getFirstName)
.collect(Collectors.toList());
// 输出:
// [Bob, Ted, Jake]
int total = this.people.stream().collect(
Collectors.summingInt(Person::getNumberOfPets));
// 输出:
// 4

既然现在我们可以在使用 Eclipse Collections 时利用 Streams,我们也应该内建自己的 Collectors——Collectors2。其中许多 Collector 都是针对特定的 Eclipse Collections 数据结构的,有些特性是 JDK 没有直接提供的,如 toBag()、toImmutableSet() 等。

(点击查看大图)

上图简要介绍了 Collectors2 API。上面的方框是所有可以存储 Collectors2 结果的不同数据结构,下面各项是部分可以达到这个目的 API。可以看到,Collectors2 既支持 JDK 和 Eclipse Collections 的类型,也支持原始集合。开发人员甚至可以通过 Collectors2 使用他们熟悉的 collect()、select()、reject() 等 API。

Collectors 和 Collectors2 之间也可以交互;二者并不相互排斥。看下下面这个例子,我们使用了 JDK 8 Collectors,但方便起见,我们接着使用了 EC 8.0 Collectors2:

复制代码
Map<Integer, String> people = this.people
.stream()
.collect(Collectors.groupingBy(
Person::getNumberOfPets,
Collectors2.makeString()));
Map<Integer, String> people2 = this.people
.stream()
.collect(Collectors.groupingBy(
Person::getNumberOfPets,
Collectors.mapping(
Object::toString,
Collectors.joining(","))));
// 输出: {1=Ted, Jake, 2=Bob}

上面两段代码的输出完全一种,但实现上有细微的差别:Eclipse Collections 提供了 makeString() 功能,它创建了一个逗号分隔的元素集合,并表示为一个字符串。使用 Java 8 做到这一点,就需要多做一点工作,调用 Collectors.mapping(),将每个对象转换成 toString 值,然后使用逗号连接在一起。

默认方法

对于像 Eclipse Collections 这样的框架,默认方法是对 JDK 的一个很好的补充。我们可以在部分最上层的接口之上实现新 API,而不必修改许多底层的实现。reduceInPlace() 是我们向 RichIterable 添加的其中一个新方法——他有什么功能?

复制代码
/**
* 该方法生成的结果和下面的代码完全相同
* {@link Stream#collect(Collector)}.
* <p>
* <pre>
* MutableObjectLongMap<Integer> map2 =
* Lists.mutable
.with(1, 2, 3, 4, 5)
.reduceInPlace(
Collectors2.sumByInt(
i -> Integer.valueOf(i % 2), Integer::intValue));
* </pre>
* @since 8.0
*/
default <R, A> R reduceInPlace(Collector<? super T, A, R> collector)
{
A mutableResult = collector.supplier().get();
BiConsumer<A, ? super T> accumulator = collector.accumulator();
this.each(each -> accumulator.accept(mutableResult, each));
return collector.finisher().apply(mutableResult);
}

reduceInPlace 和在 Stream 上使用 Collector 的效果完全一样。但是我们为什么要在 Eclipse Collections 中加入这个方法呢?原因非常有趣;在涉及 Eclipse Collections 提供的 Immutable 或 Lazy API 时,我们就不必再使用 streaming API 了。在这一点上,我们无法使用 Collectors 获得同样的功能,因为我们已经无法使用 stream(),也无法调用后续的 API;这就该 reduceInPlace 发挥作用了。

如下图所示,一旦我们调用了集合的.toImmutable() 或.asLazy() 方法,我们就无法再调用.stream() 了。因此,如果我们想使用 Collectors,那么我们现在可以使用.reduceInPlace() 实现同样的效果。

“原始集合(Primitive Collections)”

从 GS Collections 3.0 开始,我们就受益于原始集合。Eclipse Collections 优化了所有原始类型集合的内存,提供了和 Object 类型类似的接口,并且和原始类型相对应。

(点击查看大图)

从上图可以看出,使用原始集合有若干好处。开发人员可以不用装箱非原始类型,节省大量的内存。从 Java 8 开始,我们有三种原始类型(int、long 和 double),使用专用的原始流和 Lambda 表达式。在 Eclipse Collections 中,如果你希望使用同样的惰性求值,那么我们在八种原始类型上都直接提供了 API。让我们看一下代码示例。

Streams——类似 Iterator

复制代码
IntStream stream = IntStream.of(1, 2, 3);
Assert.assertEquals(1, stream.min().getAsInt());
Assert.assertEquals(3, stream.max().getAsInt());
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.base/java.util.stream.IntPipeline.reduce(IntPipeline.java:474)
at java.base/java.util.stream.IntPipeline.max(IntPipeline.java:437)
LazyIntIterable lazy = IntLists.mutable.with(1, 2, 3).asLazy();
Assert.assertEquals(1, lazy.min());
Assert.assertEquals(3, lazy.max()); // 重用!

在上述代码中,我们创建 IntStream 1、2、3,并试图调用它的 min() 和 max() 的方法。Java 8 的 Streams 和迭代器类似,不允许重用。Eclipse Collections LazyIterables 允许重用。让我们看一个更复杂的例子:

复制代码
List<Map.Entry<Integer, Long>> counts = this.people.stream().flatMap(
person -> person.getPets().stream())
.collect(Collectors.groupingBy(Pet::getAge, Collectors.counting()))
.entrySet()
.stream()
.filter(e -> e.getValue().equals(Long.valueOf(1)))
.collect(Collectors.toList());
// 输出:[3=1, 4=1]
MutableIntBag counts2 = this.people.asLazy()
.flatCollect(Person::getPets)
.collectInt(Pet::getAge)
.toBag()
.selectByOccurrences(IntPredicates.equal(1));
// 输出:[3, 4]

这里,我们想筛选出年龄只出现过一次的宠物。由于 Java 8 没有 Bag 数据类型(将项映射到数量),所以我们必须对集合进行分组操作并把计数结果存储到 map 中。注意,一旦我们在宠物上调用了 collectInt() 方法,我们就转换到了原始集合和 API。当调用.toBag() 方法时,我们会得到一个专用的原始 IntBag。selectByOccurrences() 是 Bag 特有的 API,使开发人员可以根据出现的次数筛选 Bag 里的数据项。

Java 9——下一步呢?

众所周知,Java 9 为 Java 生态系统带来了许多有趣的变化,如新的模块化系统和内部 API 封装。为了保持兼容,Eclipse Collections 也必须做出相应的改变。

在 8.2 版本中,为了项目构建的顺利完成,我们不得不修改所有用了反射的方法。下面举个 ArrayListIterate 的例子:

复制代码
public final class ArrayListIterate
{
private static final Field ELEMENT_DATA_FIELD;
private static final Field SIZE_FIELD;
private static final int MIN_DIRECT_ARRAY_ACCESS_SIZE = 100;
static
{
Field data = null;
Field size = null;
try
{
data = ArrayList.class.getDeclaredField("elementData");
size = ArrayList.class.getDeclaredField("size");
data.setAccessible(true);
size.setAccessible(true);
}
catch (Exception ignored)
{
data = null;
size = null;
}
ELEMENT_DATA_FIELD = data;
SIZE_FIELD = size;
}

在这个例子里,我们一调用 data.setAccessible(true) 就会抛出异常。为了让代码可以继续执行,我们采用了一种变通方案,仅仅将 data 和 size 置为 null。遗憾的是,我们不能再使用这些字段优化我们的迭代模型了,但现在,EC 已经兼容 Java 9 了,这解决了我们的反射问题。

如果现在还没有迁移当前代码的打算,也有变通方案。可以通过增加一个命令行参数来避免抛出这些异常,但作为一个框架,我们不希望把这种负担加在用户身上。所有的反射问题都获得了积极的解决,用户可以开始使用 Java 9 编码了!

小结

Eclipse Collections 会继续随着不断发展变化的 Java 生态而不断地发展演化。如果你还没有这样做过的话,可以在使用 Java 8 的代码中试一下,实际地看一看上述新特性。如果你是 EC 的新用户,还不知道从哪里入手,那么可以参考下面这些资源:

感兴趣的读者可以查看完整的《Eclipse Collections 随 Java 版本的演变》的视频。

快乐编码,享受编码!

关于作者

Kristen O'Leary是 Goldman Sachs 平台组的合伙人。该小组负责公司的许多技术工具和框架。她已经向 Eclipse Collections 贡献了若干容器、API 和性能优化。她在公司内部和外部教授有关该框架的课程。

查看英文原文:The Java Evolution of Eclipse Collections

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论