Pontos Principais
- Foi desenvolvido internamente na Goldman Sachs por 10 anos antes de ser tornar código aberto em 2012 como GS Collections. Em 2015, foi migrada para a fundação Eclipse;
- O Eclipse Collections é um framework de coleções do Java de alto desempenho, redesenhado para além do Java 8, adicionando ricas funcionalidades para o framework Collections do Java;
- Utiliza estrutura de dados primitivos que executam muito melhor do que as coleções primitivas tradicionais;
- As versões anteriores a 8.0 da Eclipse Collections, são apenas compatíveis com as versões 5 - 7. A versão 8.0 precisa do Java 8 ou superior para utilizar;
- Optional para lidar com valores potencialmente nulos;
- A versão mais recente também foi atualizada para suportar módulos no Java 9.
Introdução em 30 segundos - O que é Eclipse Collections?
O Eclipse Collections é uma troca sem impactos do framework Collections do Java. É compatível com as implementações List, Set e Map do JDK, mas com uma API rica, e também possui estruturas adicionais não-encontrados no JDK como Bags, Multimaps e BiMaps. O Eclipse Collections também possui um complemento robusto de contêineres primitivos. Foi desenvolvido internamente na Goldman Sachs por 10 anos antes de ser tornar código aberto em 2012 como GS Collections. Em 2015, foi migrado para a fundação Eclipse e, desde então, todo o desenvolvimento ativo para o framework foi feito sobre o nome e repositório Eclipse Collections. Se preferir uma boa introdução na literatura, veja o artigo "GS Collections Por Exemplo" os artigos da InfoQ de Donald Raab, parte I e parte II.
Domínio
Antes de entrarmos em qualquer detalhe ou em exemplos de códigos, vamos falar sobre o domínio que usaremos neste artigo para nossos trechos de código, apresentado a seguir:
(Click on the image to enlarge it)
Temos uma lista de pessoas (tipo Person), cada pessoa tem uma lista de animais (Pets), e cada pet é de um certo enum de tipos (PetType).
Versão 8 para Java 8
Antes do lançamento do Eclipse Collections, o EC era compatível com as versões 5 - 7. Também poderia ser usado no Java 8 e melhorado com lambdas e method references ao usar a API rica, e de fato funcionou muito bem.
Mas isso é tudo. O Eclipse Collections era compatível com Java 8, mas ele não usou e nem abraçou isso. Agora, iniciando com Eclipse Collections, tomamos a decisão de projetar para ser compatível com Java 8+ para alavancar o uso de alguns dos recursos legais do Java 8 em nosso próprio código base.
Optional
O Optional é um dos novos recursos mais populares para o Java 8. No Javadoc consta "Um objeto contêiner que pode ou não conter um valor non-null. Se um valor está presente, isPresent() retornará true e get() retornará o valor". Basicamente, o Optional ajuda a proteger de NullPointerExceptions forçando a lidar com itens potencialmente nulos. Então, como isto pode ser utilizado no Eclipse Collections?
RichIterable.detectWith() se encaixa perfeitamente. O detectWith aceita um argumento Predicate e retorna o primeiro elemento da coleção que satisfaça a condição. Se não for encontrado nenhum elemento, o retorno é null. Assim, na versão 8.0 foi introduzido o detectWithOptional(). Ao invés de retornar o elemento ou nulo, retorna um Optional que é deixado para o usuário manipular. Veja o código de exemplo a seguir (retirado dos materiais de tutorial):
Person person = this.people.detectWith(Person::named, "Mary Smith");
//null pointer exception
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());
Aqui, queremos encontrar Mary Smith. Quando é feita a chamada no detectWith, a person é definida como nula, pois não foi possível encontrar nada que satisfaça as condições do Predicate. Assim, o código lança um NullPointerException.
Person person = this.people.detectWith(Person::named, "Mary Smith");
if (person == null)
{
person = new Person("Mary", "Smith");
}
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());
No próximo, antes do Java 8, poderíamos usar uma verificação nula como visto no código anterior. Mas o Java 8 oferece um Optional, então vamos usá-lo!
Optional<Person> optional =
this.people.detectWithOptional(Person::named, "Mary Smith");
Person person = optional.orElseGet(() -> new Person("Mary", "Smith"));
Assert.assertEquals("Mary", person.getFirstName());
Assert.assertEquals("Smith", person.getLastName());
Aqui, ao invés de retornar null, o detectWithOptional retornará um wrapper Optional em cima do Person. E agora é o momento de decidir de como lidar com este potencial valor nulo. Neste código está sendo realizado a chamada ao orElseGet() para criar uma instância de Person se ela for nula. O teste passa e são evitadas quaisquer exceptions!
Coletores
Quando utilizarmos streams no código, provavelmente utilizamos Collector também. Um colector é uma forma para implementar uma operação de redução mutável. Por exemplo, o Collectors.toList() permite acumular itens vindos do stream para uma lista. O JDK tem diversos Collectors "embutidos" que podem ser encontrados na classe Collectors. Veja alguns exemplos de códigos Java 8 (no Eclipse Collections).
List<String> names = this.people.stream()
.map(Person::getFirstName)
.collect(Collectors.toList());
// Output:
// [Bob, Ted, Jake]
int total = this.people.stream().collect(
Collectors.summingInt(Person::getNumberOfPets));
// Output:
// 4
Agora, uma vez que podemos aproveitar os streams usando o Eclipse Collections, deveríamos ter nosso próprio Collectors embutido também - o Collectors2. Muitos desses coletores são para estruturas de dados específicas do Eclipse Collections; recursos que não podem ser obtidos de fora do JDK - como toBag(), toImmutableSet (), etc.
(Clique para ampliar a imagem)
Este gráfico arranha a superfície da API Collectors2. As caixas de cima são todas de estruturas de dados diferentes, na qual os resultados podem ser adicionados no Collectors2, e os itens abaixo são algumas das diferentes API disponíveis para fazer. Existe um tipo de suporte para JDK como para estrutura de dados do Eclipse Collections, e também para coleções primitivas. Também é possível utilizar nossas familiares collect(), select(), reject(), entre outras, API via Collectors2.
Existe a possibilidade da interoperabilidade entre Collectors e Collectors2; os dois não são mutuamente exclusivos. Veja no exemplo a seguir, a utilização do JDK 8 Collectors, mas depois utilizando EC 8.0 Collectors2 por conveniência:
Map<Integer, String> people = this.people
.stream()
.collect(Collectors.groupingBy(
Person::getNumberOfPets,
Collectors2.makeString()));
Map<Integer, String> people2 = this.people
.stream()
.collect(Collectors.groupingBy(
Person::getNumberOfPets,
Collectors.mapping(
Object::toString,
Collectors.joining(","))));
// Output: {1=Ted, Jake, 2=Bob}
As duas partes do código produzem a mesma saída, mas observe uma diferença sutil: o Eclipse Collections oferece a funcionalidade makeString(), que cria uma coleção, separada por vírgula, de elementos representados como um String. Para fazer isto utilizando Java 8 é necessário um pouco mais de trabalho chamando o Collectors.mapping(), transformando cada objeto em seu valor toString e juntando com uma vírgula.
Métodos default
Para um framework como o Eclipse Collections, os métodos padrão são uma ótima adição ao JDK. A novas APIs podem ser implementada em algumas interfaces mais altas sem ter que mudar muitas das implementações abaixo. O reduceInPlace() é um dos novos métodos adicionados para RichIterable - mas o que isso faz?
/**
* This method produces the equivalent result as
* {@link Stream#collect(Collector)}.
* <p>
* <pre>
* MutableObjectLongMap<Integer> map2 =
* Lists.mutable
.with(1, 2, 3, 4, 5)
.reduceInPlace(
Collectors2.sumByInt(
i -> Integer.valueOf(i % 2), Integer::intValue));
* </pre>
* @since 8.0
*/
default <R, A> R reduceInPlace(Collector<? super T, A, R> collector)
{
A mutableResult = collector.supplier().get();
BiConsumer<A, ? super T> accumulator = collector.accumulator();
this.each(each -> accumulator.accept(mutableResult, each));
return collector.finisher().apply(mutableResult);
}
Usar o reduceInPlace é o equivalente a usar o Collector na stream. Mas por que precisamos adicionar isso ao Eclipse Collections? A razão é muito interessante; uma vez na API imutável ou Lazy que a Eclipse Collections oferece, não se pode mais usar a API de streaming. Neste ponto, não se pode ter as mesmas funcionalidades que se pode ter utilizando Collectors, porque o stream() não está mais disponível para nenhuma chamada subsequente da API; é neste momento que o reduceInPlace entra no jogo.
No código a seguir, uma vez que é chamado o .toImmutable() ou .asLazy() em um coleção, não se pode mais chamar o .stream(). Então, se precisar aprimorar os coletores, agora é possível utilizar o .reduceInPlace() e obter o mesmo resultado.
Coleções Primitivas
Temos o benefício das coleções primitivas deste o GS Collections 3.0. O Eclipse Collections adiciona otimizações de memória nas coleções para todos os tipos primitivos, com interfaces parecidas com os tipos Object bem como a simetria entre os tipos primitivos.
(Clique para ampliar a imagem)
Como mostrado no gráfico, existem diversos benefícios na utilização de coleções primitivas. A economia de memória é grande, podendo evitar o boxing nos tipos primitivos. Começando com o Java 8, existem três tipos primitivos (int, long e double) que utilizam streams primitivos especializados e expressões lambdas. No Eclipse Collections, é oferecida uma API direta com todos os 8 tipos primitivos. A seguir veremos alguns exemplos.
Streams - como o Iterator
IntStream stream = IntStream.of(1, 2, 3);
Assert.assertEquals(1, stream.min().getAsInt());
Assert.assertEquals(3, stream.max().getAsInt());
java.lang.IllegalStateException: stream has already been operated upon or closed
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:229)
at java.base/java.util.stream.IntPipeline.reduce(IntPipeline.java:474)
at java.base/java.util.stream.IntPipeline.max(IntPipeline.java:437)
LazyIntIterable lazy = IntLists.mutable.with(1, 2, 3).asLazy();
Assert.assertEquals(1, lazy.min());
Assert.assertEquals(3, lazy.max()); //reuse!
No código anterior, foi criado um IntStream de 1, 2 e 3, tentando chamar min() e max(). No Java 8 stream são como um iterator que não pode ser reutilizado. As LazyIterables do Eclipse Collections permitem reuso. Vamos ver um exemplo mais complexo:
List<Map.Entry<Integer, Long>> counts = this.people.stream().flatMap(
person -> person.getPets().stream())
.collect(Collectors.groupingBy(Pet::getAge, Collectors.counting()))
.entrySet()
.stream()
.filter(e -> e.getValue().equals(Long.valueOf(1)))
.collect(Collectors.toList());
// Output: [3=1, 4=1]
MutableIntBag counts2 = this.people.asLazy()
.flatCollect(Person::getPets)
.collectInt(Pet::getAge)
.toBag()
.selectByOccurrences(IntPredicates.equal(1));
// Output: [3, 4]
Neste exemplo, foi filtrada a idade dos animais somente uma vez. Como o Java 8 não tem a estrutura de dados Bag (que mapeia itens para suas contagens), deve-se agrupar as coleções por contagem dentro do mapa. Observe que uma vez chamado o collectInt() nos animais, está sendo passado para coleções de API primitivas. Quando é feita a chamada .toBag(), obtém uma IntBag.selectByOccurrences() especializada primitiva que é uma API específica de Bag que permite filtrar itens com base nos números de ocorrências que eles possuem.
Java 9 - O que vem pela frente?
Como conhecemos, o Java 9 introduz muitas mudanças interessantes no ecossistema Java, como o novo sistema de módulos e encapsulamento interno de API. O Eclipse Collections precisava ser alterado para serem compatíveis.
No release 8.2, foi modificado qualquer método que utiliza reflection para obter a construção do projeto. Pode ser visto como exemplo o ArrayListIterate a seguir:
public final class ArrayListIterate
{
private static final Field ELEMENT_DATA_FIELD;
private static final Field SIZE_FIELD;
private static final int MIN_DIRECT_ARRAY_ACCESS_SIZE = 100;
static
{
Field data = null;
Field size = null;
try
{
data = ArrayList.class.getDeclaredField("elementData");
size = ArrayList.class.getDeclaredField("size");
data.setAccessible(true);
size.setAccessible(true);
}
catch (Exception ignored)
{
data = null;
size = null;
}
ELEMENT_DATA_FIELD = data;
SIZE_FIELD = size;
}
Neste exemplo, uma vez que o data.setAccessible(true) é chamado, uma exception pode ser lançada. Como solução provisória, foi definido null para data e size para simplificar e continuar em frente. Infelizmente não poderá utilizar estes campos para otimizar qualquer padrão de iteração, mas com o Java 9 o problema de reflexão foi resolvido.
Existem soluções paliativas para reflexão nos código que não estão preparados para serem migrados. Podemos evitar que exceções sejam lançadas ao adicionar um argumento na linha de comando, contudo, como um framework, não queríamos colocar este peso nos usuários. Todos os problemas de reflexão foram resolvidos proativamente para que já possa codificar no Java 9!
Conclusão
O Eclipse Collections continua crescendo e evoluindo com a constante mudança no Java. Se ainda não está por dentro, faça testes com seu código do Java 8 e veja os novos recursos descritos! Se for novo no framework, na lista a seguir temos alguns links para indicar:
Também recomendamos dar uma olhada no vídeo completo em A evolução do Java no Collections Eclipse.