Eclipse Collections:让 Java Streams 更上一层楼

阅读数:2682 2018 年 6 月 21 日

关键要点

  • Eclipse Collections 是一个高性能的 Java 集合框架,为原生 JDK 集合增加了丰富的功能。
  • Streams 是 JDK 的一个非常受欢迎的功能,但它缺少了一些特性,严重依赖旧版的集合实现和冗长的 API。
  • Eclipse Collections 为传统 JDK 数据结构提供了替代品,并支持 Bag 和 Multimap 等数据结构。
  • 将 Streams 重构为 Eclipse Collections 有助于提高代码可读性并减少内存占用。
  • 最重要的是,使用 Eclipse Collections 来重构 Streams 非常简单!

在 Java 8 中引入的 Java Streams 非常棒——让我们可以充分利用 lambda 表达式来替换循环迭代代码,让代码更加接近于函数式编程风格。

然而,尽管 Streams 带来了改进,但它最终只是对现有集合框架的扩展,仍然背着很多包袱。

我们可以进一步改进吗?我们能否拥有更丰富的接口和更清晰、更易读的代码?与传统的集合相比,我们能否节省更多内存?我们能否更好、更无缝地支持函数式编程?

答案是肯定的! Eclipse Collections (以前叫作 GS Collections)是 Java Collections 框架的一个替代品,我们可以用它来实现我们的目的。

在本文中,我们将演示几个例子,将标准的 Java 代码重构成 Eclipse Collections 数据结构和 API,以及如何节省内存。

这里将会有很多代码示例,它们将展示如何将使用标准 Java 集合和 Streams 的代码改为使用 Eclipse Collection 框架的代码。

在深入研究代码之前,我们将花一些时间来了解 Eclipse Collections 是什么、我们为什么需要它,以及为什么需要将惯用的 Java 重构成 Eclipse Collections。

Eclipse Collections 的历史

Eclipse Collections 最初是由高盛公司创建的,他们的应用平台有一个大型的分布式缓存组件。该系统将数百 GB 的数据存储在内存中(现在仍在生产环境运行)。

事实上,缓存就是一个 Map,我们在 Map 里保存和读取对象。这些对象可以包含其他 Map 和集合。最初,缓存基于 java.util.* 包中的标准数据结构而构建。但很明显,这些集合有两个明显的缺点:内存使用效率低下,而且接口非常有限(导致重复且难以阅读的代码)。由于问题源于集合的实现,因此无法通过额外的代码库来解决这些问题。为了同时解决这两个问题,高盛公司决定从头开始创建一个新的集合框架。

在当时,它似乎是一个激进的解决方案,不过它确实可行。现在,这个框架托管给了 Eclipse 基金会。

在文章的最后,我们分享了一些链接,这些链接将帮助你了解有关这个项目本身的更多信息、学习如何使用 Eclipse Collections 以及如何成为这个项目的代码贡献者。

为什么要重构为 Eclipse Collections?

Eclipse Collections 有什么好处?因为它提供了更丰富的 API、高效的内存使用以及更好的性能。在我们看来,Eclipse Collections 是 Java 生态圈中最为丰富的集合库。而且它与 JDK 中的集合完全兼容。

轻松迁移

在深入了解这些好处之前,请务必注意,迁移到 Eclipse Collections 非常容易,不一定要一次性完成所有工作。Eclipse Collections 完全兼容 JDK 的 java.util.* List、Set 和 Map 接口。它也与 JDK 中的其他库兼容,比如 Collectors。我们的数据结构继承了 JDK 的这些接口,所以它们可以作为 JDK 对应的替代品(不过 Stack 接口是不兼容的,还有新的不可变集合也不兼容,因为在 JDK 中不存在相应的接口)。

更丰富的 API

实现了 java.util.List、Set 和 Map 接口的 Eclipse Collections 具有更丰富的 API,我们将在后面的代码示例中探讨这些 API。JDK 中缺少了一些类型,例如 Bag、Multimap 和 BiMap。Bag 是一种多重集,可以包含重复元素。从逻辑上讲,我们可以将其视为元素到它们出现次数的映射。BiMap 是一种“倒置”的 Map,不仅可以通过按键来查找值,也可以通过值来查找键。Multimap 是一种 Map,它的值就是集合(如 Key->List、Key->Set 等)。

eager 还是 lazy?

