Calculadora eficiente de frequência de palavras em Java
1. Visão geral
Neste tutorial, mostraremos várias maneiras de implementar um contador de palavras em Java.
2. Implementações de contador
Vamos começar simplesmente calculando a contagem de palavras nesta matriz:
static String[] COUNTRY_NAMES
= { "China", "Australia", "India", "USA", "USSR", "UK", "China",
"France", "Poland", "Austria", "India", "USA", "Egypt", "China" };
Se quisermos processar arquivos grandes, precisamos ir para outras opções descritas emhere.
2.1. Map comIntegers
Uma das soluções mais simples seria criar umMap, armazenar palavras como chaves e o número de ocorrências como valores:
Map counterMap = new HashMap<>();
for (String country : COUNTRY_NAMES) {
counterMap.compute(country, (k, v) -> v == null ? 1 : v + 1);
}
assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());
Simplesmente usamos o método útilcompute deMap, que incrementa o contador ou o inicializa com 1 se a chave não estiver presente.
No entanto,this method of creating counter isn’t efficient as Integer is immutable, so every time when we increment the counter, we create a new Integer object.
2.2. API Stream
Agora, vamos aproveitar a API Java 8 Stream,Streams paralelo e o coletorgroupingBy():
@Test
public void whenMapWithLambdaAndWrapperCounter_runsSuccessfully() {
Map counterMap = new HashMap<>();
Stream.of(COUNTRY_NAMES)
.collect(Collectors.groupingBy(k -> k, ()-> counterMap,
Collectors.counting());
assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());
}
Da mesma forma, poderíamos usar umparallelStream:
@Test
public void whenMapWithLambdaAndWrapperCounter_runsSuccessfully() {
Map counterMap = new HashMap<>();
Stream.of(COUNTRY_NAMES).parallel()
.collect(Collectors.groupingBy(k -> k, ()-> counterMap,
Collectors.counting());
assertEquals(3, counterMap.get("China").intValue());
assertEquals(2, counterMap.get("India").intValue());
}
2.3. Map com uma matrizInteger
A seguir, vamos usar umMap que envolve um contador dentro de uma matrizInteger usada como um valor:
@Test
public void whenMapWithPrimitiveArrayCounter_runsSuccessfully() {
Map counterMap = new HashMap<>();
counterWithPrimitiveArray(counterMap);
assertEquals(3, counterMap.get("China")[0]);
assertEquals(2, counterMap.get("India")[0]);
}
private void counterWithPrimitiveArray(Map counterMap) {
for (String country : COUNTRY_NAMES) {
counterMap.compute(country, (k, v) -> v == null ?
new int[] { 0 } : v)[0]++;
}
}
Observe como criamos umHashMap simples comint arrays como valores.
No métodocounterWithPrimitiveArray, enquanto iteramos sobre cada valor da matriz, nós:
-
invoque umget nocounterMap passando o nome do país como uma chave
-
verifique se uma chave já estava presente ou não. Se a entrada já estiver presente, criamos uma nova instância do array inteiro primitivo com um único "1". Se a entrada estiver ausente, incrementamos o valor do contador presente na matriz
Este método é melhor do que a implementação do wrapper -as it creates fewer objects.
2.4. Map com umMutableInteger
A seguir, vamos criar um objeto wrapper que incorpora um contador inteiro primitivo como abaixo:
private static class MutableInteger {
int count = 1;
public void increment() {
this.count++;
}
// getter and setter
}
Vamos ver como podemos usar a classe acima como um contador:
@Test
public void whenMapWithMutableIntegerCounter_runsSuccessfully() {
Map counterMap = new HashMap<>();
mapWithMutableInteger(counterMap);
assertEquals(3, counterMap.get("China").getCount());
assertEquals(2, counterMap.get("India").getCount());
}
private void counterWithMutableInteger(
Map counterMap) {
for (String country : COUNTRY_NAMES) {
counterMap.compute(country, (k, v) -> v == null
? new MutableInteger(0) : v).increment();
}
}
No métodomapWithMutableInteger, enquanto iteramos sobre cada país na matrizCOUNTRY_NAMES, nós:
-
invoque um get nocounterMap passando o nome do país como uma chave
-
verifique se a chave já está presente ou não. Se uma entrada estiver ausente, criamos uma instância deMutableInteger que define o valor do contador como 1. Incrementamos o valor do contador presente emMutableInteger se o país estiver presente no mapa
Este método de criação de um contador é melhor do que o anterior -as we’re reusing the same MutableInteger and thereby creating fewer objects.
É assim que o Apache CollectionsHashMultiSet funciona, onde incorpora umHashMap com o valorMutableInteger internamente.
3. Análise de desempenho
O gráfico acima é criado usando JMH e aqui está o código que criou as estatísticas acima:
Map counterMap = new HashMap<>();
Map counterMutableIntMap = new HashMap<>();
Map counterWithIntArrayMap = new HashMap<>();
Map counterWithLongWrapperMap = new HashMap<>();
@Benchmark
public void wrapperAsCounter() {
counterWithWrapperObject(counterMap);
}
@Benchmark
public void lambdaExpressionWithWrapper() {
counterWithLambdaAndWrapper(counterWithLongWrapperMap );
}
@Benchmark
public void parallelStreamWithWrapper() {
counterWithParallelStreamAndWrapper(counterWithLongWrapperStreamMap);
}
@Benchmark
public void mutableIntegerAsCounter() {
counterWithMutableInteger(counterMutableIntMap);
}
@Benchmark
public void mapWithPrimitiveArray() {
counterWithPrimitiveArray(counterWithIntArrayMap);
}
4. Conclusão
Neste artigo rápido, ilustramos várias maneiras de criar contadores de palavras usando Java.
A implementação desses exemplos pode ser encontrada emthe GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.