You could try something like this:
public class CharCounter {
final int max;
final int[] counts;
public CharCounter(char max) {
this.max = (int) max;
counts = new int[this.max + 1];
}
public void addCounts(char[] line) {
for (int symbol : line) {
counts[symbol]++;
}
}
public Map<Integer, Integer> getCounts() {
Map<Integer, Integer> countsMap = new HashMap<>();
for (int symbol = 0; symbol < counts.length; symbol++) {
int count = counts[symbol];
if (count > 0) {
countsMap.put(symbol, count);
}
}
return countsMap;
}
}
This uses an array to keep the counts and uses the char itself as an index to the array.
This eliminates the need to check if a map contains the given key etc. It also removes the need for autoboxing the chars.
And a performance comparison shows roughly 20x speedup:
public static final char MIN = 'a';
public static final char MAX = 'f';
private static void count1(Map<Integer, Integer> occurrences, char[] line) {
for (int symbol : line) {
Integer amount = 1;
if (occurrences.containsKey(symbol)) {
amount += occurrences.get(symbol);
}
occurrences.put(symbol, amount);
}
}
private static void count2(CharCounter counter, char[] line) {
counter.addCounts(line);
}
public static void main(String[] args) {
char[] line = new char[1000];
for (int i = 0; i < line.length; i++) {
line[i] = (char) ThreadLocalRandom.current().nextInt(MIN, MAX + 1);
}
Map<Integer, Integer> occurrences;
CharCounter counter;
// warmup
occurrences = new HashMap<>();
counter = new CharCounter(MAX);
System.out.println("Start warmup ...");
for (int i = 0; i < 500_000; i++) {
count1(occurrences, line);
count2(counter, line);
}
System.out.println(occurrences);
System.out.println(counter.getCounts());
System.out.println("Warmup done.");
// original method
occurrences = new HashMap<>();
System.out.println("Start timing of original method ...");
long start = System.nanoTime();
for (int i = 0; i < 500_000; i++) {
count1(occurrences, line);
}
System.out.println(occurrences);
long duration1 = System.nanoTime() - start;
System.out.println("End timing of original method.");
System.out.println("time: " + duration1);
// alternative method
counter = new CharCounter(MAX);
System.out.println("Start timing of alternative method ...");
start = System.nanoTime();
for (int i = 0; i < 500_000; i++) {
count2(counter, line);
}
System.out.println(counter.getCounts());
long duration2 = System.nanoTime() - start;
System.out.println("End timing of alternative method.");
System.out.println("time: " + duration2);
System.out.println("Speedup: " + (double) duration1 / duration2);
}
Output:
Start warmup ...
{97=77000000, 98=82000000, 99=86500000, 100=86000000, 101=80000000, 102=88500000}
{97=77000000, 98=82000000, 99=86500000, 100=86000000, 101=80000000, 102=88500000}
Warmup done.
Start timing of original method ...
{97=77000000, 98=82000000, 99=86500000, 100=86000000, 101=80000000, 102=88500000}
End timing of original method.
time: 7110894999
Start timing of alternative method ...
{97=77000000, 98=82000000, 99=86500000, 100=86000000, 101=80000000, 102=88500000}
End timing of alternative method.
time: 388308432
Speedup: 18.31249185698857
Also if you add the -verbose:gc
JVM flag you can see that the original method needs to do quite a bit of garbage collecting while the alternative method doesn't need any.