在使用 Eclipse Collections 时,我们可以非常容易地在 lazy 和 eager 两种实现模式间切换,有助于编写、理解和调试函数式代码。与 Streams API 不同的是,eager 是默认的模式。如果你想要使用 lazy 模式,只需要在开始你的逻辑代码之前,在你数据结构上调用.asLazy()。

不可变集合接口

有了不可变集合,你可以在 API 层面通过不可变性写出更加正确的代码。在这种情况下,程序的正确性将由编译器来保证,避免在执行过程中出现意外。借助不可变集合和更丰富的接口,你可以在 Java 中写出纯函数式代码。

原始类型集合

Eclipse Collections 也提供了原始类型的容器,所有原始集合类型都有不可变的对等物。值得一提的是,JDK 的 Streams 支持 int、long 和 double,而 Eclipse Collections 支持所有八个原始类型,并且可以定义用于直接保存原始值的集合(与它们的装箱对象不同,例如 Eclipse Collections IntList 是一个 int 列表,而 JDK 中的 List<Integer> 是一个装箱的原始值列表)。

没有“bun”方法

什么是“bun”方法?这是由 Oracle Java 首席设计师 Brian Goetz 发明的一个比喻说法。一个汉堡包(两片圆面包中间夹着肉)代表典型的流式代码结构。在使用 Java Streams 时,如果你想做点什么,必须把你的方法放在两块“面包”之间——前面是 stream()(或 parallelStream())方法,后面是 collect() 方法。这些面包其实没有什么营养,但如果没有它们,你就无法吃到肉。在 Eclipse Collections 中,这些方法不是必需的。下面的例子演示了 JDK 中的 bun 方法:假设我们有一个名单,上面有他们的姓名和年龄,我们想要取出年龄超过 21 岁的人的姓名:

复制代码
var people = List.of(new Person("Alice", 19),
new Person("Bob", 52), new Person("Carol", 35));
var namesOver21 = people.stream() // Bun
.filter(person -> person.getAge() > 21) // Meat
.map(Person::getName) // Meat
.collect(Collectors.toList()); // Bun
namesOver21.forEach(System.out::println);

下面是 Eclipse Collections 的代码——不需要 bun 方法!

复制代码
var people = Lists.immutable.of(new Person(“Alice”, 19),
new Person(“Bob”, 52), new Person(“Carol”, 35));
var namesOver21 = people
.select(person -> person.getAge() > 21) // Meat, no buns
.collect(Person::getName); // Meat
namesOver21.forEach(System.out::println);

任何你需要的类型

在 Eclipse Collections 中,每种情况都有相应的类型和方法,你可以根据你的需求找到它们。没有必要记住它们的名字——只要想想你需要什么样的数据结构。你需要一个可变或不可变的集合吗?排序的?你想要在集合中存储什么类型的数据——原始值还是对象?你需要什么样的结合?lazy 的、eager 的还是 parallel 的?后面将给出一张图表,按照这张图表中所列的方法,就可以轻松构建我们所需的数据结构。

通过工厂方法来实例化它们

这与 Java 9 中 List、Set 和 Map 接口的工厂方法类似,而且提供了更多选项!

部分按类别分组的方法

集合类型本身就提供了丰富的 API,可直接使用。这些集合类型继承了 RichIterable 接口(或 PrimitiveIterable)。我们将在接下来的例子中看到部分这样的 API。

更多方法

词云——这也不是什么新东西了,不是吗?不过,这并不是完全没有道理的——它表达了一些重要的观点。首先,方法太多了,涵盖了每个可以想象得到的迭代模式,可直接在集合类型上使用。其次,词云中的单词数量与方法的数量成正比。针对特定类型而优化的不同集合类型上有多种方法实现。

示例:字数统计

让我们从简单的事情开始。

给定一个文本(在本例中是一首童谣),计算文本中每个单词的出现次数,输出结果是单词集合和每个单词相应的出现次数。

复制代码
@BeforeClass
static public void loadData()
{
words = Lists.mutable.of((
"Bah, Bah, black sheep,\n" +
"Have you any wool?\n").split("[ ,\n?]+")
);
}

请注意,我们将使用 Eclipse Collections 工厂方法来计算单词。这相当于 JDK 中的 Arrays.asList(…) 方法,不过它返回的是 MutableList 的一个实例。由于 MutableList 接口与 JDK 的 List 完全兼容,因此我们可以在下面的 JDK 和 Eclipse Collections 示例中使用此类型。

