@lsmn
2017-10-16T09:10:12.000000Z
字数 7699
阅读 2120
Java
Eclipse
Collections
对于Java的每个后继版本,框架都必须调整转化,以便保持同步。本文将介绍高性能Java集合框架Eclipse Collections中部分Java 8的新特性,并展望下我们针对Java 9所做的一些新东西。本文将大致介绍下这些新特性及变化。
本文要点
- 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和性能优化。她在公司内部和外部教授有关该框架的课程。 |