18

I have been using the Counter() data structure in Python as a key-value store that allows me to have the objects sorted according to their value using the most_common method. More info here.

Is there any similar data structure for the Java language? For example, I have seen many related answers that focus on sorting HashMaps or TreeMaps by the data structure is not initially defined for that purpose. In my case I usually need to to keep counters of objects and then to select the most common or the ones with the highest score (Top-N queries). However, it is difficult for me since I need to insert to a HashMap and then sort or to use multiple data structures.

Community
  • 1
  • 1
nikosdi
  • 2,028
  • 5
  • 25
  • 34
  • Maybe what you're looking for is in the answer of [This](http://stackoverflow.com/questions/2864840/treemap-sort-by-value?lq=1) question. – Ferdinand Neman Sep 02 '15 at 08:47

2 Answers2

13

From here:

The Counter class is similar to bags or multisets in other languages.

Java does not have a Multiset class, or an analogue. Guava has a MultiSet collection, that does exactly what you want.

In pure Java, you can use a Map and the new merge method:

final Map<String, Integer> counts = new HashMap<>();

counts.merge("Test", 1, Integer::sum);
counts.merge("Test", 1, Integer::sum);
counts.merge("Other", 1, Integer::sum);
counts.merge("Other", 1, Integer::sum);
counts.merge("Other", 1, Integer::sum);

System.out.println(counts.getOrDefault("Test", 0));
System.out.println(counts.getOrDefault("Other", 0));
System.out.println(counts.getOrDefault("Another", 0));

Output:

2
3
0

You can wrap this behaviour in a class in a few lines of code:

public class Counter<T> {
    final Map<T, Integer> counts = new HashMap<>();

    public void add(T t) {
        counts.merge(t, 1, Integer::sum);
    }

    public int count(T t) {
        return counts.getOrDefault(t, 0);
    }
}

And use it like this:

final Counter<String> counts = new Counter<>();

counts.add("Test");
counts.add("Test");
counts.add("Other");
counts.add("Other");
counts.add("Other");

System.out.println(counts.count("Test"));
System.out.println(counts.count("Other"));
System.out.println(counts.count("Another"));

Output:

2
3
0
arodriguezdonaire
  • 5,086
  • 24
  • 48
5

Here's a class that looks like it implements enough of Counter to do what you want.

static class Counter<T> {

    final ConcurrentMap<T, Integer> counts = new ConcurrentHashMap<>();

    public void put(T it) {
        add(it, 1);
    }

    public void add(T it, int v) {
        counts.merge(it, v, Integer::sum);
    }

    public List<T> mostCommon(int n) {
        return counts.entrySet().stream()
                // Sort by value.
                .sorted((e1, e2) -> Integer.compare(e2.getValue(), e1.getValue()))
                // Top n.
                .limit(n)
                // Keys only.
                .map(e -> e.getKey())
                // As a list.
                .collect(Collectors.toList());
    }
}

public void test() {
    Counter<String> c = new Counter<>();
    String[] numbers = {"Zero", "One", "Two", "Three", "Four", "Five", "Six"};
    for (int i = 0; i < numbers.length; i++) {
        c.add(numbers[i], i);
    }
    System.out.println(c.mostCommon(3));
}

It uses Java 8 functionality.

OldCurmudgeon
  • 62,806
  • 15
  • 115
  • 208
  • 2
    I logged in just to upvote this - thank you. I also noticed that the `mostCommon` method actually returns the least common elements. The edit got rejected and I'm not too sure why. But I at least want to leave a note for anyone who uses this - switch `Integer.compare(e1.getValue(), e2.getValue())` with `Integer.compare(e2.getValue(), e1.getValue())` in order to get the most common elements – Myles Hollowed Oct 09 '19 at 18:17
  • @MylesHollowed - Good catch - thanks for the heads-up - fixed. – OldCurmudgeon Oct 10 '19 at 07:47