首先,让我们来看看一个不使用 Streams 的实现:

复制代码
@Test
public void countJdkNaive()
{
Map<String, Integer> wordCount = new HashMap<>();
words.forEach(w -> {
int count = wordCount.getOrDefault(w, 0);
count++;
wordCount.put(w, count);
});
System.out.println(wordCount);
Assert.assertEquals(2, wordCount.get(“Bah”).intValue());
Assert.assertEquals(1, wordCount.get(“sheep”).intValue());
}

可以看到,我们创建了一个 String 到 Integer 的 HashMap(将每个单词映射到它的出现次数),遍历每个单词,并从 Map 中获得它的出现次数,如果单词不存在则默认为零。然后,我们增加该值并将其存回 Map 中。这不是一个很好的实现,因为我们关注的是“如何”而不是“什么”,并且性能也不是很好。让我们尝试使用 Streams 来重写它:

复制代码
@Test
public void countJdkStream()
{
Map<String, Long> wordCounts = words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
Assert.assertEquals(2, wordCounts.get(“Bah”).intValue());
Assert.assertEquals(1, wordCounts.get(“sheep”).intValue());
}

在这种情况下,代码具有更好的可读性,但效率仍然不是很高。你还需要了解如何使用 Collectors 类的方法——这些方法不容易被找到,因为它们不属于 Streams API。

高效的实现方法是引入一个单独的计数器类,并将其作为值保存在 Map 中。比方说,我们有一个名为 Counter 的类,用于保存一个整数值,并提供 increment() 方法,用于将该值递增 1。然后,我们可以将上面的代码重写为:

复制代码
@Test
public void countJdkEfficient()
{
Map<String, Counter> wordCounts = new HashMap<>();
words.forEach(
w -> {
Counter counter = wordCounts.computeIfAbsent(w, x -> new Counter());
counter.increment();
}
);
Assert.assertEquals(2, wordCounts.get(“Bah”).intValue());
Assert.assertEquals(1, wordCounts.get(“sheep”).intValue());
}

这实际上是一个非常高效的解决方案,但我们必须编写一个全新的类(Counter)。

Eclipse Collection Bag 提供了为这种问题量身定做的解决方案,并进行了优化。

复制代码
@Test
public void countEc()
{
Bag<String> bagOfWords = wordList.toBag();
// toBag() is a method on MutableList
Assert.assertEquals(2, bagOfWords.occurrencesOf(“Bah”));
Assert.assertEquals(1, bagOfWords.occurrencesOf(“sheep”));
Assert.assertEquals(0, bagOfWords.occurrencesOf(“Cheburashka”));
// null safe - returns a zero instead of throwing an NPE
}

我们所要做的就是调用集合的 toBag() 方法。而且,我们还可以不直接调用对象的 intValue() 方法来避免可能抛出的 NPE。

示例:动物园

假设我们有一个动物园。在动物园里,我们饲养着各种以不同食物为食的动物。

我们想查询一些有关动物和它们所吃食物的信息:

  • 最受欢迎的食物
  • 动物清单和它们喜欢的食物的数量
  • 食物单品
  • 食物种类
  • 肉类和非肉食动物

这些代码片段已经使用 Java Microbenchmark Harness(JMH)框架进行了测试。我们将过一遍代码,然后对它们进行比较。具体的性能比较结果,请参阅下面的“JMH 基准测试结果”部分。

这些是动物和它们喜欢吃的食物(每种食物都有名称、种类和数量)。

