Pontos Principais
- O Eclipse Collections é uma estrutura de coleções de alto desempenho para Java, que adiciona funcionalidades ricas às coleções nativas do JDK;
- Embora os fluxos sejam uma adição bem-vinda ao JDK, eles ainda sofrem com muitos recursos ausentes, dependências da implementação de coleção legada e APIs detalhadas;
- O Eclipse Collections oferece substituições drop-in para as estruturas de dados tradicionais do JDK, bem como suporte para as estruturas de dados adicionais, como o Bag e o Multimap;
- Refatorar seus fluxos para o Eclipse Collections pode ajudar a melhorar a legibilidade do código e reduzir o espaço ocupado pela memória;
- E o melhor de tudo, a refatoração para o Eclipse Collections é fácil!
O Java Stream, introduzido no Java 8, é excelente - ele permite que aproveitemos ao máximo as expressões lambda para substituir código repetitivo por métodos que capturam padrões de interação comuns, resultando em um código mais funcional.
No entanto, mesmo os streams sendo uma melhoria em última análise, são apenas uma extensão da estrutura de coleta existente e, portanto, carregam um pouco da bagagem.
Podemos melhorar ainda mais as coisas? Podemos ter interfaces ainda mais ricas, além de um código mais limpo e legível? Podemos perceber uma economia de memória tangível em comparação com implementações de coleções legadas? Podemos ter um suporte melhor e mais uniforme para o paradigma de programação funcional?
A resposta é sim! O Eclipse Collections (anteriormente GS Collections), um substituto direto do framework Java Collections, fará exatamente isso.
Neste artigo, serão demonstrados vários exemplos de refatoração de código Java padrão para a estrutura de dados e APIS do Eclipse Collections, e também para demonstrar algumas das economias de memória possíveis de obter.
Haverá muitos exemplos de código, que mostrarão como alterar o código que usa coleções e fluxos Java padrão, para o código que usa a estrutura do Eclipse Collections.
Mas antes de nos aprofundarmos no código, gastamos algum tempo para entender o que é o Eclipse Collections, a razão de sua necessidade, e por que você pode querer refazer um trabalho funcionando em Java para o Eclipse Collections.
História do Eclipse Collections O Eclipse Collections foi inicialmente criado no Goldman Sachs para uma plataforma de aplicativos com um componente muito grande de cache distribuído. O sistema, ainda em produção, armazena centenas de gigabytes de dados na memória. Na verdade, um cache é simplesmente um mapa; nós armazenamos objetos lá e o tiramos. Esses objetos podem conter outros mapas e coleções. Inicialmente, o cache era baseado nas estruturas de dados padrão do pacote java.util.*. Mas logo ficou claro que essas coleções têm duas desvantagens significativas: uso ineficiente de memória e interfaces muito limitadas (uma síndrome que leva a códigos difíceis de ler e repetitivos). Como os problemas estavam enraizados na implementação da coleção não era possível corrigir o código de cache com as bibliotecas de utilitários. Para resolver esses dois problemas de uma só vez, foi tomada uma decisão na Goldman Sachs Engineering, de criar uma nova estrutura de Coleção totalmente do zero. Na época, parecia ser uma solução um pouco radical, mas funcionou. Agora, esse framework está sob a égide da Eclipse Foundation. No final do artigo, são compartilhados links que ajudarão a descobrir mais sobre o projeto em si e as maneiras de aprender a usar o Eclipse Collections e contribuir com ele. |
Por que refatorar para o Eclipse Collections?
Quais são os benefícios do Eclipse Collections? Graças às suas APIs ricas, uso eficiente de memória e melhor desempenho, o Eclipse Collections é a biblioteca de coleções mais rica para Java. Ele também é projetado para ser totalmente compatível com as coleções obtidas do JDK.
Fácil Migração
Antes de aprofundarmos nos benefícios, é importante observar que a mudança para o Eclipse Collections é fácil e não precisa ser feito tudo de uma única vez. O Eclipse Collections inclui implementações totalmente compatíveis com as interfaces JDK java.util.* List, Set e Map. Também é compatível com bibliotecas do JDK, como os Collectors. As estruturas de dados são herdadas das interfaces do JDK para esses tipos, portanto, são substitutos para contrapartes do JDK (com exceção de nossa interface Stack, que não é compatível, e de novas coleções primitivas e imutáveis, para as quais não há equivalentes no JDK).
APIs Ricas
As implementações do Eclipse Collections das interfaces java.util.List, Set e Map possuem APIs muito mais ricas, que serão exploradas posteriormente nos exemplos de código. Existem também tipos adicionais que não estão presentes no JDK, como o Bag, o Multimap e o BiMap. Um Bag é um multiset; um conjunto com elementos repetidos. Logicamente, pode-se pensar nisso como um mapa de itens para o número de suas ocorrências. BiMap é um mapa "invertido", onde se pode não apenas encontrar o valor por chave, mas também encontrar a chave por valor. Multimaps são mapas onde os próprios valores são Collections (Key -> List, Key -> Set, etc.).
Escolha por Eager ou Lazy
O Eclipse Collections permite alternar facilmente entre implementações lazy e eager nas collections, o que ajuda muito na gravação, compreensão e depuração de um código funcional Java. Ao contrário da API de fluxos, a avaliação rápida é o padrão. Se você quiser uma avaliação lazy, simplesmente escreva .asLazy()
em sua estrutura de dados antes de continuar escrevendo sua lógica.
Interfaces de Collections Imutáveis
As collections imutáveis permitem que possamos desenvolver um código mais correto impondo a imutabilidade no nível da API. A exatidão do programa neste caso será garantida pelo compilador, o que evitará surpresas durante sua execução. A combinação de collections imutáveis e interfaces mais ricas permite escrever um código puramente funcional em Java.
Collections de tipos primitivos
O Eclipse Collections também tem um acréscimo completo de contêineres primitivos, e todos os tipos primitivos de coleta tem equivalentes imutáveis. Também é importante notar que, embora os fluxos do JDK forneçam suporte a fluxos de int, long e double, o Eclipse Collections tem suporte para os oito tipos primitivos e permite às coleções que mantenham seus valores primitivos (em oposição aos objetos boxed, por exemplo, o Eclipse Collections IntList, uma lista de ints, versus o JDK List<Integer>, uma lista de valores boxed).
Nenhum método "Bun"
O que são métodos "bun"? Esta é uma analogia inventada por Brian Goetz, arquiteto lider na Oracle . Um hambúrguer (dois pães com carne no meio) representa a estrutura do código de fluxo típico. Usando fluxos Java, independentemente do que se deseja fazer, é necessário colocar seus métodos entre dois buns - o método stream() (ou parallelStream()) no início, e um método coletor no final. Estes pãezinhos são calorias vazias, que não são realmente necessários, mas sem eles não se pode chegar à carne. No Eclipse Collections, esses métodos não são necessários. Aqui está um exemplo de métodos bun na renderização do JDK: imagine que tenhamos uma lista de pessoas com seus nomes e idades, e queremos extrair os nomes das pessoas com mais de 21 anos:
var people = List.of(new Person("Alice", 19),
new Person("Bob", 52), new Person("Carol", 35));
var namesOver21 = people.stream() // Bun
.filter(person -> person.getAge() > 21) // Meat
.map(Person::getName) // Meat
.collect(Collectors.toList()); // Bun
namesOver21.forEach(System.out::println);
Este é o como o mesmo código ficaria com o Eclipse Collections - sem necessidade de "buns"!
var people = Lists.immutable.of(new Person(“Alice”, 19),
new Person(“Bob”, 52), new Person(“Carol”, 35));
var namesOver21 = people
.select(person -> person.getAge() > 21) // Meat, no buns
.collect(Person::getName); // Meat
namesOver21.forEach(System.out::println);
Quaisquer tipos necessários
No Eclipse Collections existem tipos e métodos para cada caso de uso, e eles são fáceis de encontrar, a partir dos recursos que se fazem necessários. Não é necessário memorizar seus nomes individuais - apenas pense em que tipo de estrutura deseja. A necessidade é de uma collection que seja mutável ou imutável? Classificado? Que tipo de dados queremos armazenar na collection - primitivo ou objeto? Que tipo de collection você precisa? Lazy, eager ou paralelo? Seguindo o gráfico da próxima seção, é muito fácil construir a estrutura de dados que precisamos.
Instanciar usando Factories
Isso é semelhante aos métodos de collections factory do Java 9 nas interfaces List, Set e Map, com ainda mais opções!
Métodos [apenas alguns] por categoria
APIs ricas estão disponíveis diretamente em cada tipo de collection, que herda da interface RichIterable (ou Primitivelterable no lado primitivo). Vamos dar uma olhada em algumas dessas APIs nos próximos exemplos.
Métodos - muito mais...
As nuvens de palavras - foram tudo há dois anos, não? No entanto, não são inteiramente gratuitas - isso ilustra alguns pontos importantes. Primeiro, há muitos métodos, cobrindo todos os padrões imagináveis de iteração, disponíveis diretamente nos tipos de coleção. Segundo, os tamanhos das palavras nessa nuvem são proporcionais aos números de implementações do método. Existem várias implementações de métodos em diferentes tipos de coleção otimizados para esses tipos específicos (portanto, não há métodos padrão de menor denominador comum aqui).
Exemplos de Códigos
Exemplo: Contagem de palavras
Vamos começar com algo simples.
Dado um texto (neste caso, uma cantiga infantil), contar o número de ocorrências de cada palavra no texto. O resultado é uma coleção de palavras e o correspondente número de ocorrências.
@BeforeClass
static public void loadData()
{
words = Lists.mutable.of((
"Bah, Bah, black sheep,\n" +
"Have you any wool?\n").split("[ ,\n?]+")
);
}
Observe que estamos usando um método do Eclipse Collections para preencher com palavras uma lista. Isso é equivalente ao método Arrays.asList(...) do JDK, mas retorna uma instância do Eclipse Collections MutableList
. Como a interface MutableList
é totalmente compatível com o List
do JDK, podemos usar esse tipo para os exemplos JDK e Eclipse Collections abaixo.
Primeiro, vamos considerar uma implementação ingênua, que não usa streams:
@Test
public void countJdkNaive()
{
Map<String, Integer> wordCount = new HashMap<>();
words.forEach(w -> {
int count = wordCount.getOrDefault(w, 0);
count++;
wordCount.put(w, count);
});
System.out.println(wordCount);
Assert.assertEquals(2, wordCount.get(“Bah”).intValue());
Assert.assertEquals(1, wordCount.get(“sheep”).intValue());
}
É possível observar que foi criado um novo HashMap de String para Integer (mapeando cada palavra para o número de suas ocorrências), percorrendo cada palavra e obtendo sua contagem do mapa ou padrão para 0 se a palavra ainda não estiver no mapa. Em seguida, foi incrementado o valor e então armazenado no mapa. Esta não é uma grande implementação, pois foi focada em "como" em vez de "o quê" do algoritmo, e o desempenho também não é grande. Tentando reescrevê-lo usando o código de fluxo idiomático:
@Test
public void countJdkStream()
{
Map<String, Long> wordCounts = words.stream()
.collect(Collectors.groupingBy(w -> w, Collectors.counting()));
Assert.assertEquals(2, wordCounts.get(“Bah”).intValue());
Assert.assertEquals(1, wordCounts.get(“sheep”).intValue());
}
Neste caso, o código é bastante legível, mas ainda não é realmente eficiente (que é possível confirmar executando microbenchmarks sobre ele). Também é preciso estar ciente dos métodos utilitários na classe Collectors - eles não são facilmente detectáveis, pois não estão disponíveis diretamente na API de fluxo.
Uma maneira de ter uma implementação realmente eficiente é introduzir uma classe de contador separada e armazená-la como um valor no mapa. Digamos que temos uma classe chamada Counter, que armazena um valor inteiro e tem um método increment(), que incrementa esse valor em 1. Então, podemos reescrever o código acima como:
@Test
public void countJdkEfficient()
{
Map<String, Counter> wordCounts = new HashMap<>();
words.forEach(
w -> {
Counter counter = wordCounts.computeIfAbsent(w, x -> new Counter());
counter.increment();
}
);
Assert.assertEquals(2, wordCounts.get(“Bah”).intValue());
Assert.assertEquals(1, wordCounts.get(“sheep”).intValue());
}
Esta é, de fato, uma solução muito eficiente, mas tivemos que escrever uma nova classe inteira (Counter) para que funcionasse.
O Eclipse Collections Bag oferece uma solução personalizada para esse problema e fornece uma implementação otimizada apenas para esse tipo específico de coleta.
@Test
public void countEc()
{
Bag<String> bagOfWords = wordList.toBag();
// toBag() is a method on MutableList
Assert.assertEquals(2, bagOfWords.occurrencesOf(“Bah”));
Assert.assertEquals(1, bagOfWords.occurrencesOf(“sheep”));
Assert.assertEquals(0, bagOfWords.occurrencesOf(“Cheburashka”));
// null safe - returns a zero instead of throwing an NPE
}
Tudo o que temos a fazer aqui é pegar nossa coleção e chamar toBag(). Também fomos capazes de evitar um possível NPE (Null Pointer Exception) em nossa última asserção, não chamando intValue() diretamente no objeto.
Exemplo: Zoológio
Digamos que administramos um zoológico. Em um zoológico, mantemos animais que comem diferentes tipos de comida.
Queremos consultar alguns fatos sobre os animais e os alimentos que eles comem:
- A maior quantidade de comida;
- Lista de animais e o número de suas comidas favoritas;
- Alimentos exclusivos;
- Tipos de comida;
- Encontrar os carnívoros e os não-carnívoros.
Esses trechos de código foram testados com a estrutura Java Microbenchmark Harness (JMH). Vamos percorrer os exemplos de código e, em seguida, dar uma olhada em como eles se comparam. Para uma comparação de desempenho, consulte a seção "Resultados de Benchmark da JMH" abaixo.
Aqui está o domínio - animais de zoológicos e os respectivos itens de comida que cada um gosta (cada item de comida tem um nome, categoria e quantidade).
private static final Food BANANA = new Food(“Banana”, FoodType.FRUIT, 50);
private static final Food APPLE = new Food(“Apple”, FoodType.FRUIT, 30);
private static final Food CAKE = new Food(“Cake”, FoodType.DESSERT, 22);
private static final Food CEREAL = new Food(“Cereal”, FoodType.DESSERT, 80);
private static final Food SPINACH = new Food(“Spinach”, FoodType.VEGETABLE, 26);
private static final Food CARROT = new Food(“Carrot”, FoodType.VEGETABLE, 27);
private static final Food HAMBURGER = new Food(“Hamburger”, FoodType.MEAT, 3);
private static MutableList<Animal> zooAnimals = Lists.mutable.with(
new Animal(“ZigZag”, AnimalType.ZEBRA, Lists.mutable.with(BANANA, APPLE)),
new Animal(“Tony”, AnimalType.TIGER, Lists.mutable.with(CEREAL, HAMBURGER)),
new Animal(“Phil”, AnimalType.GIRAFFE, Lists.mutable.with(CAKE, CARROT)),
new Animal(“Lil”, AnimalType.GIRAFFE, Lists.mutable.with(SPINACH)),
Ex. 1 - Determinar o item alimentar mais popular: como são as demandas das opções de comida?
@Benchmark
public List<Map.Entry<Food, Long>> mostPopularFoodItemJdk()
{
//output: [Hamburger=2]
return zooAnimals.stream()
.flatMap(animals -> animals.getFavoriteFoods().stream())
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet()
.stream()
.sorted(Map.Entry.<Food, Long>comparingByValue().reversed())
.limit(1)
.collect(Collectors.toList());
}
Passo a passo: primeiro fazemos o stream do zooAnimals, e o flatMap() de cada um dos animais e seus alimentos favoritos, retornando um fluxo dos alimentos consumidos por cada animal. Em seguida, queremos agrupar os alimentos usando sua identidade como chave e a contagem como o valor, para que possamos determinar a contagem de animais por alimento. Este é um trabalho para o Collectors.counting(). Para ordenar, foi utilizado o .entrySet() desse mapa, em seguida o stream, e a ordenação por valores invertidos (lembre, o valor é a contagem de cada alimento, e se estivermos interessado no mais popular, queremos a ordem inversa), limit(1) retorna primeiro o valor e, finalmente coletamos tudo em uma lista.
Na saída, o alimento mais popular é [Hamburguer=2].
Ufa! Vamos ver como podemos conseguir o mesmo usando o Eclipse Collections.
@Benchmark
public MutableList<ObjectIntPair<Food>> mostPopularFoodItemEc()
{
//output: [Hamburger:2]
MutableList<ObjectIntPair<Food>> intIntPairs = zooAnimals.asLazy()
.flatCollect(Animal::getFavoriteFoods)
.toBag()
.topOccurrences(1);
return intIntPairs;
}
Começamos o mesmo mapeando cada animal para suas comidas favoritas. Porque o que realmente queremos é um mapa de itens para contar, uma bag é um caso de uso perfeito para nos ajudar a resolver o nosso problema. Chamamos toBag
e topOccurrences
, que retornam os itens que ocorrem com mais frequência. O topOccurrences(1)
retorna o item mais popular desejado, como uma lista de ObjectIntPairs (note que int é primitivo); [Hamburger:2].
Ex 2 - Número de alimentos favoritos para animais: quão diversificada é a escolha de cada animal - quantos animais comem apenas uma coisa? Quantos comem mais de uma coisa?
Primeiro a entrega do JDK:
@Benchmark
public Map<Integer, String> printNumberOfFavoriteFoodItemsToAnimalsJdk()
{
//output: {1=[Lil, GIRAFFE],[Simba, LION], 2=[ZigZag, ZEBRA],
// [Tony, TIGER],[Phil, GIRAFFE]}
return zooAnimals.stream()
.collect(Collectors.groupingBy(
Animal::getNumberOfFavoriteFoods,
Collectors.mapping(
Object::toString,
// Animal.toString() returns [name, type]
Collectors.joining(“,”))));
// Concatenate the list of animals for
// each count into a string
}
E novamente, usando o Eclipse Collections:
@Benchmark
public Map<Integer, String> printNumberOfFavoriteFoodItemsToAnimalsEc()
{
//output: {1=[Lil, GIRAFFE], [Simba, LION], 2=[ZigZag, ZEBRA],
// [Tony, TIGER], [Phil, GIRAFFE]}
return zooAnimals
.stream()
.collect(Collectors.groupingBy(
Animal::getNumberOfFavoriteFoods,
Collectors2.makeString()));
}
Este exemplo destaca o uso de Java Collections nativos junto com o Eclipse Collections, Collectors2; os dois são mutuamente exclusivos. Neste exemplo, queremos obter o número de itens alimentares por animal. Como alcançamos isso? No Java nativo, primeiro usamos o Collectors.groupingBy
para agrupar cada animal com o número de seus alimentos favoritos. Em seguida, usamos a função Collectors.mapping
para mapear cada objeto para sua toString
e, finalmente, chamamos Collectors.joining
para unir as strings, separadas por virgulas
No Eclipse Collections, também podemos usar o método Collectors.groupingBy
, mas em vez disso, chamamos Collectors2.makeString
para ter um pouco menos de verbosidade e obter o mesmo resultado (o makeString
coleta um conjunto em uma string delimitada por vírgula).
Ex. 3 - alimentos únicos: quantos tipos diferentes de alimentos existem e quais são eles?
@Benchmark
public Set<Food> uniqueFoodsJdk()
{
return zooAnimals.stream()
.flatMap(each -> each.getFavoriteFoods().stream())
.collect(Collectors.toSet());
}
@Benchmark
public Set<Food> uniqueFoodsEcWithoutTargetCollection()
{
return zooAnimals.flatCollect(Animal::getFavoriteFoods).toSet();
}
@Benchmark
public Set<Food> uniqueFoodsEcWithTargetCollection()
{
return zooAnimals.flatCollect(Animal::getFavoriteFoods,
Sets.mutable.empty());
}
Aqui temos algumas maneiras de resolver este problema! Usando o JDK, simplesmente transmitimos o zooAnimals, depois mapeamos seus alimentos favoritos e, finalmente, os coletamos em um conjunto. No Eclipse Collections, temos duas opções. O primeiro é aproximadamente o mesmo que a versão do JDK; simplificar os alimentos favoritos, em seguida, chamar .toSet()
para colocá-los em um conjunto. O segundo é interessante porque usa o conceito de coleções de destino. Você perceberá que o flatCollect()
é um método sobrecarregado, por isso temos diferentes construtores disponíveis. Passar em um conjunto como um segundo parâmetro significa que vamos enxugar diretamente no conjunto e pular a lista intermediária que teria sido usada no primeiro exemplo. Poderíamos ter chamado asLazy()
para evitar esse lixo extra; a avaliação aguardaria até a operação do terminal e assim evitaria o estado intermediário. Mas se você preferir menos chamadas de API ou precisar acumular os resultados em uma instância de coleção existente, considere o uso de coleções de destino ao converter de um tipo para outro.
Ex. 4 - Carnívoros e não carnívoros: Quantos animais comem carne? E quantos não comemcarne?
Observe que, em ambos os exemplos a seguir, escolhemos declarar explicitamente o predicado lambda na parte superior, em vez de inseri-los, para enfatizar a distinção entre o predicado do JDK e o predicado do Eclipse Collections. O Eclipse Collections tem suas próprias definições para Função, Predicado e muitos outros tipos funcionais muito antes de aparecerem no pacote java.util.function
no Java 8. Agora, os tipos funcionais no Eclipse Collections estendem os tipos equivalentes do JDK, mantendo assim a interoperabilidade com as bibliotecas de tipos JDK.
@Benchmark
public Map<Boolean, List<Animal>> getMeatAndNonMeatEatersJdk()
{
java.util.function.Predicate<Animal> eatsMeat = animal ->
animal.getFavoriteFoods().stream().anyMatch(
food -> food.getFoodType()== FoodType.MEAT);
Map<Boolean, List<Animal>> meatAndNonMeatEaters = zooAnimals
.stream()
.collect(Collectors.partitioningBy(eatsMeat));
//returns{false=[[ZigZag, ZEBRA], [Phil, GIRAFFE], [Lil, GIRAFFE]],
true=[[Tony, TIGER], [Simba, LION]]}
return meatAndNonMeatEaters;
}
@Benchmark
public PartitionMutableList<Animal> getMeatAndNonMeatEatersEc()
{
org.eclipse.collections.api.block.predicate.Predicate<Animal> eatsMeat =
animal ->animal.getFavoriteFoods()
.anySatisfy(food -> food.getFoodType() == FoodType.MEAT);
PartitionMutableList<Animal> meatAndNonMeatEaters =
zooAnimals.partition(eatsMeat);
// meatAndNonMeatEaters.getSelected() = [[Tony, TIGER], [Simba, LION]]
// meatAndNonMeatEaters.getRejected() = [[ZigZag, ZEBRA], [Phil, GIRAFFE],
// [Lil, GIRAFFE]]
return meatAndNonMeatEaters;
}
Aqui, queremos dividir nossos elementos por carnívoros e não carnívoros. Construímos um predicado, "eatsMeat" que analisa as comidas favoritas de cada animal e vê, se houver, anyMatch / anySatisfy (JDK e EC, respectivamente), a condição de que o tipo de alimento é de FoodType.MEAT.
A partir daí, em nosso exemplo de JDK, fazemos um .stream()
em nosso zooAnimals
, e os coletamos com um .partitioningBy()
Collector, passando nosso predicado eatsMeat. O tipo de retorno para isso é um Map com uma chave verdadeira e uma chave falsa. A chave "verdadeira" retorna aqueles animais que comem carne, enquanto a chave "falsa" retorna os animais que não comem carne.
No Eclipse Collections, chamamos partition()
no zooAnimals
, novamente passando pelo predicado. Ficamos com um PartitionMutableList
, que tem dois pontos da API - getSelected()
e getRejected()
, ambos retornando MutableLists
. Os elementos selecionados são nossos comedores de carne, e os rejeitados são os que não comem carne.
Comparação do uso de Memória
Nos exemplos acima, o foco era principalmente nos tipos e interfaces das coleções. Mencionamos no início que a transição para o Eclipse Collections também permite que otimizemos o uso da memória. O efeito pode ser bastante significativo, dependendo de quão extensivamente as Collections são usadas em sua aplicação e do tipo de Collection.
Nos gráficos, podemos observar a comparação de uso de memória entre o Eclipse Collections e as Collections do java.util.*.
[Clique nas imagens para aumentá-las]
O eixo horizontal mostra o número de elementos armazenados em uma collection e o eixo vertical representa a sobrecarga de armazenamento em kilobytes. A sobrecarga aqui significa que rastreamos a memória alocada depois de subtrair o tamanho da carga útil da coleta (assim, mostramos apenas a memória que as próprias estruturas de dados ocupam). O valor que medimos é simplesmente totalMemory()
- freeMemory()
, depois de pedir System.gc()
. Os resultados que observamos são estáveis e coincidem com os resultados obtidos com os mesmos exemplos no Java 8 usando jdk.nashorn.internal.ir.debug.ObjectSizeCalculator do projeto Nashor. (Esse utilitário mede o tamanho com precisão, mas infelizmente não é compatível com o Java 9 e além).
O primeiro gráfico mostra a vantagem de uma lista primitiva de valores int (inteiros) do Eclipse Collections, em comparação com uma lista de valores inteiros JDK. O gráfico mostra que, para um milhão de valores, a implementação de uma lista java.util.* usará mais de 15 megabytes de memória (cerca de 20 MB de sobrecarga de armazenamento para o JDK contra cerca de 5MB para o Eclipse Collections).
Os Maps em Java são extremamente ineficientes e exigem um objeto Map.Entry, que aumenta o uso de memória.
Mas se os maps não são eficientes em termos de memória, então os conjuntos (sets) são simplesmente terríveis, pois o Set usa Map na implementação subjacente, o que é um desperdício. O Map.Entry não serve para fins úteis, uma vez que apenas uma propriedade é necessária - a chave, que é o elemento do conjunto. Portanto, você vê que o Set e o Map em Java usam a mesma quantidade de memória, embora um Set possa ser muito mais compacto, o que é feito no Eclipse Collections. Ele acaba usando muito menos memória do que o JDK Set, conforme ilustrado acima.
Finalmente, o quarto gráfico mostra as vantagens dos tipos de coletas especializados. Como mencionado anteriormente, um Bag é simplesmente um conjunto, que permite múltiplas instâncias de cada um dos seus elementos, e pode ser considerado o mapeamento de elementos para o seu número de ocorrências. Pode-se usar o Bag para contar as ocorrências de um item. Em java.util.* uma estrutura de dados equivalente é um Map de item para Integer, em que o desenvolvedor é encarregado de manter o valor do total de ocorrências atualizado. Novamente, observe o quanto a estrutura de dados especializada (Bag) foi otimizada para minimizar o uso de memória e a coleta de lixo.
Claro, recomendamos testar isso para cada caso. Se substituirmos as Collections Java padrão pela Eclipse Collections, certamente obteremos resultados aprimorados, mas a magnitude do impacto que têm no uso geral da memória do seu programa depende das circunstâncias específicas.
Resultados de Benchmark da JMH
Nesta seção analisaremos a velocidade de execução dos exemplos que abordamos antes, comparando o desempenho do código antes e depois de reescrevê-lo usando o Eclipse Collections. O gráfico mostra o número de operações por segundo medido para as versões do Eclipse Collections e do JDK para cada teste. As barras mais longas representam melhores resultados. Como pode ser observado, a aceleração é dramática:
[Clique nas imagens para aumentá-las]
Queremos enfatizar que, ao contrário do uso de memória,os resultados que estamos mostrando são aplicáveis apenas aos nossos exemplos específicos. Novamente, os resultados específicos dependerão muito da sua situação em particular, portanto, teste-os em relação ao cenários reais que fazem sentido para a sua aplicação.
Conclusão
O Eclipse Collections foi desenvolvido nos últimos 10 anos para otimizar códigos e aplicativos Java. É fácil começar - as estruturas de dados são substituídas e a API geralmente é mais fluente do que o código tradicional de fluxos. Sabe de um caso de uso que ainda não resolvemos? Ficaremos felizes em receber suas contribuições! Sinta-se à vontade para consultar no GitHub. Boa codificação!
Links Uteis
- Seu ponto único para iniciar com o Eclipse Collections
- O projeto Eclipse Collections no GitHub
- Código fonte deste artigo
- Boa literatura introdutória para Eclipse Collections (anteriormente GS Collections)
- Estratégias de otimização com Eclipse Collections
- UnifiedSet - O economizador de memória
- UnifiedMap: Como funciona?
- Bag - o contador
Sobre os Autores
Kristen O'Leary integra o Goldman Sachs no grupo Prime Services Engineering. Ela contribuiu com vários aprimoramentos sobre contêiner, API e desempenho para Eclipse Collections e também ministrou treinamentos interna e externamente sobre o framework.
Vladimir Zakharov tem mais de vinte anos de experiência em desenvolvimento de software. Atualmente é diretor administrativo da Unidade de Negócios de Plataforma da divisão de Tecnologia da Goldman Sachs. Ele trabalhou com Java nos últimos dezoito anos, e em Smalltalk e em várias outras linguagens menos populares antes disso.