在撰写此评论时,我发现需要一个类似于Python的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()吗?


评论

注意:此数据结构通常称为Multiset或Bag。

#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>时,此强制转换实际上是不安全的,其中ST的超类型。


我通过两次调用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(),我从
从头开始。我是否可以使用更好的数据结构来
有效地支持SortedSetentrySet()


为什么不在内部使用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 other)吗?
\ $ \ 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

这就是探查器的用途。您可以尝试不同的实现,看看巫婆更快还是有所作为。
看起来不错,但这是探查器可以回答问题的另一种情况。


最后注意:nequals()似乎过于复杂,但我并没有深入研究是否存在任何实际问题。

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.


就这样....