复制代码
private static final Food BANANA = new Food(“Banana”, FoodType.FRUIT, 50);
private static final Food APPLE = new Food(“Apple”, FoodType.FRUIT, 30);
private static final Food CAKE = new Food(“Cake”, FoodType.DESSERT, 22);
private static final Food CEREAL = new Food(“Cereal”, FoodType.DESSERT, 80);
private static final Food SPINACH = new Food(“Spinach”, FoodType.VEGETABLE, 26);
private static final Food CARROT = new Food(“Carrot”, FoodType.VEGETABLE, 27);
private static final Food HAMBURGER = new Food(“Hamburger”, FoodType.MEAT, 3);
private static MutableList<Animal> zooAnimals = Lists.mutable.with(
new Animal(“ZigZag”, AnimalType.ZEBRA, Lists.mutable.with(BANANA, APPLE)),
new Animal(“Tony”, AnimalType.TIGER, Lists.mutable.with(CEREAL, HAMBURGER)),
new Animal(“Phil”, AnimalType.GIRAFFE, Lists.mutable.with(CAKE, CARROT)),
new Animal(“Lil”, AnimalType.GIRAFFE, Lists.mutable.with(SPINACH)),

示例 1——最受欢迎的食物。

复制代码
@Benchmark
public List<Map.Entry<Food, Long>> mostPopularFoodItemJdk()
{
//output: [Hamburger=2]
return zooAnimals.stream()
.flatMap(animals -> animals.getFavoriteFoods().stream())
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.sorted(Map.Entry.<Food, Long>comparingByValue().reversed())
.limit(1)
.collect(Collectors.toList());
}

我们首先对 zooAnimals 进行流式化,并将每只动物 flatMap() 到它最喜欢的食物,返回一个流。接下来,我们使用食物的标识作为关键字、数量作为值对食物进行分组,这样就可以确定每个食物对应的动物的数量。这是 Collectors.counting() 要做的工作。为了对它进行排序,我们调用 Map 的 entrySet() 方法,对它进行流式化,并通过反向值对它进行排序(这个值是每种食物的计数,如果我们想知道最受欢迎的食物,就需要按照逆序排序),然后调用 limit(1) 返回最大值,最后,我们将它收集到一个 List 中。

结果最受欢迎的食物是 [Hamburger = 2]。

接下来,让我们来看看如何使用 Eclipse Collections 实现同样的功能。

复制代码
@Benchmark
public MutableList<ObjectIntPair<Food>> mostPopularFoodItemEc()
{
//output: [Hamburger:2]
MutableList<ObjectIntPair<Food>> intIntPairs = zooAnimals.asLazy()
.flatCollect(Animal::getFavoriteFoods)
.toBag()
.topOccurrences(1);
return intIntPairs;
}

我们也从将每只动物 flatMap 到它最喜欢的食物开始。因为我们真正想要的是食物到数量的 Map,所以 Bag 可以完美解决我们的问题。我们先调用 toBag(),再调用 topOccurrences(),它返回最频繁出现的食物项目。topOccurrences(1) 返回最受欢迎的食物,并作为 ObjectIntPairs 列表返回(注意 int 是原始类型),结果是 [Hamberger:2]。

示例 2——动物喜欢的食物的数量:有多少动物只吃一种食物?有多少动物吃两种食物?

首先是 JDK 的实现:

复制代码
@Benchmark
public Map<Integer, String> printNumberOfFavoriteFoodItemsToAnimalsJdk()
{
//output: {1=[Lil, GIRAFFE],[Simba, LION], 2=[ZigZag, ZEBRA],
// [Tony, TIGER],[Phil, GIRAFFE]}
return zooAnimals.stream()
.collect(Collectors.groupingBy(
Animal::getNumberOfFavoriteFoods,
Collectors.mapping(
Object::toString,
// Animal.toString() returns [name, type]
Collectors.joining(“,”))));
// Concatenate the list of animals for
// each count into a string
}

然后是使用 Eclipse Collections:

复制代码
@Benchmark
public Map<Integer, String> printNumberOfFavoriteFoodItemsToAnimalsEc()
{
//output: {1=[Lil, GIRAFFE], [Simba, LION], 2=[ZigZag, ZEBRA],
// [Tony, TIGER], [Phil, GIRAFFE]}
return zooAnimals
.stream()
.collect(Collectors.groupingBy(
Animal::getNumberOfFavoriteFoods,
Collectors2.makeString()));
}

本示例重点介绍了如何结合使用原生 Java Collectors 和 Eclipse Collections Collector2,两者并不相互排斥。在这个例子中,我们想要获得每只动物的食物数量。那么如何实现这一目的?在原生 Java 中,我们首先使用 Collectors.groupingBy 将每只动物按照其最喜欢的食物数量分组。然后,我们使用 Collectors.mapping 函数将每个对象映射到它的 toString 方法,最后调用 Collectors.joining 将字符串连接起来,并用逗号分隔。

在 Eclipse Collections 中,我们也可以使用 Collectors.groupingBy 方法,不过也会调用更简洁的 Collectors2.makeString 来获得相同的结果(makeString 将一个流变成一个以逗号分隔的字符串)。

示例 3——食物单品:有多少种不同类型的食物,它们分别是什么?

复制代码
@Benchmark
public Set<Food> uniqueFoodsJdk()
{
return zooAnimals.stream()
.flatMap(each -> each.getFavoriteFoods().stream())
.collect(Collectors.toSet());
}
@Benchmark
public Set<Food> uniqueFoodsEcWithoutTargetCollection()
{
return zooAnimals.flatCollect(Animal::getFavoriteFoods).toSet();
}
@Benchmark
public Set<Food> uniqueFoodsEcWithTargetCollection()
{
return zooAnimals.flatCollect(Animal::getFavoriteFoods,
Sets.mutable.empty());
}

我们有几种方法可用来解决这个问题!如果使用 JDK,我们对 zooAnimals 进行流式化,然后对它们最喜欢的食物进行 flatMap,最后将它们收集到一个集合中。如果使用 Eclipse Collections,我们有两种处理方式。第一种与 JDK 版本大致相同,flat 食物,然后调用 toSet() 将它们放入一个集合中。第二种方式很有趣,因为它使用了目标集合的概念。flatCollect 是一个重载的方法,所以提供了几种不同的使用方式。如果传入一个集合作为第二个参数,意味着我们将直接将食物 flat 到集合中,并跳过第一个示例中使用的中间列表。我们可以调用 asLazy() 来避免这种中间结果,运算会一直等待最终操作结束,从而避免出现中间状态。不过,如果你喜欢较少的 API 调用,或者需要将结果累加到现有的集合中,那么在从一种类型转换为另一种类型时请考虑使用目标集合。

示例 4——肉食和非肉食动物:有多少肉食动物?多少非肉食动物?

请注意,在以下的两个示例中,我们选择在顶部显式(而不是通过内联的方式)声明 Predicate lambda,用以强调 JDK Predicate 和 Eclipse Collections Predicate 之间的区别。 Eclipse Collections 早在 Java 8 的 java.util.function 包出现之前,就已经有了 Function、Predicate 和其他函数类型的定义。现在,Eclipse Collections 中的函数类型扩展了 JDK 中的等价类型,从而可以与依赖 JDK 库进行互操作。

复制代码
@Benchmark
public Map<Boolean, List<Animal>> getMeatAndNonMeatEatersJdk()
{
java.util.function.Predicate<Animal> eatsMeat = animal ->
animal.getFavoriteFoods().stream().anyMatch(
food -> food.getFoodType()== FoodType.MEAT);
Map<Boolean, List<Animal>> meatAndNonMeatEaters = zooAnimals
.stream()
.collect(Collectors.partitioningBy(eatsMeat));
//returns{false=[[ZigZag, ZEBRA], [Phil, GIRAFFE], [Lil, GIRAFFE]],
true=[[Tony, TIGER], [Simba, LION]]}
return meatAndNonMeatEaters;
}
@Benchmark
public PartitionMutableList<Animal> getMeatAndNonMeatEatersEc()
{
org.eclipse.collections.api.block.predicate.Predicate<Animal> eatsMeat =
animal ->animal.getFavoriteFoods()
.anySatisfy(food -> food.getFoodType() == FoodType.MEAT);
PartitionMutableList<Animal> meatAndNonMeatEaters =
zooAnimals.partition(eatsMeat);
// meatAndNonMeatEaters.getSelected() = [[Tony, TIGER], [Simba, LION]]
// meatAndNonMeatEaters.getRejected() = [[ZigZag, ZEBRA], [Phil, GIRAFFE],
// [Lil, GIRAFFE]]
return meatAndNonMeatEaters;
}

