BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles GS Collections by Example – Part 2

GS Collections by Example – Part 2

Leia em Português

This item in japanese

Lire ce contenu en français

Bookmarks

 

In part 1 of GS Collections by Example, I demonstrated several different ways to filter a collection in GS Collections using select and selectWith.  

In order to call select, we passed in a Predicate, usually as a lambda. To call selectWith we passed a Predicate2, usually as a method reference.  

There are quite a few methods in GS Collections that take a Predicate including select, reject, detect, anySatisfy, allSatisfy, noneSatisfy, count and partition.  

There are also forms that take a Predicate2 called selectWith, rejectWith, detectWith, anySatisfyWith, allSatisfyWith, noneSatisfyWith, countWith and partitionWith.  

In part 2 we will take a deeper look at calls with Predicates and see some examples of “transforming” methods (collect, flatCollect, groupBy, groupByEach), which all take the type Function as an argument.  

I will then show examples that leverage the API of GS Collections to move from object containers to primitive containers and how to leverage the APIs available on the primitive container types. I will cover a few examples using a simple domain shown below.

The examples are unit tests and require Java 8 to run.

[Click on the image to enlarge it]

Hopefully by the end of the examples you’ll be interested to further explore the rich and complete API we have developed in GS Collections.

Example 2: Do any elements of a collection match a given predicate?

Use anySatisfy:

@Test
public void doAnyPeopleHaveCats()
{
    Predicate<Person> 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);
}

Example 3: Do all elements of a collection match a given predicate?

Use 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);
}

Example 4: Do none of the elements of a collection match a given predicate?

Use 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);
}

Example 5: Count the number of elements that match a given predicate

Use 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);
}

Example 6: Find the first element of a collection that matches a given predicate

Use 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());
}

The methods any/all/noneSatisfy and detect are examples of short-circuiting methods that may return a result without looping through the whole collection. For example, anySatisfy behaves like a logical or; it returns true as soon as it finds an element that satisfies the Predicate or iterates through the entire collection and returns false.

The following examples also take predicates but do not short-circuit.

Example 7: Selecting the elements of a collection

I showed many examples of filtering in part 1 of this series.  Here is a refresher using the Person/Pet domain.  The method to filter a collection is named select.

@Test
public void getPeopleWithCats()
{
    MutableList<Person> peopleWithCats =
        this.people.select(person -> person.hasPet(PetType.CAT))
    Verify.assertSize(2, peopleWithCats);

    MutableList<Person> peopleWithCats1 =
        this.people.selectWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(2, peopleWithCats1);
}

Example 8: Find the elements of a collection that do not match a given predicate

Use reject:

@Test
public void getPeopleWhoDontHaveCats()
{
    MutableList<Person> peopleWithNoCats =
        this.people.reject(person -> person.hasPet(PetType.CAT));
    Verify.assertSize(5, peopleWithNoCats);
   
    MutableList<Person> peopleWithNoCats1 =
        this.people.rejectWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(5, peopleWithNoCats1);
}

Example 9: Partition the elements of a collection into those that do and do not match a given predicate

Use partition:

@Test
public void partitionPeopleByCatOwnersAndNonCatOwners()
{
    PartitionMutableList<Person> catsAndNoCats =
	this.people.partition(person -> person.hasPet(PetType.CAT));
    Verify.assertSize(2, catsAndNoCats.getSelected());
    Verify.assertSize(5, catsAndNoCats.getRejected());

    PartitionMutableList<Person> catsAndNoCats1 =
        this.people.partitionWith(Person::hasPet, PetType.CAT);
    Verify.assertSize(2, catsAndNoCats1.getSelected());
    Verify.assertSize(5, catsAndNoCats1.getRejected());
}

In this example, a special type PartitionMutableList was returned.  The parent of this interface is PartitionIterable.  This interface has two methods: getSelected and getRejected.  These methods are covariant in the subtypes of PartitionIterable.  So in PartitionMutableList, the return types of both of these methods are MutableList.  In the parent type, PartitionIterable, the return type would be defined as RichIterable. 

