在 GS Collections 实例教程(第一部分)中,我展示了几种运用select和selectWith的方法去筛选一个集合。
要调用方法select,我们通常会传递一个Lambda表达式的Predicate。同样来说,如果要调用方法selectWith,我们会传递一个Predicate2作为方法引用。
在GS Collections中有许多方法接受Predicate参数,比如说select、reject、detect,、anySatisfy、allSatisfy、noneSatisfy、count以及partition。
同样,也有许多方法接受Predicate2参数,例如selectWith、rejectWith、detectWith、anySatisfyWith、allSatisfyWith、noneSatisfyWith、countWith以及partitionWith。
在实例教程(第二部分)中,我们会深入地研究对Predicates的调用和使用Function作为参数的转化性方法(例如 collect、flatCollect、groupBy、groupByEach)。
接下来,我们会展示一些例子,演示采用GS Collections的API去转化object container成为primitive container,以及如何使用这些primitive container。这些例子都是基于以下这个简单的领域模型。
以下例子为单元测试,须在Java 8下运行。
希望这些例子能激发你的兴趣,去深入探索GS Collections里面完善和丰富的API。
例子2: 集合里面 任意 一个元素满足所给的条件吗?
使用anySatisfy:
@Test public void doAnyPeopleHaveCats() { Predicate predicate = person -> person.hasPet(PetType.CAT); boolean result = this.people.anySatisfy(predicate); Assert.assertTrue(result); boolean result1 = this.people.anySatisfyWith(Person::hasPet, PetType.CAT); Assert.assertTrue(result1); }
例子3: 集合里面 所有 元素满足所给的条件吗?
使用allSatisfy:
@Test public void doAllPeopleHaveCats() { boolean result = this.people.allSatisfy(person -> person.hasPet(PetType.CAT)); Assert.assertFalse(result); boolean result1 = this.people.allSatisfyWith(Person::hasPet, PetType.CAT); Assert.assertFalse(result1); }
例子4: 集合里面 没有 元素满足所给的条件吗?
使用noneSatisfy:
@Test public void doNoPeopleHaveCats() { boolean result = this.people.noneSatisfy(person -> person.hasPet(PetType.CAT)); Assert.assertFalse(result); boolean result1 = this.people.noneSatisfyWith(Person::hasPet, PetType.CAT); Assert.assertFalse(result1); }
例子5: 集合里面有多少元素满足所给的条件。
使用count:
@Test public void howManyPeopleHaveCats() { int count = this.people.count(person -> person.hasPet(PetType.CAT)); Assert.assertEquals(2, count); int count1 = this.people.countWith(Person::hasPet, PetType.CAT); Assert.assertEquals(2, count1); }
例子6: 寻找集合里面满足所给条件的第一个元素。
使用detect:
@Test public void findPersonNamedMarySmith() { Person result = this.people.detect(person -> person.named("Mary Smith")); Assert.assertEquals("Mary", result.getFirstName()); Assert.assertEquals("Smith", result.getLastName()); Person result1 = this.people.detectWith(Person::named, "Mary Smith"); Assert.assertEquals("Mary", result1.getFirstName()); Assert.assertEquals("Smith", result1.getLastName()); }
any、all、noneSatisfy以及detect都是短路求值方法的例子,它们可以不用遍历每一个集合里的元素就可返回结果。比如说,anySatisfy就像逻辑中的“ 或 ”,只要找到一个满足条件的元素它就会判断为是,否则将检视整个集合而判断为否。
以下的例子也接受predicates ,但不会进行短路操作。
例子7: 选取集合里面满足所给条件的所有元素。
在GS Collections 实例教程(第一部分)中展示了许多这方面的例子。以下是利用Person/Pet领域对这个方法的回顾。用来过滤集合的方法称为select。
@Test public void getPeopleWithCats() { MutableList peopleWithCats = this.people.select(person -> person.hasPet(PetType.CAT)) Verify.assertSize(2, peopleWithCats); MutableList peopleWithCats1 = this.people.selectWith(Person::hasPet, PetType.CAT); Verify.assertSize(2, peopleWithCats1); }
例子8: 寻找集合里所有不满足所给条件的元素。
使用reject:
@Test public void getPeopleWhoDontHaveCats() { MutableList peopleWithNoCats = this.people.reject(person -> person.hasPet(PetType.CAT)); Verify.assertSize(5, peopleWithNoCats); MutableList peopleWithNoCats1 = this.people.rejectWith(Person::hasPet, PetType.CAT); Verify.assertSize(5, peopleWithNoCats1); }
例子9: 分隔集合里满足所给条件的元素和不满足所给条件的元素。
使用partition:
@Test public void partitionPeopleByCatOwnersAndNonCatOwners() { PartitionMutableList catsAndNoCats = this.people.partition(person -> person.hasPet(PetType.CAT)); Verify.assertSize(2, catsAndNoCats.getSelected()); Verify.assertSize(5, catsAndNoCats.getRejected()); PartitionMutableList catsAndNoCats1 = this.people.partitionWith(Person::hasPet, PetType.CAT); Verify.assertSize(2, catsAndNoCats1.getSelected()); Verify.assertSize(5, catsAndNoCats1.getRejected()); }
在以上例子中,返回类型是特殊的PartitionMutableList,它的父接口是PartitionIterable。这个接口包含了两个方法:getSelected和getRejected。这些方法与PartitionIterable的子类型是共变的,所以对于PartitionMutableList来说,这两个方法的返回值是MutableList。对于父类型PartitionIterable来说,它的返回值 将会 是RichIterable。
以上总结了调用Predicate和Predicate2作为参数的API。接下来让我们看看调用Function和Function2作为参数的API。
例子10: 将一个集合从一种类型转化为另一种类型。
使用collect:
@Test public void getTheNamesOfBobSmithPets() { Person person = this.people.detectWith(Person::named, "Bob Smith"); MutableList pets = person.getPets(); MutableList names = pets.collect(Pet::getName); Assert.assertEquals("Dolly, Spot", names.makeString()); }
在以上的例子中,我们先找到叫“Bob Smith”的人,取得他宠物的列表,然后再从列表中搜集宠物的名字,最终转化成为MutableList
例子11: 根据一个特性来展平一个集合。
如果你想从一个集合的集合中收集一个属性作为一个统一的集合,应当使用flatCollect。
@Test public void getAllPets() { Function<Person, Iterable<PetType>> function = person -> person.getPetTypes(); Assert.assertEquals( UnifiedSet.newSetWith(PetType.values()), this.people.flatCollect(function).toSet() ); Assert.assertEquals( UnifiedSet.newSetWith(PetType.values()), this.people.flatCollect(Person::getPetTypes).toSet() ); }
在第一个例子中,为了解释flatCollect期待的类型,我抽取了Lambda作为一个单独的变量。Collect方法采用Function<? super T, ? extends V> flatCollect方法则采用 Function<? super T, ? extends Iterable<V>>。换句话来说,传入flatCollect的Function必须返回iterable类型。
例子12: 根据某种function对一个集合进行分类。
使用groupBy:
@Test public void groupPeopleByLastName() { Multimap<String, Person> byLastName = this.people.groupBy(Person::getLastName); Verify.assertIterableSize(3, byLastName.get("Smith")); }
在上述的例子中,根据大家的姓氏进行分类。这个分类表明有3个人姓Smith。groupBy方法的返回值是Multimap。Multimap可以被想成等同于Map<K, Iterable<V>>。在GS Collections中有许多特殊化定义的Multimap(例如List、Set、 Bag、SortedBag、SortedSet),它们既有可变的也有不可变的类型。这个例子里使用了父类型Multimap,但是也可以用特殊化的ListMultimap或者MutableListMultimap。
例子13: 根据返回值是多个元素的function对一个集合进行分类。
使用groupByEach:
@Test public void groupPeopleByTheirPets() { Multimap<PetType, Person> peopleByPets = this.people.groupByEach(Person::getPetTypes); RichIterable<Person> catPeople = peopleByPets.get(PetType.CAT); Assert.assertEquals( "Mary, Bob", catPeople.collect(Person::getFirstName).makeString() ); RichIterable<Person> dogPeople = peopleByPets.get(PetType.DOG); Assert.assertEquals( "Bob, Ted", dogPeople.collect(Person::getFirstName).makeString() ); }
类似于flatCollect,groupByEach方法采用了一个Function<? super T, ? extends Iterable<V>>。返回值Multimap<K, T>实际上就是一个多重索引,其中每个值都可以有多个索引。在这个例子中,我们根据大家所拥有的宠物种类对人进行分类。一个人可以拥有多种宠物,正是因为这个原因Bob同时出现在两个字串中。我在这里使用了collect把每个Person转换成他们的名字,然后使用makeString去转换成一个以逗号分隔的字串。makeString还有其他的形式可以接受分隔符号、开始和末尾字符作为参数。
例子14: 对于基本类别的返回值进行求和。
可以使用RichIterable的四种sumOf方法之一:sumOfInt、sumOfFloat、sumOfLong以及sumOfDouble。
@Test public void getTotalNumberOfPets() { long numberOfPets = this.people.sumOfInt(Person::getNumberOfPets); Assert.assertEquals(9, numberOfPets); }
在以上的例子中,我们对每个人所拥有的宠物数量进行求和,结果是所有人所拥有的宠物的总和。如果是ints和floats求和,返回值是更大的类型long和double。sumOfInt方法采用了一个特殊形式的Function叫做IntFunction。
public interface IntFunction<T> extends Serializable { int intValueOf(T anObject); }
GS Collections里所有的Procedures,Functions,Predicates 都继承了Serializable。这样一来,程序员不用编写自己的Serializable扩展就可以安全地序列化到磁盘上或是远程传递。
例子15: 对象集合(object collection)和原始集合(primitive collection)转化的流畅性。
如果你想从一个对象集合转化为原始集合,你可以使用任何一个8种专业化原始类型的collect (collectInt / Float / Long / Double / Byte / Short / Char / Boolean) 。你可以只使用collect把一个原始集合转化为对象集合。这个功能为使用API增加了流畅性。
@Test public void getAgesOfPets() { IntList sortedAges = this.people .asLazy() .flatCollect(Person::getPets) .collectInt(Pet::getAge) .toSortedList(); IntSet uniqueAges = sortedAges.toSet(); IntSummaryStatistics stats = new IntSummaryStatistics(); sortedAges.forEach(stats::accept); Assert.assertTrue(sortedAges.allSatisfy(IntPredicates.greaterThan(0))); Assert.assertTrue(sortedAges.allSatisfy(i -> i > 0)); Assert.assertFalse(sortedAges.anySatisfy(i -> i == 0)); Assert.assertTrue(sortedAges.noneSatisfy(i -> i < 0)); Assert.assertEquals(IntHashSet.newSetWith(1, 2, 3, 4), uniqueAges); Assert.assertEquals(2.0d, sortedAges.median(), 0.0); Assert.assertEquals(stats.getMin(), sortedAges.min()); Assert.assertEquals(stats.getMax(), sortedAges.max()); Assert.assertEquals(stats.getSum(), sortedAges.sum()); Assert.assertEquals(stats.getAverage(), sortedAges.average(), 0.0); Assert.assertEquals(stats.getCount(), sortedAges.size()); }
在这个例子里面,我回答了许多关于名单上的人所拥有宠物的年龄问题。首先,我使用了asLazy()方法,这是一个谨慎的决定,因为我想尽量减少临时集合的数量。当然如果拿掉asLazy()的调用,上面的代码还是可以运行的。在这里asLazy()方法的调用完全是为了优化内存。然后,我调用了flatCollect,这个方法把所有人的宠物收集在一个平展的集合中。接着,我调用了collectInt,这个方法把LazyIterable<Pet>转化为IntIterable。每一个Pet通过getAge()方法被转化成它的年纪。如果之前没有使用asLazy()方法那将会把一个MutableList<Pet>转化为IntList。最后我调用了toSortedList,这个方法接受IntInterable然后转化为一个IntList最后给ints排序。在IntList上调用toSet方法然后保存年龄作为一个set。
以上展示了GS Collections里面原始集合的丰富性。在集合上还可以直接调用统计方法像min、max、sum、average、median。在这里我还在IntList里使用了Java 8新的统计类叫 IntSummaryStatistics 。这是通过对IntSumaryStatistics::accept的方法引用而实现的。这个例子里还使用了之前例子中展示过的any、all、noneSatisfy方法, 只不过在这里它们被用于一个原始集合中。
例子16: 计算某个元素在一个集合的出现次数。
如果你想迅速的找出各个元素在集合里面的数量,可以把集合转化为一个Bag。Bag可以等同于Map<K, Integer>, 在这里Integer是元素K的出现次数。Bag像其它的集合一样有add、remove的方法,它还有特殊的方法去计算、加减一个元素的出现次数。Bag就像一个允许重复元素的Set而且还保存了每个元素的出现次数。
@Test public void getCountsByPetType() { Bag<PetType> counts = this.people .asLazy() .flatCollect(Person::getPets) .collect(Pet::getType) .toBag(); Assert.assertEquals(2, counts.occurrencesOf(PetType.CAT)); Assert.assertEquals(2, counts.occurrencesOf(PetType.DOG)); Assert.assertEquals(2, counts.occurrencesOf(PetType.HAMSTER)); Assert.assertEquals(1, counts.occurrencesOf(PetType.SNAKE)); Assert.assertEquals(1, counts.occurrencesOf(PetType.TURTLE)); Assert.assertEquals(1, counts.occurrencesOf(PetType.BIRD)); }
在当今的JDK里面没有像Bag一样的类型,同样的也缺少像PartitionIterable或是Multimap。在Java 8 Collectors的方法中,以上的类型被模拟为Map<K, Integer> (Collectors.counting), Map<Boolean, List<V> (Collectors.partitioning)以及Map<K, List<V>> (Collectors.groupingBy)。
例子17: 计算某个原始值在一个集合的出现次数。
如果你想找出某个原始值在集合里面的数量,可以使用primitive Bag。
@Test public void getCountsByPetAge() { IntBag counts = this.people .asLazy() .flatCollect(Person::getPets) .collectInt(Pet::getAge) .toBag(); Assert.assertEquals(4, counts.occurrencesOf(1)); Assert.assertEquals(3, counts.occurrencesOf(2)); Assert.assertEquals(1, counts.occurrencesOf(3)); Assert.assertEquals(1, counts.occurrencesOf(4)); Assert.assertEquals(0, counts.occurrencesOf(5)); }
在这个例子中,我从所有宠物的年龄集中创建了一个IntBag。这样一来就可以使用IntBag附带的occurencesOf方法找出每一年龄的出现次数。这是实例教程(第二部分)的最后一个例子。
这些例子提供了一个小样本,演示了用GS Collections的API可以做什么样的事情。现在RichIterable接口有一百多个方法可供使用。这为Java开发员提供了一套极为丰富的功能去处理集合的问题。
在2014 JavaOne大会上,Craig Motlin 和我在“GS Collections and Java 8: Functional, Fluent, Friendly and Fun!”座谈会上讲解了几个例子去比较如何使用Java 8 Stream和GS Collections做相同的事情。你可以在 JavaOne 2014 大会网站 或是 GS Collections GitHub wiki 上找到这些演讲资料。
仅供参考,以下代码包含了前面的例子中所提到的各种测试类,你可以用来创建自己测试代码。
import com.gs.collections.api.RichIterable; import com.gs.collections.api.bag.Bag; import com.gs.collections.api.bag.MutableBag; import com.gs.collections.api.bag.primitive.IntBag; import com.gs.collections.api.block.function.Function; import com.gs.collections.api.block.predicate.Predicate; import com.gs.collections.api.list.MutableList; import com.gs.collections.api.list.primitive.IntList; import com.gs.collections.api.multimap.Multimap; import com.gs.collections.api.partition.list.PartitionMutableList; import com.gs.collections.api.set.primitive.IntSet; import com.gs.collections.impl.bag.mutable.HashBag; import com.gs.collections.impl.block.factory.Predicates2; import com.gs.collections.impl.block.factory.primitive.IntPredicates; import com.gs.collections.impl.list.mutable.FastList; import com.gs.collections.impl.set.mutable.UnifiedSet; import com.gs.collections.impl.set.mutable.primitive.IntHashSet; import com.gs.collections.impl.test.Verify import org.junit.Assert; import org.junit.Before; import org.junit.Test; import java.util.IntSummaryStatistics; public class PersonTest { MutableList<Person> people; @Before public void setUp() throws Exception { this.people = FastList.newListWith( new Person("Mary", "Smith").addPet(PetType.CAT, "Tabby", 2), new Person("Bob", "Smith").addPet(PetType.CAT, "Dolly", 3).addPet(PetType.DOG, "Spot", 2), new Person("Ted", "Smith").addPet(PetType.DOG, "Spike", 4), new Person("Jake", "Snake").addPet(PetType.SNAKE, "Serpy", 1), new Person("Barry", "Bird").addPet(PetType.BIRD, "Tweety", 2), new Person("Terry", "Turtle").addPet(PetType.TURTLE, "Speedy", 1) new Person("Harry", "Hamster").addPet(PetType.HAMSTER, "Fuzzy", 1).addPet(PetType.HAMSTER, "Wuzzy", 1) ); } public class Person { private String firstName; private String lastName; private MutableList<Pet> pets = FastList.newList(); private Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; public String getFirstName() { return this.firstName; } public String getLastName() { return this.lastName; } public boolean named(String name) { return name.equals(this.getFirstName() + " " + this.getLastName()); } public boolean hasPet(PetType petType) { return this.pets.anySatisfyWith(Predicates2.attributeEqual(Pet::getType), petType); } public MutableList<Pet> getPets() { return this.pets; } public MutableBag<PetType> getPetTypes() { return this.pets.collect(Pet::getType, HashBag.newBag()); } public Person addPet(PetType petType, String name, int age) { this.pets.add(new Pet(petType, name, age)); return this; } public int getNumberOfPets() { return this.pets.size(); } } public class Pet { private PetType type; private String name; private int age; public Pet(PetType type, String name, int age) this.type = type; this.name = name; this.age = age; } public PetType getType() { return this.type; } public String getName() { return this.name; } public int getAge() { return this.age; } } public enum PetType { CAT, DOG, HAMSTER, TURTLE, BIRD, SNAKE } }
Donald Raab 在高盛的信息技术部领导JVM Architecture 小组,该小组隶属于Enterprise Platforms团队。Raab是JSR 335专家组(Java编程语言的Lambda Expressions)的成员,并且是高盛在JCP(Java Community Process)执行委员会的代表之一。他于2001年作为技术架构师加入高盛信息技术部的会计&风险分析组。他在2007年被授予高盛的Technology Fellow头衔,并在2013年成为董事总经理。
译者 钟 新 (Josaline Zhong), 现任高盛信息技术部的资产管理合规技术组副经理。作为GS Collections的贡献者和内部讲师之一,一直对GS Collections的推广和应用充满高度热忱。
请访问 www.gs.com/engineering 获取关于GS Collections和高盛信息技术部的更多信息。
本文章反映的信息仅为高盛信息技术部门所有,并非高盛其他部门所持信息。其不得被依赖或被视为投资建议。除非明确标识,其表达观点并非一定为高盛所持观点。高盛公司不担保、不保证本文章的精确、完整或效用。接收者不应依赖本文章,除非在自担风险的范围内。在未刊载声明的情形下,本文章不得被转发、披露。
感谢丁晓昀对本文的审校。
给InfoQ中文站投稿或者参与内容翻译工作,请邮件至editors@cn.infoq.com。也欢迎大家通过新浪微博(@InfoQ)或者腾讯微博(@InfoQ)关注我们,并与我们的编辑和其他读者朋友交流。