我们想要通过肉食和非肉食动物来分隔元素。我们构建了一个 Predicate “eatsMeat”,它检查每只动物喜欢的食物,看看是否 anyMatch(JDK)或 anySatisfy(Eclipse Collections),条件为食物类型为 FoodType.MEAT。

在 JDK 示例中,我们对动物进行 stream(),并调用 partitioningBy(),传入 eatsMeat Predicate。返回的是一个带有 true 或 false 作为键的 Map。“true”将返回肉食动物,而“false”则返回非肉食动物。

在 Eclipse Collections 中,我们在 zooAnimals 上调用 partition(),同时传入 Predicate。我们会得到一个 PartitionMutableList,它提供了两个方法——getSelected() 和 getRejected(),它们都返回 MutableLists。被选定的元素就是肉食动物,被拒绝的元素就是非肉食动物。

内存使用比较

在上面的例子中,重点主要集中在集合的类型和接口上。我们在开始的时候提到了使用 Eclipse Collections 将会带来内存方面的优化。效果可能会非常显着,具体取决于特定应用程序中使用了多大的集合以及什么类型的集合。

从图中可以看到 Eclipse Collections 和 java.util.* 集合之间的内存使用情况比较。

横轴表示存储在集合中的元素的数量,纵轴表示以千字节为单位的存储开销。这里的开销表示减去集合有效载荷之后所使用的内存(因此我们只显示数据结构本身占用的内存)。在调用 System.gc() 之后,我们使用 totalMemory()-freeMemory() 来得出内存使用量。我们观察到的结果是稳定的,并且与 Java 8 使用 jdk.nashorn.internal.ir.debug.ObjectSizeCalculator 的示例获得的结果是一致的(这个程序可以精确计算对象大小,可惜的是与 Java 9 及更高版本不兼容)。