That wraps up the section on API taking Predicate/Predicate2 parameters.  Let’s move on to API taking Function/Function2 parameters.

Example 10: Transforming a collection from one type to another

Use collect:   

@Test
public void getTheNamesOfBobSmithPets()
{
    Person person =
	this.people.detectWith(Person::named, "Bob Smith");
    MutableList<Pet> pets = person.getPets();
    MutableList<String> names =
        pets.collect(Pet::getName);
    Assert.assertEquals("Dolly, Spot", names.makeString());
}

In the above example, we find a person named “Bob Smith”, get his list of pets, and convert them to MutableList<String>.  We collect the list of pet names from the collection. 

Example 11: Flattening a collection based on an attribute

If you want to collect an attribute of a collection which also points to a collection into a single flattened collection, use 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()
    );
}

In the first example, I extracted the lambda into a separate variable to illustrate the type that flatCollect expects.  The collect method takes Function<? super T, ? extends V>.  The flatCollect method takes Function<? super T, ? extends Iterable<V>>.  In other words, the Function passed to flatCollect must return some iterable type.

Example 12: Grouping a collection based on some function

Use groupBy:

@Test
public void groupPeopleByLastName()
{
    Multimap<String, Person> byLastName =
this.people.groupBy(Person::getLastName);
    Verify.assertIterableSize(3, byLastName.get("Smith"));
}

In the above example, the people were grouped according to their last name. The test shows there are 3 people with the last name of Smith.  The result of groupBy is a Multimap.  A Multimap can be thought of as roughly equivalent to Map<K, Iterable<V>>.  There are many specializations of Multimap in GS Collections (List, Set, Bag, SortedBag, SortedSet) and there are both Mutable and Immutable forms.  In this example, I only use the parent type called Multimap, but I could have specialized it to use ListMultimap or MutableListMultimap.

Example 13: Grouping a collection based on a function which returns multiple keys

Use 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()
    );
}

Similar to flatCollect, the method groupByEach takes a Function<? super T, ? extends Iterable<V>>.  The Multimap<K, T> that is returned is effectively a multi-index, where every value can have multiple keys. In this example, we group people by the types of pets they have.  A person can have more than one pet, which is why “Bob” shows up in both Strings.  I used collect to convert each Person to their first name and then used makeString to convert it to a String of comma separated values.  There are overloads of makeString which will take a separator as a parameter as well as a start and end character.

Example 14: Sum the value of a primitive attribute returned by a method

Use one of the four sumOf methods available on RichIterable: sumOfInt, sumOfFloat, sumOfLong and sumOfDouble. 

@Test
public void getTotalNumberOfPets()
{
    long numberOfPets = this.people.sumOfInt(Person::getNumberOfPets);
    Assert.assertEquals(9, numberOfPets);
}

In the above example, we sum the value of the number of pets each person has, which gives us the total number of pets for all people.  For summing ints and floats, a wider type of long or double is returned.  The method sumOfInt takes a specialized form of Function called IntFunction. 

public interface IntFunction<T>
        extends Serializable
{
    int intValueOf(T anObject);
}

All Procedures, Functions, and Predicates in GS Collections extend Serializable.  This allows them to be safely serialized to disk or sent across the wire remotely without needing developers to create their own Serializable extensions.

Example 15: Fluency between object collections and primitive collections

If you want to convert from an object collection to a primitive collection, you can use one of the 8 specialized primitive forms of collect (collectInt/Float/Long/Double/Byte/Short/Char/Boolean).  A primitive collection can be converted back to an object collection by just using collect.  This capability allows for a lot of fluency when using the 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());
}

