While writing this review, I saw a need for a Counter
object similar to Python's. It seemed like it would be easy to write such a class, but it turned out to be surprisingly complicated.
Counter.java
import java.util.*;
public class Counter<T> implements Map<T, Integer>,
Iterable<Map.Entry<T, Integer>> {
private final Map<T, Integer> c = new HashMap<T, Integer>();
/**
* Comparator to sort entries by decreasing count. In case of a tie,
* arbitrary but deterministic tie-breakers are used.
*/
private final Comparator<Map.Entry<T, Integer>> DESC_FREQ_COMPARATOR =
new Comparator<Map.Entry<T, Integer>>() {
@Override
public int compare(Map.Entry<T, Integer> a, Map.Entry<T, Integer> b) {
int aValue = a.getValue().intValue();
int bValue = b.getValue().intValue();
int diff;
if (0 != (diff = bValue - aValue)) {
return diff;
} else if (0 != (diff = b.hashCode() - a.hashCode())) {
return diff;
} else {
T aKey = a.getKey();
T bKey = b.getKey();
if (aKey == null && bKey == null) {
return 0;
} else if (aKey == null) {
return 1;
} else if (bKey == null) {
return -1;
} else if (bKey instanceof Comparable) {
@SuppressWarnings("unchecked")
Comparable<T> bKeyCmp = (Comparable<T>)bKey;
return bKeyCmp.compareTo(aKey);
} else {
return bKey.toString().compareTo(aKey.toString());
}
}
}
};
/**
* Creates an empty counter.
*/
public Counter() {}
/**
* Copy constructor.
*/
public Counter(Map<T, Integer> counter) {
this();
this.putAll(counter);
}
/**
* Returns the number of key-value mappings in this map, including entries
* that have a zero count.
*/
@Override
public int size() {
return this.c.size();
}
/**
* Returns whether there are no entries. Any entry, even an entry with a
* zero count, makes the counter non-empty.
*/
public boolean isEmpty() {
return this.c.isEmpty();
}
/**
* Returns whether the key exists. A key with a zero count exists until it
* is removed or the entire counter is cleared.
*/
@Override
public boolean containsKey(Object key) {
return this.c.containsKey(key);
}
/**
* Returns whether any key has the specified count.
*/
@Override
public boolean containsValue(Object integralNumber) {
return integralNumber instanceof Number &&
this.c.containsValue(((Number)integralNumber).intValue());
}
/**
* Returns whether any key has the specified count.
*/
public boolean containsValue(int value) {
return this.c.containsValue(value);
}
/**
* Returns the value to which the specified key is mapped, or null if this
* map contains no mapping for the key.
*/
@Override
public Integer get(Object key) {
Integer i = this.c.get(key);
return i == null ? 0 : i.intValue();
}
/**
* Sets the count for a key.
*/
@Override
public Integer put(T key, Integer value) {
Integer prev = this.c.put(key, value);
return (prev == null) ? 0 : prev;
}
/**
* Removes a key.
*/
@Override
public Integer remove(Object key) {
Integer prev = this.c.remove(key);
return (prev == null) ? 0 : prev;
}
@Override
public void putAll(Map<? extends T, ? extends Integer> m) {
for (Map.Entry<? extends T, ? extends Integer> entry : m.entrySet()) {
this.put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
this.c.clear();
}
/**
* Returns the keys in an arbitrary order.
*/
@Override
public Set<T> keySet() {
return this.c.keySet();
}
/**
* Returns the values in an arbitrary order.
*/
@Override
public Collection<Integer> values() {
return this.c.values();
}
/**
* Compares the specified object with this map for equality. Returns true
* if the given object is also a map and the two maps have the same
* counts.
*
* For this equality test, an entry with a zero count is treated the same
* as no entry at all.
*/
public boolean equals(Object o) {
if (!(o instanceof Counter) || this.hashCode() != o.hashCode()) {
return false;
}
Counter other = (Counter)o;
Map.Entry<T, Integer>[] a = this.mostCommon();
@SuppressWarnings("unchecked")
Map.Entry<?, Integer>[] b = other.mostCommon();
int i = 0;
for (; i < Math.min(a.length, b.length); i++) {
int aCount = (a[i] == null) ? 0 : a[i].getValue();
int bCount = (b[i] == null) ? 0 : b[i].getValue();
if (aCount == 0 && bCount == 0) {
return true;
} else if (aCount != bCount) {
return false;
}
Object aKey = a[i].getKey();
Object bKey = b[i].getKey();
if (!aKey.equals(bKey)) {
return false;
}
}
// Nothing left over
int aCount = (i >= a.length || a[i] == null) ? 0 : a[i].getValue();
int bCount = (i >= b.length || b[i] == null) ? 0 : b[i].getValue();
return aCount == 0 && bCount == 0;
}
/**
* Returns the hash code value for this map. The hash code of a map is
* defined to be the sum of the hash codes of each entry in the map's
* entrySet() view. This ensures that m1.equals(m2) implies that
* m1.hashCode()==m2.hashCode() for any two maps m1 and m2, as required by
* the general contract of Object.hashCode().
*/
@Override
public int hashCode() {
int sum = 0;
for (Map.Entry<T, Integer> e : this) {
if (e == null || e.getValue() == 0) {
break;
}
sum += e.hashCode();
}
return sum;
}
public void increment(T key, int delta) {
Integer prev = this.c.put(key, delta);
if (prev != null) {
this.c.put(key, prev + delta);
}
}
/**
* Increments the count of the specified key by 1.
*/
public void increment(T key) {
this.increment(key, 1);
}
/**
* Increments the count of each element in the collection by 1.
*/
public void increment(Collection<T> elements) {
for (T e : elements) {
this.increment(e, 1);
}
}
/**
* Increments the count of each element by some number.
*/
public void increment(Iterable<Map.Entry<T, Integer>> elements) {
for (Map.Entry<T, Integer> e : elements) {
this.increment(e.getKey(), e.getValue());
}
}
/**
* Decrements the count of the specified key by 1.
*/
public void decrement(T key) {
this.increment(key, -1);
}
/**
* Decrements the count of each element in the collection by 1.
*/
public void decrement(Collection<T> elements) {
for (T e : elements) {
this.increment(e, -1);
}
}
/**
* Decrements the count of each element by some number.
*/
public void decrement(Iterable<Map.Entry<T, Integer>> elements) {
for (Map.Entry<T, Integer> e : elements) {
this.increment(e.getKey(), -e.getValue());
}
}
/**
* Returns an iterator over <tt>entrySet()</tt>.
*/
@Override
public Iterator<Map.Entry<T, Integer>> iterator() {
return this.entrySet().iterator();
}
/**
* Returns a <tt>SortedSet</tt> of the entries in descending order of
* frequency. Entries with the same frequency are returned in an
* arbitrary order.
*/
@Override
public Set<Map.Entry<T, Integer>> entrySet() {
SortedSet<Map.Entry<T, Integer>> ss = new TreeSet<Map.Entry<T, Integer>>(DESC_FREQ_COMPARATOR);
ss.addAll(this.c.entrySet());
return ss;
}
/**
* Returns the entries in descending order of frequency. Entries with the
* same frequency are returned in an arbitrary order.
*/
public Map.Entry<T, Integer>[] mostCommon() {
return this.mostCommon(this.size());
}
/**
* Returns the <em>n</em> entries with the highest count in descending
* order of frequency. Entries with the same frequency are returned in an
* arbitrary order.
*
* @return An array of length <em>n</em>. If <em>n</em> exceeds the number
* of entries that exist, the result is padded with nulls.
*/
@SuppressWarnings("unchecked")
public Map.Entry<T, Integer>[] mostCommon(int n) {
Map.Entry[] top = new Map.Entry[n];
int i = 0;
for (Map.Entry<T, Integer> e : this) {
top[i++] = e;
if (i == n) break;
}
return (Map.Entry<T, Integer>[])top;
}
}
CounterTest.java
import static org.junit.Assert.*;
import static org.junit.Assume.*;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.Ignore;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.BreakIterator;
import java.util.Arrays;
import java.util.Map;
import java.util.Scanner;
// javac -cp .:junit.jar Counter.java CounterTest.java
// java -cp .:junit.jar:hamcrest-core.jar org.junit.runner.JUnitCore CounterTest
@RunWith(JUnit4.class)
public class CounterTest {
private static String SHAKESPEARE_CORPUS;
@BeforeClass
public static void downloadShakespeare() throws MalformedURLException {
URL shakespeare = new URL("http://norvig.com/ngrams/shakespeare.txt");
try (Scanner s = new Scanner(shakespeare.openStream(), "UTF-8")) {
SHAKESPEARE_CORPUS = s.useDelimiter("\\A").next();
} catch (IOException e) {
assertNull(SHAKESPEARE_CORPUS);
}
}
@Test
public void increment() {
Counter<String> c = new Counter<String>();
c.increment("duck");
assertEquals("One duck", 1, (int)c.get("duck"));
c.increment("duck");
c.increment("goose");
assertEquals("Two ducks", 2, (int)c.get("duck"));
assertEquals("One goose", 1, (int)c.get("goose"));
assertEquals("No pheasant", 0, (int)c.get("pheasant"));
}
@Test
public void incrementCollectionRemoveCopy() {
Counter<String> c = new Counter<String>();
c.increment(Arrays.asList(new String[] { "duck", "duck", "goose" }));
assertEquals("Two ducks", 2, (int)c.get("duck"));
assertEquals("One goose", 1, (int)c.get("goose"));
assertEquals("No pheasant", 0, (int)c.get("pheasant"));
Counter<String> cc = new Counter<String>(c);
c.remove("goose");
assertEquals("No goose", 0, (int)c.get("goose"));
assertEquals("Two ducks", 2, (int)cc.get("duck"));
assertEquals("One goose", 1, (int)cc.get("goose"));
assertEquals("No pheasant", 0, (int)cc.get("pheasant"));
}
@Test
public void incrementPutSubtractRemove() {
Counter<String> c = new Counter<String>();
c.increment("duck");
assertEquals("One duck", 1, (int)c.get("duck"));
c.increment("duck", 98);
assertEquals("99 ducks", 99, (int)c.get("duck"));
c.put("duck", 200);
assertEquals("200 ducks", 200, (int)c.get("duck"));
c.increment("duck", -50);
assertEquals("150 ducks", 150, (int)c.get("duck"));
c.remove("duck");
assertEquals("No duck", 0, (int)c.get("duck"));
}
@Test
public void nullSemantics() {
Counter<Integer> c = new Counter<Integer>();
assertFalse("does not contain key null", c.containsKey(null));
assertFalse("does not contain value 0", c.containsValue(0));
c.put(null, 0);
assertEquals("0 nulls", 0, (int)c.get(null));
assertTrue("contains key null", c.containsKey(null));
assertTrue("contains value 0", c.containsValue(0));
c.increment((Integer)null);
assertEquals("1 null", 1, (int)c.get(null));
assertTrue("contains key null", c.containsKey(null));
assertTrue("contains value 1", c.containsValue(1));
c.decrement((Integer)null);
assertEquals("no nulls", 0, (int)c.get(null));
assertTrue("contains key null", c.containsKey(null));
assertTrue("contains value 0", c.containsValue(0));
assertFalse("contains no value 1", c.containsValue(1));
}
@Test
public void mostCommon() {
Counter<String> c = new Counter<String>();
c.increment(Arrays.asList(new String[] { "duck", "duck", "goose" }));
Map.Entry<String, Integer>[] common = c.mostCommon();
assertEquals("2 entries", 2, common.length);
assertEquals("2 ducks first", "duck", common[0].getKey());
assertEquals("2 ducks first", 2, (int)common[0].getValue());
assertEquals("1 goose last", "goose", common[1].getKey());
assertEquals("1 goose last", 1, (int)common[1].getValue());
}
@Test
public void mostCommonTooMany() {
Counter<String> c = new Counter<String>();
c.increment("duck");
Map.Entry<String, Integer>[] common = c.mostCommon(3);
assertEquals("3 entries", 3, common.length);
assertEquals("1 duck first", "duck", common[0].getKey());
assertNull("null", common[1]);
assertNull("null", common[2]);
}
@Test
public void emptinessAndSizeSemantics() {
Counter<Integer> c = new Counter<Integer>();
assertTrue("new counter is empty", c.isEmpty());
assertEquals("new counter has 0 entries", 0, c.size());
c.put(0, 1);
assertFalse("entry makes it non-empty", c.isEmpty());
assertEquals("(0, 1) has 1 entry", 1, c.size());
c.decrement(0);
assertFalse("entry with zero count makes it non-empty", c.isEmpty());
assertEquals("(0, 0) has 1 entry", 1, c.size());
c.remove(0);
assertTrue("removed sole entry makes it empty", c.isEmpty());
assertEquals("removed sole entry has 0 entries", 0, c.size());
c.increment((Integer)null);
assertFalse("null entry makes it non-empty", c.isEmpty());
c.clear();
assertTrue("clearing makes it empty", c.isEmpty());
}
@Test
public void equality() {
Counter<String> a = new Counter<String>();
Counter<String> b = new Counter<String>();
assertEquals("nothing == nothing", a, b);
a.increment("duck");
assertNotEquals("1 duck != nothing", a, b);
a.decrement("duck");
a.put(null, 0);
assertEquals("0 duck + 0 null == nothing", a, b);
assertEquals("nothing == 0 duck + 0 null", b, a);
b.put("goose", 2);
assertNotEquals("0 duck != 2 geese", a, b);
a.increment("goose", 2);
assertEquals("2 geese + 0 duck == 2 geese", a, b);
assertEquals("2 geese == 2 geese + 0 duck", b, a);
}
@Test(timeout=500)
public void performance() throws IOException {
Counter<String> c = new Counter<String>();
assumeTrue(null != SHAKESPEARE_CORPUS);
StreamTokenizer tok = new StreamTokenizer(new StringReader(SHAKESPEARE_CORPUS));
tok.ordinaryChar('\'');
tok.ordinaryChar('"');
int type;
String word = "Ophelia";
int KNOWN_COUNT = 19, myCount = 0;
while (StreamTokenizer.TT_EOF != (type = tok.nextToken())) {
if (StreamTokenizer.TT_WORD == type) {
c.increment(tok.sval);
if (word.equals(tok.sval)) {
myCount++;
}
}
}
assumeTrue(KNOWN_COUNT == myCount);
assertEquals(word, myCount, (int)c.get(word));
}
}
Please critique both the Counter
and its unit tests in general.
Some concerns I have include:
- Is there a common Java library that serves a similar purpose, or am I reinventing-the-wheel?
- Are the semantics for
null
keys and zero counts reasonable? - Is it useful to implement the
Map<T, Integer>
andIterable
interfaces? Or does that unnecessarily complicate things? - I wrote many
increment()
anddecrement()
methods. Do they make the class convenient to use, or are they too redundant? - What do you think of the method signatures for
mostCommon()
andmostCommon(int)
? Is there too much redundancy withentrySet()
anditerator()
? - I had to use
@SuppressWarnings("unchecked")
in three places. How can I avoid those warnings in the first place? - I increment a count using two calls to
put()
of aHashMap
. Is there a more efficient way? - To support
entrySet()
andmostCommon()
, I build aSortedSet
from scratch. Is there a better data structure I could have used that could supportentrySet()
andmostCommon()
efficiently?