第一张图显示了 Eclipse Collections int 列表与 JDK Integer 列表对比的优势。该图显示,对于一百万个值,java.util.* 中的列表将多用 15MB 内存(对于 JDK 约为 20MB 的内存开销,对于 Eclipse Collections 约为 5MB)。

Java 中的 Map 效率非常低,因为需要用到 Map.Entry 对象,这会扩大内存使用量。

如果说 Map 内存效率不高,那么 Set 的效率就是糟糕透顶,因为 Set 的底层实现使用了 Map,这太浪费内存了。Map.Entry 没有多大用处,因为它只有一个属性是有用的——键,也就是集合的元素。因此,你会发现,Java 中的 Set 和 Map 使用相同数量的内存,但 Set 可以变得更加紧凑,Eclipse Collections 就做到了这一点。它最终使用的内存比 JDK 集合少得多,如上图所示。

最后,第四张图显示了特定结合类型的优点。如前所述,Bag 只是​​一个集合,它允许每个元素存在多个实例,并且可以将元素与其出现的次数映射起来。你可以使用 Bag 来统计元素的出现次数。java.util.* 中的等效数据结构是元素到其次数的 Map,开发人员需要负责更新元素出现的次数。可以看到,特定数据结构(Bag)已经被优化到可以最大限度地减少内存使用和垃圾收集。

当然,我们建议对每个个案进行测试。如果用 Eclipse Collections 替换标准 Java 集合,那么结果肯定会得到改进,但是它们对内存整体使用的影响程度取决于具体情况。

JMH 基准测试结果

在本节中,我们将分析之前那些示例的运行速度,对比改用 Eclipse Collections 重写之前和之后代码的性能差别。该图显示了每个示例中 Eclipse Collections 和 JDK 的每秒操作数量。较长的条表示更好的结果。正如你所看到的,速度的提升是非常明显的:

有必要强调的是,我们展示的结果仅适用于上述的具体示例。具体结果将取决于你们的特定情况,因此请务必针对你们的真实场景进行测试。

结论

Eclipse Collections 在过去的 10 多年中一直在演化,用以优化 Java 代码和应用程​​序。它简单易用——现成的数据结构,并提供了比传统流式代码更流畅的 API。还有我们没有解决的用例?我们希望你们能够加入到贡献者行列中!欢迎从 GitHub 上拉取我们的代码,一起分享你们的结果!我们很乐意看到你们分享使用 Eclipse Collections 的体验以及它如何影响你们的应用程序。祝编码愉快!

有用的链接

关于作者

Kristen O'Leary 是高盛服务工程小组的高级副总裁。她为 Eclipse Collections 带来了多个容器、API 和性能增强功能,并且还在公司内部和外部教授有关该框架的课程。

Vladimir Zakharov 在软件开发方面有超过二十年的经验。他目前是高盛平台业务部门的董事总经理。他在过去的 18 年中一直使用 Java 进行开发,在此之前他还使用了 Smalltalk 和其他一些比较晦涩的编程语言。

查看英文原文 Refactoring to Eclipse Collections: Making Your Java Streams Leaner, Meaner, and Cleaner

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论