Java collection framework is not that powerful as experienced Java developer would expect.
For example, how do you sort a list?
Simple answer would be to use java.util.Collections.sort() method with some kind of java.util.Comparator implementation. Additionally Guava Ordering support can be used.
However, the solution is not exactly what object oriented developer looks for.
Similarly to sorting a collection you would probably deal with finding min or max element in a collection using java.util.Collections.min() and java.util.Collections.max() methods respectively.
After all how to filter a collection? Or how to select a list of particular property extracted from the objects stored in the collection? It can be done in pure Java using a for loop, using Apache Commons Collections and its CollectionUtils.filter(), CollectionUtils.collect() or Guava Collections2.filter(). Nonetheless, still none of those solutions is fully satisfying from my point of view.
Of course, there is Java 8 in the game, but it is a quite new release that cannot be used in every project, especially in legacy one and its collection framework is still not optimal.
As a rescue for the above problems the Goldman Sachs Collections (GS Collections) framework comes in. It is a collection framework that Goldman Sachs open sourced in January 2012.
Here is quick feature overview of GS Collections comparing to Java 8, Guava, Trove and Scala:
Seeing this, even if you thought that Java 8 had everything you need from collections, you still should have a look at GS Collections.
Following this brief introduction I am going to present a quick overview of the main features GS Collections has to offer. Some of the examples are variants of the exercises in the GS Collections Kata which is a training class they use in Goldman Sachs to train developers how to use GS Collections. The training is also open sourced as a separate repository.
Going back to the example from the beginning of the post, it would be perfect if we have methods like sort(), min(), max(), select(), collect(), etc. on every collection. It is simple to put them in a util class but it does not reflect the object oriented design.
GS Collections has an interfaces accomplishing this in the following way (as an example):
public interface MutableList<T> extends List<T>{ MutableList<T> sortThis(Comparator<? super T> comparator); <V> MutableList<V> collect(Function<? super T, ? extends V> function); MutableList<T> select(Predicate<? super T> predicate); ... }
GS Collections classes do not extend Java Collection Framework classes. They are instead new implementation of both Java Collection Framework and GS Collections interfaces.
Collect pattern
The collect patterns returns a new collection where each element has been transformed. An example can be a case when we need to return price of each item in the shopping cart.
MutableList<Customer> customers = company.getCustomers(); MutableList<String> customerCities = customers.collect(new Function<Customer, String>() { @Override public String valueOf(Customer customer) { return customer.getCity(); } });
or using Java 8 lambda expressions:
MutableList<Customer> customers = company.getCustomers(); MutableList<String> customerCities = customers.collect(customer->customer.getCity());
MutableList<String>customerCities=customers.collect(Customer::getCity);
Select pattern
The select pattern (aka filter) returns the elements of a collection that satisfy some condition. For example select only those customers who live in London. The pattern uses predicate which is a type taking an object and returning a boolean.
MutableList<Customer> customers = company.getCustomers(); MutableList<Customer> customersFromLondon = customers.select(new Predicate<Customer>() { @Override public boolean accept(Customer each) { return each.getCity().equalsIgnoreCase("London"); } });
or using Java 8 lambda expressions:
MutableList<Customer> customers = this.company.getCustomers(); MutableList<Customer> customersFromLondon = customers.select( each -> each.getCity().equalsIgnoreCase("London"));
Reject pattern
MutableList<Customer> customersNotFromLondon = this.company.getCustomers() .reject(new Predicate<Customer>() { @Override public boolean accept(Customer each) { return each.getCity().equalsIgnoreCase("London"); } });
MutableList<Customer> customersNotFromLondon = this.company.getCustomers() .reject(Customer.CUSTOMERS_FROM_LONDON);
Other patterns using Predicate
- Count pattern
- Returns the number of elements that satisfy the Predicate.
- Detect pattern
- Finds the first element that satisfies the Predicate.
- Any Satisfy
- Returns true if any element satisfies the Predicate.
- All Satisfy
- Returns true if all elements satisfy the Predicate.
Testing
Assert.assertEquals(2, customersFromLondon.size());
Verify.assertSize(2, customersFromLondon) MutableList<Integer> list = FastList.newListWith(1, 2, 0, -1); Verify.assertAllSatisfy(list, IntegerPredicates.isPositive());
Verify.assertEmpty(customersFromLondon); Verify.assertNotEmpty(customersFromLondon); Verify.assertContains(customer, customersFromLondon); Verify.assertContainsAll(customersFromLondon, customer1, customer2, customer3);
Predicates
MutableList<Integer> mutableList = FastList.newListWith(25, 50, 75, 100); MutableList<Integer> selected = mutableList.select(Predicates.greaterThan(50)); MutableList<Person> theLondoners = people.select( Predicates.attributeEqual( Person::getCity, "London"));
Immutability
MutableList<Integer> list = FastList.newListWith(3, 1, 2, 2, 1); MutableList<Integer> noDuplicates = list.toSet().toSortedList();
ImmutableList<Integer> immutableList = FastList.newListWith(1, 2, 3).toImmutable(); ImmutableList<Integer> immutableList2 = Lists.immutable.of(1, 2, 3);
Flat collect
company.getCustomers.flatCollect(Customer::getOrders);
company.getCustomers().flatCollect(new Function<Customer, Iterable<Order>>() { @Override public Iterable<Order> valueOf(Customer customer) { return customer.getOrders(); } });
Static utilities
List<Integer> list = ...; MutableList<Integer> selected = ListIterate.select(list, Predicates.greaterThan(50));
Integer[] array = ...; MutableList<Integer> selected = ArrayIterate.select(array, Predicates.greaterThan(50));
String result= StringIterate.select( "1a2a3", CharPredicate.IS_DIGIT); Assert.assertEquals("123", result);
Parralel iteration
List<Integer> list = ...; Collection<Integer> selected = ParallelIterate.select(list, Predicates.greaterThan(50));
FastList as a replacement for ArrayList
FastList is considered a drop-in replacement for ArrayList. It is definitely more memory efficient and can be used to refactor legacy code in steps.
List<Integer> integers = new ArrayList<>(); integers.add(1); integers.add(2); integers.add(3);
List<Integer> integers = new FastList<Integer>(); integers.add(1); integers.add(2); integers.add(3);
List<Integer> integers = FastList.newList(); integers.add(1); integers.add(2); integers.add(3);
List<Integer> integers = FastList.newListWith(1, 2, 3);
List<Integer> integers = FastList.newListWith(1, 2, 3).asUnmodifable();
MutableList<Integer> integers = FastList.newListWith(1, 2, 3);
UnifiedMap<Integer, String> map = UnifiedMap.newWithKeysValues( 1, "1", 2, "2", 3, "3");
Parallel lazy evaluation
MutableList<Item> data = ...; ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(); ParallelListIterable<Item> itemsLazy = FastList.newList(data).asParallel(executorService, 50000); itemsLazy.forEach(Item::calculateNewPrice);
asParallel() method takes two parameters:
- executorService
- batchSize which determines the number of elements from the backing collection that get processed by each task submitted to the thread pool; from my experience the appropriate batch size has significant influence on performance and should be determined during performance tests
Performance