I have a HashMap<Token, Integer>
, which counts occurrences of Token
. Each time Token
is found, the value in the map should be incremented.
Map<Token, Integer> occurrences = new HashMap<Token, Integer>();
// ...
public void tokenFound(Token token) {
Integer numberOfOccurs = occurrences.get(token);
Integer newNumberOfOccurs = new Integer((numberOfOccurs == null) ? 1 : numberOfOccurs.intValue() + 1);
occurrences.put(token, newNumberOfOccurs);
}
Is there a more elegant way to do this?
5 Answers 5
You have several different options for this:
Guava
Google's Guava Library introduces the idea of a Multiset which is capable of counting the occurrences, and also provides a couple of other features.
Java 8
If you are using Java 8 (which I highly recommend if you have the ability to do so), your tokenFound
method can simply be this:
occurrences.merge(token, 1, (oldValue, one) -> oldValue + one);
Or this:
occurrences.compute(token, (tokenKey, oldValue) -> oldValue == null ? 1 : oldValue + 1);
Note that as of Java 7, you can initialize the map with the "diamond operator":
Map<Token, Integer> occurrences = new HashMap<>();
Without Java 8, no libraries
If you are unable to use Java 8 and don't want to add Guava as a third party library to your project, there are a small part you can do to simplify your existing code:
Integer previousValue = occurrences.get(token);
occurrences.put(token, previousValue == null ? 1 : previousValue + 1);
More specifically:
- Using the
new Integer
constructor is not necessary, Java automatically uses boxing to do this. For Integer values close to zero, this will actually save you a little bit because Java keeps some integers cached. - You don't need the
newNumberOfOccurs
variable as it's only used once.
-
\$\begingroup\$ To supplement the integer caching part: docs.oracle.com/javase/7/docs/api/java/lang/… (-128 to 127) \$\endgroup\$h.j.k.– h.j.k.2014年07月15日 14:07:42 +00:00Commented Jul 15, 2014 at 14:07
-
3\$\begingroup\$ Awesome! p.s. I found out that you can slightly simplify the first example to:
occurrences.merge(token, 1, Integer::sum);
(Integer::sum is just a BiFunction that adds two integers) \$\endgroup\$Eran Medan– Eran Medan2016年02月11日 21:40:15 +00:00Commented Feb 11, 2016 at 21:40
Guava's Multiset
and its AtomicLongMap
are designed for this kind of counting.
See also:
- Guava's new collection types, explained.
- Effective Java, 2nd edition, Item 47: Know and use the libraries (The author mentions only the JDK's built-in libraries but I think the reasoning could be true for other libraries too.)
I feel the non library answers can be improved, so here's my take at those.
For java 7 :
private final Map<Token, AtomicInteger> occurrences = new HashMap<>();
public void tokenFound(Token token) {
if (!occurrences.containsKey(token)) {
occurrences.put(token, new AtomicInteger(1));
return;
}
occurrences.get(token).incrementAndGet();
}
You use AtomicInteger
as value type, allowing an easy incrementAndGet()
, instead of having to overwrite the bucket in the Map
.
For Java 8 :
private final Map<Token, LongAdder> occurrences = new HashMap<>();
public void tokenFound(Token token) {
occurrences.computeIfAbsent(token, (t) -> new LongAdder()).increment();
}
LongAdder
is a type specifically made for tallying (especially under heavy concurrency). The added computeIfAbsent()
method on Map
and the addition of lambdas turn this whole thing into a one-liner.
If you're using java 7, I'd opt for Guava, but if you're on 8 simply use the java.util
classes.
-
2\$\begingroup\$ I would beg you not to use
AtomicInteger
for this, but only to use it if you're actually running in a concurrent context. If I had to maintain this code and found out thatAtomicInteger
was being used to avoid a single extra line of code, I'd be pretty annoyed for the time I spent trying to figure out how this class was used concurrently. \$\endgroup\$Chris Hayes– Chris Hayes2014年07月16日 02:56:33 +00:00Commented Jul 16, 2014 at 2:56 -
\$\begingroup\$ @ChrisHayes you can also simply roll your own wrapper around an
int
with anincrement()
method. The 'elegance' from my solution does not come from usingAtomicInteger
per se, but from using a mutable value type in the map. If you do useAtomicInteger
, you can annotate the class with the JCIP annotation@NotThreadSafe
to avoid confusion. \$\endgroup\$bowmore– bowmore2014年07月16日 04:45:16 +00:00Commented Jul 16, 2014 at 4:45 -
\$\begingroup\$ I think I'd be more confused if I saw a class which went out of the way to use thread-safe types within itself and was annotated as not thread safe. ;) I like the elegance of simply being able to call
increment
, to be sure. \$\endgroup\$Chris Hayes– Chris Hayes2014年07月16日 04:47:09 +00:00Commented Jul 16, 2014 at 4:47 -
\$\begingroup\$ LongAdder?! One never stops learning. Your pure java8 example really neat. I wish, I could upvote 10 times. \$\endgroup\$GhostCat– GhostCat2018年11月20日 07:28:19 +00:00Commented Nov 20, 2018 at 7:28
Instead of using a Map<Token, Integer>
, use Map<Token, int[]>
.
This can be used to avoid calling put()
whenever you want to
modify an existing value.
HashMap<String, int[]> m=new HashMap<>();
m.put("a", new int[]{0});
m.get("a")[0]++;
System.out.println("m="+m.get("a")[0]);
Outputs:
m=1
If you are using Java 8:
Although the merge
and compute
methods in Map
work for this purpose, the Map.getOrDefault(Object key, V defaultValue)
show the intention of the code more clearly to me.
Map<Token, Integer> occurrences = new HashMap<Token, Integer>();
// for each token:
occurrences.put(token, occurrences.getOrDefault(token, 0) + 1);
Java
bound... \$\endgroup\$