重要要点
- Eclipse Collections是一个高性能Java Collections框架,针对Java 8及更高版本进行了重新设计,为Java Collections框架添加了丰富的功能。
- 它在高盛内部开发了10年,然后于2012年作为GS Collections开源。 在2015年,它被迁移到Eclipse Foundation。
- 它使用原始数据结构,其性能要比传统原始集合好得多。
- 在Eclipse Collections 8.0发行版之前,EC与Java版本5-7兼容。8.0及更高版本要求Java 8+,并使用Optional处理潜在的空值。
- 最新版本也已更新为支持Java 9中的模块。
30第二篇简介–什么是Eclipse Collections?
Eclipse Collections是Java Collections框架的替代品。 它具有JDK兼容的List,Set和Map实现,具有丰富的API,以及JDK中找不到的其他类型,例如Bags,Multimaps和BiMaps。 Eclipse Collections还具有完整的原始容器。 它在高盛内部开发了10年,然后于2012年作为GS Collections开源。 在2015年,它被迁移到Eclipse基础,此后,该框架的所有活动开发都以Eclipse Collections的名称和存储库完成。 如果您需要一些入门书籍,请阅读Donald Raab的InfoQ文章,“ GS范例集”第一部分和第二部分 。
领域
在深入探讨任何细节或代码示例之前,让我们逐步介绍本文中用于代码段的领域。 如下图所示:
(点击图片放大)
我们有一个人员列表(类型Person),每个人都可以有一个Pets列表,并且每个宠物都具有某个PetType枚举。
Java 8版本8
在Eclipse Collections发行版之前,EC与Java版本5-7兼容。在使用丰富的API时,您也可以使用Java 8并利用lambda和方法引用,实际上,它工作得很好。
但这就是您真正拥有的全部。 Eclipse Collections与Java 8兼容,但是没有使用或包含它。 现在,从Eclipse Collections开始,我们已经做出了与Java 8+兼容的设计决策,以便开始利用我们自己代码库中的一些很酷的Java 8功能。
可选的
可选的是Java 8最受欢迎的新功能之一。在Javadoc中,“可能包含也可能不包含非null值的容器对象。如果存在值,则isPresent()将返回true,get()将返回true。返回值”。 基本上,Optional通过强制我们处理可能为空的项目来帮助保护我们免受NullPointerExceptions的侵害。 那么,我们可以在Eclipse Collections中使用它吗? RichIterable.detectWith()非常适合。 detectWith接受Predicate参数,并返回满足该条件的集合中的第一个元素。 如果找不到任何元素,则返回null。 因此,在8.0中,我们引入了detectWithOptional()。 它返回一个Optional,而不是返回element或null,然后留给用户处理。 请参见下面的代码示例(摘自我们的kata教程材料):
Person person = this.people.detectWith(Person::named, "Mary Smith");
//null pointer exception
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());
在这里,我们想找到玛丽·史密斯。 当我们调用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之前,我们总是可以使用空检查,如上所示。 但是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,我将调用orElseGet()创建一个新的Person实例。 测试通过了,我们避免了任何例外!
收藏家
如果在代码中使用流,则很可能以前使用过收集器。 收集器是一种实现可变还原操作的方法。 例如,Collectors.toList()允许我们将流中的项目累积到列表中。 JDK有几个“内置”收集器,可以在Collectors类中找到。 参见下面的一些Java 8(无Eclipse Collections)示例。
List<String> names = this.people.stream()
.map(Person::getFirstName)
.collect(Collectors.toList());
// Output:
// [Bob, Ted, Jake]
int total = this.people.stream().collect(
Collectors.summingInt(Person::getNumberOfPets));
// Output:
// 4
由于我们现在可以使用Eclipse Collections来利用流,因此我们也应该拥有自己的内置Collector-Collectors2。 这些收集器中有许多是针对Eclipse Collections特定的数据结构的。 不能从JDK中获得的功能,例如toBag(),toImmutableSet()等。
(点击图片放大)
这张图刮擦了Collectors2 API的表面。 顶部的框是您可以将Collectors2的结果放入的所有不同的数据结构,下面的项目符号是一些可用的不同API。 您可以看到我们对JDK和Eclipse Collections数据结构以及原始集合都具有类型支持。 您甚至可以通过Collectors2使用我们熟悉的collect(),select(),reject()等API。
还可能与收集器和收集器2互操作; 两者并不互相排斥。 参见下面的示例,我们使用的是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(","))));
// Output: {1=Ted, Jake, 2=Bob}
这两个代码片段产生相同的输出,但是要注意细微的差别:Eclipse Collections提供了makeString()功能,该功能创建一个用逗号分隔的以String表示的元素集合。 为了仅使用Java 8来执行此操作,它需要通过调用Collectors.mapping(),将每个对象转换为其toString值并以逗号将其连接来进行更多工作。
默认方法
对于像Eclipse Collections这样的框架,默认方法是JDK的一个很棒的新功能。 我们可以在一些最高级的接口上实现新的API,而不必更改下面的许多实现。 reduceInPlace()是我们添加到RichIterable中的新方法之一–它有什么作用?
/**
* This method produces the equivalent result as
* {@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等效于在流上使用Collector。 但是为什么我们需要将其添加到Eclipse Collections? 原因很有趣。 一旦进入Eclipse Collections提供的Immutable或Lazy API,我们将无法再利用流式API。 那时,我们无法获得使用收集器所具有的功能,因为stream()不再对我们可用,后续的API调用也不再可用。 这就是reduceInPlace发挥作用的地方。
如下所示,一旦在集合上调用.toImmutable()或.asLazy(),就无法再调用.stream()。 因此,如果我们想利用收集器,我们现在可以使用.reduceInPlace()并获得相同的结果。
原始集合
自GS Collections 3.0以来,我们受益于原始集合。 Eclipse集合为所有基本类型添加了内存优化的集合,具有与对象类型相似的接口以及基本类型之间的对称性。
(点击图片放大)
如上所示,使用原始集合有很多好处。 可以节省大量内存,因为您可以避免将非原始类型装箱。 从Java 8开始,我们有三种原始类型(int,long和double),它们使用专门的原始流和lambda表达式。 在Eclipse Collections中,如果您想要相同的延迟评估,我们将在所有八个原始类型上直接提供该API。 让我们看一些代码示例:
流-像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()); //reuse!
上面,我们正在创建一个1、2和3的IntStream,并尝试在其上调用min()和max()。 Java 8流就像一个迭代器,因为您不能重复使用它们。 Eclipse集合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());
// Output: [3=1, 4=1]
MutableIntBag counts2 = this.people.asLazy()
.flatCollect(Person::getPets)
.collectInt(Pet::getAge)
.toBag()
.selectByOccurrences(IntPredicates.equal(1));
// Output: [3, 4]
在这里,我们要过滤只出现一次的宠物年龄。 由于Java 8没有Bag数据结构(该数据结构将项目映射到其计数),因此我们必须通过计数将集合归类到映射中。 请注意,一旦在宠物上调用了collectInt(),我们现在就可以移至原始集合和API。 当我们调用.toBag()时,我们有一个专门的原始IntBag。 selectByOccurrences()是Bag特定的API,它使我们可以根据Bag出现的次数过滤出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即可继续。 不幸的是,我们不能再使用这些字段来优化我们的迭代模式,但是我们现在与Java 9兼容,因为此修复解决了我们的反射问题。
如果您尚未准备好迁移当前代码,则有一些解决方法可以进行反射。 您可以通过添加命令行参数来避免抛出这些异常,但是作为框架,我们不想给用户带来负担。 主动解决了所有反射问题,因此您可以开始使用Java 9进行编码!
结论
Eclipse Collections随着Java格局的不断变化而不断发展壮大。 如果您尚未这样做,请尝试使用Java 8代码进行尝试,并查看上面描述的新功能! 如果您是该框架的新手,并且需要一个开始的地方,那么这里有一些适合您的资源:
更多信息:
您还可以在这里观看Eclipse集合的Java演变的完整视频。
祝您编码愉快!