Counter
对象。编写这样的类似乎很容易,但是却发现它出奇的复杂。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));
}
}
请一般性地批评
Counter
及其单元测试。我所关注的一些问题包括:
是否有一个通用的Java库具有类似的用途,还是我在重新发明轮子?
null
键和零计数的语义是否合理?对实现
Map<T, Integer>
和Iterable
接口有用吗?还是不必要地使事情复杂化?我写了许多
increment()
和decrement()
方法。它们使类易于使用还是太多余?您如何看待
mostCommon()
和mostCommon(int)
的方法签名? entrySet()
和iterator()
是否有太多冗余?我不得不在三个地方使用
@SuppressWarnings("unchecked")
。如何首先避免那些警告?我通过两次调用
put()
的HashMap
来增加计数。有没有更有效的方法?为了支持
entrySet()
和mostCommon()
,我从头开始构建了SortedSet
。我可以使用更好的数据结构来有效支持entrySet()
和mostCommon()
吗?#1 楼
是否有一个通用的Java库具有类似的用途,或者我是在重塑车轮吗?
据我所知,JRE不具备这种功能。可以立即提供它。
空键和零计数的语义是否合理?
计数通常是很典型的吗?
null
s?我想这很有可能,即使它为例外提供了可能性。我个人不允许这样做。合理的是,对不存在的键进行计数,只返回0。
是否对实现
Map<T, Integer>
和Iterable
接口吗?还是不必要地使事情复杂化?实现Map接口是不必要的,它需要很多代码,而这些代码并没有增加预期的责任这个班级的实际上,您没有完全遵守Map接口合同。遵守“单一职责原则”的要求将其删除。
实施
Iterable
接口更加有用,因为它以标准方式公开了结果。我写了许多
increment()
和decrement()
方法。它们是否使类易于使用,还是太多余了?
我将从此类中提取一个接口来捕获其API,并且可以实现其他实现。
public interface Counter<T> extends Iterable<Map.Entry<T, Integer>> {
void increment(T key);
void decrement(T key);
int getCount(T key);
Set<T> getKeys();
}
我什至可以用
Map.Entry<T, Integer>
专用接口替换Counter
。 尝试抵制添加便利方法的冲动,您随时可以根据需要使用Decorator模式添加糖果。同时,重点放在实现出色的核心实现上。
您如何看待
mostCommon()
和mostCommon(int)
的方法签名? entrySet()
和iterator()
是否存在过多的冗余?这些都是多余的,在这种情况下,也可以使用装饰器。
我不得不在三个地方使用
@SuppressWarnings("unchecked")
。我如何首先避免这些警告?第一次出现是:
CounterImpl other = (CounterImpl)o;
Map.Entry<T, Integer>[] a = this.mostCommon();
@SuppressWarnings("unchecked")
Map.Entry<?, Integer>[] b = other.mostCommon();
您声明不带类型参数的
other
变量。避免未经检查的警告的规则1是在具有一个的类上指定type参数。毕竟,您可以忽略它们的唯一原因是引入泛型时需要向后兼容。CounterImpl<?> other = (CounterImpl)o;
Map.Entry<T, Integer>[] a = this.mostCommon();
Map.Entry<?, Integer>[] b = other.mostCommon();
mostCommon()
中也发生了同样的情况下一种情况:
} else if (bKey instanceof Comparable) {
@SuppressWarnings("unchecked")
Comparable<T> bKeyCmp = (Comparable<T>)bKey;
反射代码和泛型注定会给您带来未经检查的警告带来的麻烦。实际上,当
T
实现Comparable<S>
时,此强制转换实际上是不安全的,其中S
是T
的超类型。我通过两次调用
put()
的HashMap
来增加计数。有没有一种更有效的方法?是的。不要使用
Integer
作为Map
中的值,而使用可变计数表示。您可以轻松地自己写一个,或使用AtomicInteger
(尽管您可能不需要线程安全性)。然后定义:private final Map<T, AtomicInteger> c = new HashMap<T, AtomicInteger>();
现在,如果密钥不在
Map
中,您只需要做一次看跌期权:public void increment(T key, int delta) {
Integer prev = this.c.put(key, delta);
if (!c.containsKey(key)) {
c.put(key, new AtomicInteger());
}
c.get(key).addAndGet(delta);
}
在Java 8中,使用
java.util.Map#computeIfAbsent
变得更加简单c.computeIfAbsent(key, k -> new AtomicInteger()).addAndGet(delta);
为了支持
entrySet()
和mostCommon()
,我从从头开始。我是否可以使用更好的数据结构来
有效地支持
SortedSet
和entrySet()
?为什么不在内部使用
mostCommon()
?private final Map<T, Integer> c = new TreeMap<T, Integer>(DESC_FREQ_COMPARATOR);
这样,
SortedMap
将返回一组,迭代器将依次返回一组键。尽管此c.entrySet()
实际上不是Set
,但它仍然可以满足您的需求。一般说明:
计数器
避免缩写作为变量名。
CounterTest
为每个断言编写单独的测试: equal()可以分为7个不同的测试。您会发现断言中不再需要String参数。
在与单元测试不同的类中进行性能测试。单元测试应该尽可能轻量级。
最后考虑一下我将如何处理(JDK8)
EDIT:更新此代码以使用LongAdder代替AtomicInteger ,因为在这种情况下LongAdder的性能甚至优于AtomicInteger。
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.LongAdder;
import java.util.stream.Collectors;
public interface Counter<T> extends Iterable<Counter.Count<T>> {
void increment(T key);
void decrement(T key);
int getCount(T key);
Set<T> getKeys();
interface Count<T> {
T getKey();
int getCount();
}
}
class SimpleCounter<T> implements Counter<T> {
public static final LongAdder DEFAULT_VALUE = new LongAdder();
private final Map<T, LongAdder> counts = new HashMap<>();
@Override
public void increment(T key) {
counts.computeIfAbsent(key, k -> new LongAdder()).increment();
}
@Override
public void decrement(T key) {
counts.computeIfAbsent(key, k -> new LongAdder()).decrement();
}
@Override
public int getCount(T key) {
return counts.getOrDefault(key, DEFAULT_VALUE).intValue();
}
@Override
public Set<T> getKeys() {
return counts.keySet();
}
@Override
public Iterator<Count<T>> iterator() {
return counts.entrySet().stream().map(e -> new SimpleCount<T>(e)).collect(Collectors.<Count<T>>toSet()).iterator();
}
private static class SimpleCount<E> implements Count<E> {
private final Map.Entry<E, LongAdder> entry;
SimpleCount(Map.Entry<E, LongAdder> entry) {
this.entry = entry;
}
@Override
public E getKey() {
return entry.getKey();
}
@Override
public int getCount() {
return entry.getValue().intValue();
}
}
}
评论
\ $ \ begingroup \ $
当AtomicInteger发生突变时,TreeMap不会重新排列自身,对吗?
\ $ \ endgroup \ $
– 200_success
2014年3月19日19:40在
\ $ \ begingroup \ $
我相信我有义务实现.equals(Object other)而不是.equals(Counter
\ $ \ endgroup \ $
– 200_success
2014年3月19日19:42
\ $ \ begingroup \ $
TreeMap是按键而不是值排序的。如果引入我建议的Count接口,则可以使它成为Comparable,并且可以对它们进行简单排序,或者使用max()/ min()
\ $ \ endgroup \ $
– Bowmore
2014年3月19日19:45在
\ $ \ begingroup \ $
您有义务实现equals(Object),但前提是要实现Map接口。就个人而言,我不会为平等而烦恼。
\ $ \ endgroup \ $
– Bowmore
2014年3月19日19:46在
\ $ \ begingroup \ $
@skiwi:我已经更新了答案。提交此答案后,我才了解到新的LongAdder类,此后我再也没有讲过。感谢您指出可能的改进。
\ $ \ endgroup \ $
– Bowmore
2014年5月31日15:35
#2 楼
我现在真的没有时间阅读整个代码和现有的评论,但是到目前为止,我还没有人提到Guava的Multiset
及其AtomicLongMap
。我已经可以更改
Counter
测试条中的HashMultiset
几乎没有问题,而条形保持绿色:Multiset
不存储出现次数为零的键(元素)。 (如果需要AtomicLongMap
可能是一个更好的选择。)mostCommon
不支持Multiset
,但Multisets
支持。 (此处可能还需要Iterables.limit
。)较小的方法名称更改。
#3 楼
当您传递不是整数的containsValue()
时,Double
可以返回true。任何值的实现都应等效于以下内容。public boolean containsValue(Object integralNumber) {
for (Integer i : values()) {
if (i.equals(integralNumber) {
return true;
}
}
return false;
}
您的文档与
Map
接口的文档匹配,而不与Counter
的实际操作匹配。示例:get()
从不返回null
。是否存在
mostCommon()
返回数组而不是List
的原因?其他所有内容都与collections库对齐,因此看起来似乎不合适。您的测试正在执行edit-check-repeat。这样可以隐藏有关错误是什么的信息。当
equality()
中的第一个断言失败时,您仅知道两个空计数器是不相等的。但是,如果将其分为5个不同的测试,则可能会发现其中一些通过了,但不是全部。这样可以节省您开始调试代码的时间。您真的需要访问Internet来运行测试吗?如果恰好在适当的时间断开了Internet连接,即使您的代码没有错,您的测试也会失败。
您的问题:
我不知道。
在大多数情况下,
null
的使用似乎已被最小化,可以与强迫您使用的其他api进行交互,而不是您自己使用它。很好,但是请参见问题5。我确实认为实现
Map<T, Integer>
会使某些事情变得不干净。在某些情况下,当您必须将Object
设为T
或只是一个int时,必须将其作为参数,并且它添加了一些方法来处理您想要的api。在问题3上,我只是具有
increment()
和decrement()
。在数组中具有
null
似乎是错误的。我希望该方法最多返回一个List
个元素。这只是在转换为泛型类时必须处理的事情。 1
这就是探查器的用途。您可以尝试不同的实现,看看巫婆更快还是有所作为。
看起来不错,但这是探查器可以回答问题的另一种情况。
最后注意:
n
和equals()
似乎过于复杂,但我并没有深入研究是否存在任何实际问题。1 bowmore的答案在解决此问题时会更好。
评论
\ $ \ begingroup \ $
感谢您发现ContainsValue()错误-+1。
\ $ \ endgroup \ $
– 200_success
2014年3月19日19:51
\ $ \ begingroup \ $
我认为,假设Internet不可用,假定True(null!= SHAKESPEARE_CORPUS)应该可以防止测试失败?
\ $ \ endgroup \ $
– 200_success
2014年3月19日19:52
\ $ \ begingroup \ $
@ 200_success:的确如此。我仍然不喜欢测试可能不会基于与代码库无关的因素运行的事实。在公司环境中,构建服务器可能无法访问外部Internet。在这种情况下,您将错过有价值的回归测试。您也无法控制资源,也不知道资源是否会更改。如果该站点决定举办其他比赛,那么即使您的代码正确,您的测试也会失败。一次下载资源并将其保存在测试中可以解决所有这些问题。
\ $ \ endgroup \ $
–unholysampler
2014年3月19日20:00
#4 楼
您的担忧重新发明轮子:我不熟悉具有此功能的任何东西,但是我对Apache Commons和Guava的了解有点有限.... @palacsint?
空键看起来不错。您首先对null排序,这既不在此处也不在那里,但是您可能希望对其进行记录。至于int值...此代码使我感到困惑:
/**
* 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();
}
Javadoc说如果键不是,则返回null已映射,但实际上返回0。此外,如果已映射该值,则提取int值,然后将其自动装箱返回Integer。
类似地,在
put()
方法中,您返回0而不是null一个不是先前映射的值实现
Map<...>
-有用吗?我将在整个章节中专门说明这一点。...;-) 我对递增/递减方法的数量没有特别的关注。在概念上,它们最方便。有人可能会说YAGNI ...但是我不在那个阵营中...没有什么比需要一个方便的工具并发现它不存在更糟糕的了。
有趣的问题....我最初的反应是我不喜欢这些方法(mostCommon *)。我认为迭代和破坏将很容易。
未经检查的警告-实际上,它们也是原始警告。这与
Map<...>
接口的使用有关。是。还有更好的方法。...不要使用Map ....
这个大问题....我肯定有,但是在我完成之前不确定如何描述它....
使用Map类
我最喜欢java.util。*集合接口的地方是实现(不使用)它们有多复杂。
您的代码中存在很多与此有关的错误。
请考虑以下文档以获取有关
Map.entrySet()
的信息:返回此映射中包含的映射的Set视图。该集合受地图支持,因此对地图的更改会反映在集合中,并且
反之亦然。如果在对集合进行迭代时修改了地图(
通过迭代器自己的remove操作,或者通过
通过setValue操作对
迭代器返回的映射条目进行了修改),则迭代的结果是不确定的。该集合支持
元素删除,该元素通过Iterator.remove,Set.remove,removeAll,retainAll和clear
操作从映射中删除相应的映射,
。它不支持add或addAll操作。
由于将entrySet作为不同的数据TreeMap的
entrySet()
实现,因此您几乎无法满足所有所有这些要求.... 根据我的经验,实现Collection接口非常困难(而且我有一些成功的实现证明了这一点)。当您想对
ConcurrentModificationException
之类的东西做“正确”的事情时,尤其困难。我的建议是完全不要公开它。这样,您可以定义自己的更简单的规范。错误
我可以看到很多错误。
/>构造函数
public Counter(Map<T, Integer> counter) {...}
会给您带来许多问题:例如,人们可以为HashMap中的Integer
值提供空值。这将导致您的代码在Comparator中引发空指针异常。使用
Counter.get(key)
方法的人们将获得一个零映射的Integer值,但如果他们获得同一键的Map.Entry实例, getValue()将为null。取消自动装箱时,所有的增量/减量方法都将引发null-pointer-exception。
比较器有一个如果值太高,则会出现排序错误:
if (0 != (diff = bValue - aValue)) {
return diff;
如果
aValue
已增加为+1000,而bValue
已减少为-2000,则我们要先对aValue排序(返回负数),因为它的计数较大。函数-2000-(+1000)将返回-3000,这是正确的。但是,如果aValue增大为+ 1,000,000,000,bValue减小为-2,000,000,000,则结果将使int值下溢,并最终得到正结果....导致错误的排序。做到这一点的正确方法是使用纯色比较:
if (aValue != bValue) {
return aValue < bValue ? 1 : -1;
结论
这个概念很好,但暴露地图是一个负数。太多事情出错了。
这也使得以后几乎没有并发版本成为可能。
我将使用不公开内部的自定义接口来实现它根本没有类...使接口保持简单(基于基元):
public int getCount(T key) { ... }
public int incrementAndGet(T key) { .... } // similar to AtomicInteger
public int getAndIncrement(T key) { .... } // similar to AtomicInteger
decrement options too
public Iterator<T> iterator() { ... } // simple iterator of keys in decreasing count order.
equals and hashCode.
就这样....
评论
注意:此数据结构通常称为Multiset或Bag。