In this example, I answer some questions about the ages of the pets owned by the list of people.  First, I use the method asLazy(). This was a conscious decision I made since I wanted to reduce the amount of transient collections.  I could remove the asLazy() call and the code would still work.  In this case, asLazy() is merely a memory optimization.  Next, I call flatCollect, which gathers all the people’s Pets into a single flattened collection.  Then I call collectInt, which converts the LazyIterable<Pet> to an IntIterable.  Each Pet is converted to its age using the getAge method.  If I had not used the asLazy method earlier, I would have been converting a MutableList<Pet> to an IntList.  Finally, I call toSortedList, which takes the IntIterable and converts it to an IntList and sorts the ints.  I then call toSet on the IntList and store the unique set of int ages.

From there I demonstrate the richness of our primitive collections in GS Collections.  Statistical methods like min, max, sum, average, median are available directly on the collections.  I also leverage one of the new statistics classes in Java 8 named IntSummaryStatistics with the IntList by using a method reference to IntSumaryStatistics::accept which takes an int.  I also show methods you have seen in the earlier examples like any/all/noneSatisfy, only they are now being used with a primitive collection.

Example 16: Counting the number of occurrences of an item in a collection

If you want to quickly get a count of a number of items, you can convert a collection to a Bag.  A Bag is roughly equivalent to a Map<K, Integer> where the Integer is the count of the number of occurrences of the item K.  A Bag looks like any other collection in that it supports add, remove, etc.  It also has specialized methods for dealing efficiently with querying, adding or removing occurrences of an item. A Bag is like a Set which allows duplicates and keeps a count of occurrences.

@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));
}

There is no equivalent for Bag in the JDK today, just as there is no equivalent to PartitionIterable or Multimap.  In the Java 8 methods on the Collectors class, these types are simulated using Map<K, Integer> (Collectors.counting), Map<Boolean, List<V> (Collectors.partitioning), and Map<K, List<V>> (Collectors.groupingBy). 

Example 17: Counting the number of occurrences of a primitive value in a collection

If you want to get the count of a primitive value you can use a 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));
}

In this example, I create an IntBag from the ages of all of the pets.  This allows me to use the occurrencesOf method on IntBag to find the counts for each age.  This is the last of the examples in part 2.

These examples are meant to give you a little sample of what you can accomplish with the API in GS Collections.  There are over one hundred methods available on RichIterable today.  This provides an extremely rich set of features for Java developers when handling collections.

At JavaOne 2014, Craig Motlin and I covered several examples comparing how to do things in Java 8 with Streams vs. how to do things using GS Collections in a session titled: “GS Collections and Java8: Functional, Fluent, Friendly and Fun!”  The slides from the talk are available at the JavaOne 2014 site and can also be found on the GS Collections GitHub wiki.   

For reference, here is the code I used for the test setup and domain classes.

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
    }
}

About the Author

Donald Raab manages the JVM Architecture team, which is part of the Enterprise Platforms group in the Technology division at Goldman Sachs. Raab served as a member of the JSR 335 Expert Group (Lambda Expressions for the Java Programming Language) and is one of the alternate representatives for Goldman Sachs on the JCP Executive Committee. He joined Goldman Sachs in 2001 as a technical architect on the PARA team. He was named technology fellow in 2007 and managing director in 2013.

For more information about GS Collections and Goldman Sachs Engineering visit this link.

Disclaimer

This article reflects information available to the Technology Division of Goldman Sachs only and not any other part of Goldman Sachs. It should not be relied upon or considered investment advice. Opinions expressed may not be those of Goldman Sachs unless otherwise expressly noted. Goldman, Sachs & Co. (“GS”) does not warrant or guarantee the accuracy, completeness or efficacy of this article, and recipients should not rely on it except at their own risk. This article may not be forwarded or otherwise disclosed except with this disclaimer intact.

Rate this Article

Adoption
Style

Hello stranger!

You need to Register an InfoQ account or or login to post comments. But there's so much more behind being registered.

Get the most out of the InfoQ experience.

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Community comments

  • Very fun

    by Eric Fruttero,

    Your message is awaiting moderation. Thank you for participating in the discussion.

    to use with Groovy closure coercion.
    Vive Smalltalk ! :)
    (and thanks)

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

Allowed html: a,b,br,blockquote,i,li,pre,u,ul,p

BT