Eu sou um desenvolvedor Java, Tech Fellow e Managing Director no Goldman Sachs. Sou o criador do framework GS Collections, que teve o código aberto pelo Goldman Sachs em Janeiro de 2012. Também já fui um desenvolvedor Smalltalk.
Quando comecei a trabalhar com Java, sentia falta de duas coisas:
1. Block closures do Smalltalk (também conhecido por lambdas)
2. O Collections Framework do Smalltalk, maravilhosamente rico em funcionalidades.
Queria ter ambas funcionalidades, bem como compatibilidade com as interfaces já existentes do Java Collections. Por volta de 2004 percebi que ninguém me daria tudo que eu procurava no Java. Naquela época também sabia que provavelmente estaria programando em Java por ao menos os próximos 10 a 15 anos da minha carreira. Então decidi construir aquilo de que precisava.
Avançando o calendário em 10 anos, agora tenho quase tudo que sempre quis em Java. Tenho suporte para lambdas no Java 8, e posso utilizar lambdas e referências a métodos com, possivelmente, o framework Java collections mais rico em funcionalidades disponível - GS Collections.
Aqui está uma comparação das funcionalidades disponíveis em GS Collections, Java 8, Guava, Trove e Scala. Estas podem não ser todas as funcionalidades que você está procurando em um framework de Collections, mas são as que eu, bem como outros desenvolvedores do GS com quem trabalhei, precisamos pelos últimos 10 anos em Java.
Features |
GSC 5.0 |
Java 8 |
Guava |
Trove |
Scala |
Rich API |
✓ |
✓ |
✓ |
|
✓ |
Interfaces |
Readable, Mutable, Immutable, FixedSize, Lazy |
Mutable, Stream |
Mutable, Fluent |
Mutable |
Readable, Mutable, Immutable, Lazy |
Optimized Set / Map |
✓ (+Bag) |
|
|
✓ |
|
Immutable Collections |
✓ |
|
✓ |
|
✓ |
Primitive Collections |
✓ (+Bag, +Immutable) |
|
|
✓ |
|
Multimaps |
✓ (+Bag, +SortedBag) |
|
✓ (+Linked) |
|
(Multimap trait) |
Bags (Multisets) |
✓ |
|
✓ |
|
|
BiMaps |
✓ |
|
✓ |
|
|
Iteration Styles |
Eager/Lazy, Serial/Parallel |
Lazy, Serial/Parallel |
Lazy, Serial |
Eager, Serial |
Eager/Lazy, Serial/Parallel (Lazy Only) |
Descrevi a combinação de funcionalidades que torna o GS Collections interessante em uma entrevista no jClarity ano passado. Você pode encontrá-la em seu formato original neste link.
Por que você utilizaria GS Collections agora que o Java 8 está aí e inclui a API de Streams? Apesar de ser uma grande melhoria para o Java Collections Framework, a API de Streams não possui todas as funcionalidades das quais você pode precisar. Conforme mostra a tabela acima, GS Collections possui multimaps, bags, contêiners imutáveis e contêiners primitivos. GS Collections possui substitutos otimizados para HashSet e HashMap, e suas interfaces Bags e Multimaps são implementadas usando estes tipos otimizados. Os padrões de iteração do GS Collections já estão nas interfaces das coleções, então não há necessidade de "entrar" na API com uma chamada a stream() e "sair" da API com uma chamada a collect(). Isso resulta em um código muito mais sucinto em muitos casos. Por fim, GS Collections é compatível até com Java 5. Esta é uma característica particularmente importante para desenvolvedores de bibliotecas, já que tendem a suportar suas bibliotecas em versões mais antigas do Java, mesmo bem depois de uma nova versão ter sido lançada.
Irei demonstrar em alguns exemplos de como você pode se beneficiar destas funcionalidades em diferentes maneiras. Estes exemplos são variações dos exercícios contidos no GS Collections Kata, um curso de treinamento que utilizamos no Goldman Sachs para ensinar a nossos desenvolvedores como utilizar o GS Collections. Disponibilizamos este treinamento em código aberto como um repositório no GitHub.
Exemplo 1: Filtrando uma coleção
Uma das coisas mais comuns que você deseja com GS Collections é filtrar a coleção. GS Collections provê diversas formas de conseguir isto.
No GS Collections Kata, frequentemente começaremos com uma lista de clientes. Em um dos exercícios, quero filtrar a lista de clientes a uma sub-lista que contenha apenas aqueles que morem em Londres. O código a seguir mostra como consigo isto usando um padrão de iteração chamado "select".
import com.gs.collections.api.list.MutableList; import com.gs.collections.impl.test.Verify; @Test public void getLondonCustomers() { MutableList<Customer> customers = this.company.getCustomers(); MutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Verify.assertSize("Should be 2 London customers", 2, londonCustomers); }
O método select em MutableList retorna uma MutableList. Este código executa antecipadamente (eager evaluation), o que significa que toda a computação para selecionar da lista de origem os elementos compatíveis e adicioná-los à lista resultante já foi executada quando a chamada a select() termina. O nome "select" é uma herança do Smalltalk. Smalltalk possui um conjunto básico de protocolos de coleções chamados select (também conhecido como filter), reject (também conhecido como filterNot), collect (também conhecido como map, transform), detect (também conhecido como findOne), detectIfNone, injectInto (também conhecido como foldLeft), anySatisfy e allSatisfy.
Se quisesse atingir o mesmo resultado utilizando avaliação postergada (lazy evaluation), eu escreveria da seguinte forma:
MutableList<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
Neste exemplo eu adicionei uma chamada ao método asLazy(). Todo o resto do código permaneceu inalterado. O tipo de retorno do método select mudou devido à chamada ao asLazy(). Ao invés de MutableList<Customer> agora recebo de volta um LazyIterable<Customer>. Isto é praticamente o equivalente ao código a seguir, usando a nova Streams API do Java 8:
List<Customer> customers = this.company.getCustomers(); Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); List<Customer> londonCustomers = stream.collect(Collectors.toList()); Verify.assertSize(2, londonCustomers);
Aqui o método stream() e então a chamada a filter() retornam um Stream<Customer>. Para conseguir verificar o tamanho, preciso ou converter o Stream para um List, conforme o trecho de código anterior, ou posso usar o método Stream.count() do Java 8:
List<Customer> customers = this.company.getCustomers(); Stream<Customer> stream = customers.stream().filter(c -> c.livesIn("London")); Assert.assertEquals(2, stream.count());
Ambas interfaces MutableList e LazyIterable do GS Collections compartilham um ancestral em comum chamado RichIterable. De fato, eu poderia escrever todo este código usando somente RichIterable. Aqui está um exemplo usando somente RichIterable<Customer>, primeiro de forma lazy (postergada):
RichIterable<Customer> customers = this.company.getCustomers(); RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
e novamente, de forma eager (antecipada):
RichIterable<Customer> customers = this.company.getCustomers(); RichIterable<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
Conforme mostrado nestes exemplos, RichIterable pode ser usada no lugar de LazyIterable e MutableList, porque são derivadas da mesma interface.
É possível que a lista de clientes possa ser imutável. Se eu tivesse uma ImmutableList<Customer>, os tipos seriam alterados assim:
ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); ImmutableList<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Verify.assertIterableSize(2, londonCustomers);
Como outros RichIterables, podemos iterar sobre ImmutableList de forma lazy.
ImmutableList<Customer> customers = this.company.getCustomers().toImmutable(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
Existe uma interface pai comum para ambas MutableList e ImmutableList chamada ListIterable. Ela pode ser utilizada no lugar de qualquer uma das duas como um tipo mais geral. RichIterable é o tipo pai de ListIterable. Então este código também pode ser escrito de forma mais genérica, como a seguir:
ListIterable<Customer> customers = this.company.getCustomers().toImmutable(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
Ou ainda de forma mais genérica:
RichIterable<Customer> customers = this.company.getCustomers().toImmutable(); RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
A hierarquia de interfaces do GS Collections segue um padrão muito básico. Para cada tipo (List, Set, Bag, Map), existe uma interface de leitura (ListIterable, SetIterable, Bag, MapIterable), uma interface mutável (MutableList, MutableSet, MutableBag, MutableMap), e uma interface imutável (ImmutableList, ImmutableSet, ImmutableBag, ImmutableMap).
(Clique na imagem para ampliar)
Figura 1. Hierarquia Básica de Interfaces Containers GSC
Segue abaixo um exemplo do mesmo código usando um Set ao invés de List:
MutableSet<Customer> customers = this.company.getCustomers().toSet(); MutableSet<Customer> londonCustomers = customers.select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
Aqui uma solução similar escrita de forma lazy com um Set:
MutableSet<Customer> customers = this.company.getCustomers().toSet(); LazyIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
Aqui está uma solução com Set, utilizando a interface mais geral possível:
RichIterable<Customer> customers = this.company.getCustomers().toSet(); RichIterable<Customer> londonCustomers = customers.asLazy().select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
A seguir irei ilustrar os mecanismos que podem ser utilizados para converter de um tipo de container para outro. Primeiro, vamos converter de List para Set, filtrando de forma lazy:
MutableList<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> lazyIterable = customers.asLazy().select(c -> c.livesIn("London")); MutableSet<Customer> londonCustomers = lazyIterable.toSet(); Assert.assertEquals(2, londonCustomers.size());
Como a API é muito fluída, eu posso encadear todos estes métodos juntos:
MutableSet<Customer> londonCustomers = this.company.getCustomers() .asLazy() .select(c -> c.livesIn("London")) .toSet(); Assert.assertEquals(2, londonCustomers.size());
Deixarei para o leitor decidir se isto impacta ou não a legibilidade. Eu tenho a tendência de quebrar estas chamadas fluídas e introduzir tipos intermediários se acho que isto ajudará futuros leitores a entender melhor o código. Isto vem ao custo de mais código para ler, mas em compensação pode reduzir o custo do entendimento, o que pode ser mais importante para leitores menos frequentes do código.
Posso conseguir a conversão de List em Set no próprio método select. O método select tem uma forma sobrecarregada (overloaded) definida que recebe um Predicate como o primeiro parâmetro e a coleção de resultado como segundo parâmetro:
MutableSet<Customer> londonCustomers = this.company.getCustomers() .select(c -> c.livesIn("London"), UnifiedSet.newSet()); Assert.assertEquals(2, londonCustomers.size());
Note como posso usar este mesmo método para retornar qualquer tipo Collection que quiser. No caso a seguir, recebo de volta um MutableBag<Customer>:
MutableBag<Customer> londonCustomers = this.company.getCustomers() .select(c -> c.livesIn("London"), HashBag.newBag()); Assert.assertEquals(2, londonCustomers.size());
No caso a seguir, recebo de volta uma um CopyOnWriteArrayList, que é parte do JDK. O ponto é que este método irá retornar qualquer tipo que eu especificar, desde que seja uma classe que implemente java.util.Collection:
CopyOnWriteArrayList<Customer> londonCustomers = this.company.getCustomers() .select(c -> c.livesIn("London"), new CopyOnWriteArrayList<>()); Assert.assertEquals(2, londonCustomers.size());
Nós temos utilizado lambda em todos os exemplos. O método select recebe um Predicate, que é uma interface funcional em GS Collections definida da seguinte forma:
public interface Predicate<T> extends Serializable { boolean accept(T each); }
O lambda que tenho utilizado é razoavelmente simples. Irei extraí-lo em uma variável separada para que seja mais claro o que este lambda representa no código:
Predicate<Customer> predicate = c -> c.livesIn("London"); MutableList<Customer> londonCustomers = this.company.getCustomers().select(predicate); Assert.assertEquals(2, londonCustomers.size());
O método livesIn() em Customer é bem simples. Ele é definido conforme a seguir:
public boolean livesIn(String city) { return city.equals(this.city); }
Seria bom se eu pudesse utilizar uma referência de método ao invés de um lambda, aproveitando o método livesIn. Mas este código não irá compilar:
Predicate<Customer> predicate = Customer::livesIn;
O compilador retorna o seguinte erro:
Error:(65, 37) java: incompatible types: invalid method reference incompatible types: com.gs.collections.kata.Customer cannot be converted to java.lang.String
Isto porque esta referência de método iria necessitar de dois parâmetros: um Customer e o String da cidade. Existe uma forma alternativa de Predicate chamada Predicate2 que irá funcionar aqui.
Predicate2<Customer, String> predicate = Customer::livesIn;
Note que Predicate2 recebe dois tipos genéricos, Customer e String. Há uma forma especial de select chamada selectWith que pode usar Predicate2.
Predicate2<Customer, String> predicate = Customer::livesIn; MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(predicate, "London"); Assert.assertEquals(2, londonCustomers.size());
Isto pode ser simplificado como a seguir, incorporando a referência do método:
MutableList<Customer> londonCustomers = this.company.getCustomers().selectWith(Customer::livesIn, "London"); Assert.assertEquals(2, londonCustomers.size());
A String "London" é passada como o segundo parâmetro a cada chamada do método definido no Predicate2. O primeiro parâmetro será o Customer da lista.
O método selectWith, assim como select, é definido na RichIterable. Portanto, tudo que demonstrei também irá funcionar com selectWith. Isto inclui o suporte em todas as diferentes interfaces mutáveis e imutáveis, suporte a diferentes tipos covariantes, e suporte a iteração lazy. Existe também a forma de selectWith que recebe um terceiro parâmetro. Similar ao select com dois parâmetros, o terceiro parâmetro em selectWith pode receber uma coleção alvo.
Por exemplo, o código a seguir filtra uma List em um Set usando selectWith:
MutableSet<Customer> londonCustomers = this.company.getCustomers() .selectWith(Customer::livesIn, "London", UnifiedSet.newSet()); Assert.assertEquals(2, londonCustomers.size());
Isto também pode ser alcançado de forma lazy com o código a seguir:
MutableSet<Customer> londonCustomers = this.company.getCustomers() .asLazy() .selectWith(Customer::livesIn, "London") .toSet(); Assert.assertEquals(2, londonCustomers.size());
A última coisa que irei mostrar é que os métodos select e selectWith podem ser usados com qualquer coleção que herde de java.lang.Iterable. Isto inclui todos os tipos do JDK, assim como qualquer biblioteca de coleções desenvolvida por terceiros. A primeira classe que veio a existir em GS Collections foi uma classe utilitária chamada Iterate. A seguir temos um exemplo de código que mostra como executar um select em um Iterable utilizando Iterate.
Iterable<Customer> customers = this.company.getCustomers(); Collection<Customer> londonCustomers = Iterate.select(customers, c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
A variação selectWith também está disponível:
Iterable<Customer> customers = this.company.getCustomers(); Collection<Customer> londonCustomers = Iterate.selectWith(customers, Customer::livesIn, "London"); Assert.assertEquals(2, londonCustomers.size());
Há também variações que recebem a coleção alvo. Todos os protocolos de iteração básicos estão disponíveis em Iterate. Existe também uma classe utilitária que cobre iteração lazy (chamada LazyIterate), e ela também funciona com qualquer container que herde java.lang.Iterable. Por exemplo:
Iterable<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = LazyIterate.select(customers, c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
Um jeito melhor de conseguir isto usando uma API mais orientada a objetos é utilizar classes adaptadoras (adapter). Aqui está um exemplo utilizando um ListAdapter com um java.util.List:
List<Customer> customers = this.company.getCustomers(); MutableList<Customer> londonCustomers = ListAdapter.adapt(customers).select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
Isto pode ser escrito de forma lazy, como você já deve imaginar a essa altura:
List<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = ListAdapter.adapt(customers) .asLazy() .select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
O método selectWith() também irá funcionar de forma lazy em um ListAdapter:
List<Customer> customers = this.company.getCustomers(); LazyIterable<Customer> londonCustomers = ListAdapter.adapt(customers) .asLazy() .selectWith(Customer::livesIn, "London"); Assert.assertEquals(2, londonCustomers.size());
SetAdapter pode ser usado de forma similar para qualquer implementação de java.util.Set.
Agora se você tem o tipo de problema que pode se beneficiar de paralelismo ao nível de dados, você pode utilizar uma das duas abordagens para paralelizar este problema. Primeiro, irei demonstrar como utilizar a classe ParallelIterate para resolver este problema utilizando um algoritmo eager/paralelo:
Iterable<Customer> customers = this.company.getCustomers(); Collection<Customer> londonCustomers = ParallelIterate.select(customers, c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.size());
A classe ParallelIterate irá aceitar qualquer Iterable como parâmetro, e irá sempre retornar java.util.Collection como resultado. ParallelIterate existe no GS Collections desde 2005. Eager/parallel foi a única forma de paralelismo suportada no GS Collections até a versão 5.0, quando adicionamos uma API lazy/parallel a RichIterable. Nós não temos uma API eager/parallel em RichIterable, porque entendemos que lazy/parallel faz mais sentido como caso padrão. Poderemos adicionar uma API eager/parallel a RichIterable no futuro, dependendo do feedback recebido sobre a utilidade da API lazy/parallel.
Se eu quisesse resolver o mesmo problema utilizando a API lazy/parallel, iria escrever o código conforme a seguir:
FastList<Customer> customers = this.company.getCustomers(); ParallelIterable<Customer> londonCustomers = customers.asParallel(Executors.newFixedThreadPool(2), 100) .select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.toList().size());
Hoje, o método asParallel() somente existe em alguns contâiners concretos no GS Collections. A API ainda não foi promovida a nenhuma interface como MutableList, ListIterable ou RichIterable. O método asParallel() recebe dois parâmetros - um ExecutorService e o tamanho do lote de processamento (batch size). No futuro, poderemos adicionar uma versão do asParallel() que calcula o batch size automaticamente.
Eu poderia escolher utilizar um tipo mais específico neste exemplo:
FastList<Customer> customers = this.company.getCustomers(); ParallelListIterable<Customer> londonCustomers = customers.asParallel(Executors.newFixedThreadPool(2), 100) .select(c -> c.livesIn("London")); Assert.assertEquals(2, londonCustomers.toList().size());
Existe uma hierarquia de ParallelIterable que inclui ParallelListIterable, ParallelSetIterable e ParallelBagIterable.
Demonstrei diversas formas de filtrar uma coleção em GS Collections utilizando select() e selectWith(). Mostrei a vocês muitas combinações de iterações eager, lazy, serial e paralela, usando diferentes tipos da hierarquia RichIterable do GS Collections.
Na parte 2 deste artigo, irei abordar exemplos incluindo collect, groupBy, flatCollect, assim como alguns containers primitivos e a rica API também disponível. Os exemplos pelos quais passo na parte 2 não entram em tantos detalhes nem exploram tantas opções, mas vale ressaltar que estes detalhes e opções também estão disponíveis.
Sobre o Autor
Donald Raab gerencia o time de Arquitetura (JVM Architecture Team), que é parte do grupo de Plataformas Empresariais (Enterprise Platforms) da Divisão de Tecnologia no Goldman, Sachs / Co. ("Goldman Sachs"). Raab foi membro do JSR 335 Expert Group (Expressões Lambda para a Linguagem de Programação Java) e é um dos suplentes do Goldman Sachs no Comitê Executivo do JCP. Ele se juntou ao Goldman Sachs em 2001 como arquiteto técnico no time PARA. Foi nomeado "technology fellow" em 2007 e "managing director" em 2013.
Para mais informações sobre GS Collections e Engenharia no Goldman Sachs visite este link.
Aviso
Este artigo reflete informação referente somente à Divisão de Tecnologia do Goldman, Sachs / Co. ("Goldman Sachs") e de nenhuma outra divisão ou entidade afiliada do Goldman Sachs. Não deve ser interpretado nem considerado como aconselhamento de investimento. As opiniões no artigo não são do Goldman Sachs, a menos que expressamente especificadas. Goldman Sachs não se responsabiliza nem garante a exatidão, completitude ou eficácia deste artigo, e os destinatários não devem contar com isso exceto por própria conta e risco. Este artigo não pode ser encaminhado ou divulgado sem este aviso.