16

I need a Map that could be iterated in the decreasing order of its values. Does any of the standard libraries like Apache Commons or Guava provide this kind of map ?

Rajat Gupta
  • 25,853
  • 63
  • 179
  • 294
  • The standard Java library has an extension of the `Map` interface that is sorted called SortedMap: http://docs.oracle.com/javase/7/docs/api/java/util/SortedMap.html You can then just pick a concrete implementation of the `SortedMap` interface – Hunter McMillen Jan 17 '12 at 14:53
  • I find it odd that you want to have the decreasing order of *values* - do you really want that or did you mean *keys*? – nd. Jan 17 '12 at 14:57
  • @nd: yes I'm sure I really want that – Rajat Gupta Jan 17 '12 at 14:58
  • 9
    SortedMap orders keys, not values - it's stated on firts line of its documentation. – Rostislav Matl Jan 17 '12 at 15:10

8 Answers8

13

I would do this with Guava as follows:

Ordering<Map.Entry<Key, Value>> entryOrdering = Ordering.from(valueComparator)
  .onResultOf(new Function<Entry<Key, Value>, Value>() {
    public Value apply(Entry<Key, Value> entry) {
      return entry.getValue();
    }
  }).reverse();
// Desired entries in desired order.  Put them in an ImmutableMap in this order.
ImmutableMap.Builder<Key, Value> builder = ImmutableMap.builder();
for (Entry<Key, Value> entry : 
    entryOrdering.sortedCopy(map.entrySet())) {
  builder.put(entry.getKey(), entry.getValue());
}
return builder.build();
// ImmutableMap iterates over the entries in the desired order
Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
  • could you clarify how do I define this valueComparator ? – Rajat Gupta Jan 17 '12 at 16:49
  • 1
    @user it's [`Comparator`](http://docs.oracle.com/javase/6/docs/api/java/util/Comparator.html) you want to use in first place. If values implement `Comparable`, then you can use [`Ordering.natural()`](http://docs.guava-libraries.googlecode.com/git-history/v11.0.1/javadoc/com/google/common/collect/Ordering.html#natural()) instead of custom `valueComparator`. – Grzegorz Rożniecki Jan 17 '12 at 18:04
  • 1
    `Ordering.from(Ordering.natural())...` leads to a deprecated function call, there is a alternative method which asks for a comparator type argument instead. My values are actually of type `Integer`. Could you suggest what comparator should I pass for integer values sorting? Thanks a lot – Rajat Gupta Jan 18 '12 at 04:37
  • 4
    Don't use Ordering.from(Ordering.natural()), just use Ordering.natural() directly. – Louis Wasserman Jan 18 '12 at 06:40
12

With guava, there is even cleaner way than @LoisWasserman's anwer - using Ordering combined with Functions.forMap:

Ordering.natural().reverse().nullsLast().onResultOf(Functions.forMap(map, null))

or if values aren't Comparable:

Ordering.fromComparator(yourComparator).reverse().nullsLast().onResultOf(Functions.forMap(map, null))

An example (with first option - natural ordering):

final Map<String, String> map = ImmutableMap.of(
    "key 1", "value 1",
    "key 2", "value 2",
    "key 3", "another value",
    "key 4", "zero value");

final Ordering<String> naturalReverseValueOrdering =
    Ordering.natural().reverse().nullsLast().onResultOf(Functions.forMap(map, null));

System.out.println(ImmutableSortedMap.copyOf(map, naturalReverseValueOrdering));

outputs:

{key 4=zero value, key 2=value 2, key 1=value 1, key 3=another value}

(I use ImmutableSortedMap here, but TreeMap can also be used if mutability is required.)

EDIT:

If there are identical values (more exactly if there are two values for which Comparator.compare(String v1, String v2) returns 0) ImmutableSortedMap throws an exception. Ordering must not return, so i.e. you should order map by values first and keys next if both values are equal (keys aren't supposed to be equal) by using Ordering.compound:

final Map<String, String> map = ImmutableMap.of(
    "key 1", "value 1",
    "key 2", "value 2",
    "key 3", "zero value",
    "key 4", "zero value");

final Ordering<String> reverseValuesAndNaturalKeysOrdering =
    Ordering.natural().reverse().nullsLast().onResultOf(Functions.forMap(map, null)) // natural for values
        .compound(Ordering.natural()); // secondary - natural ordering of keys

System.out.println(ImmutableSortedMap.copyOf(map, reverseValuesAndNaturalKeysOrdering));

prints:

{key 3=zero value, key 4=zero value, key 2=value 2, key 1=value 1}
Grzegorz Rożniecki
  • 27,415
  • 11
  • 90
  • 112
  • 1
    this leads to illegal argument exception incase two keys are set with same value... – Rajat Gupta Jan 17 '12 at 18:50
  • @user that's because comparing them returns 0 (suggesting equality, what's correct) and ImmutableSortedMap treats that as exceptional case (what should it do? entry: `key1=same_value` first or rather `key2=same_value` first?). Comparator **must not** return 0 in this case, edited my answer and example (but remember - it's for Strings!). – Grzegorz Rożniecki Jan 17 '12 at 19:26
  • I'm really fundamentally uncomfortable with this solution, specifically because your ImmutableSortedMap won't just return null on keys that aren't in the map: it'll fail completely, because the comparator doesn't apply. – Louis Wasserman Jan 18 '12 at 00:13
  • @LouisWasserman isn't this enough when same `map` is used in `Functions.forMap` and `ImmutableSortedMap.copyOf` constructor? – Grzegorz Rożniecki Jan 18 '12 at 09:53
  • It's enough to make the ImmutableSortedMap creation work fine, yes. And then it'll explode in your face when you try to query the `ImmutableSortedMap` later on and have no idea why it's throwing weird exceptions. That is, `map.get(keyNotInTheMap)` will throw an exception. Ew. – Louis Wasserman Jan 19 '12 at 03:28
  • @LouisWasserman ah, that's your point! `Functions.forMap(map, null)` should be used then. – Grzegorz Rożniecki Jan 19 '12 at 10:47
  • That's still iffy and bug-prone, since then you need your comparator to be able to deal with nulls. And that's all setting aside the potential cost of calling map.get every time you do a comparison. This approach as a whole is just much more prone to things going wrong in subtle and unexpected ways. =/ – Louis Wasserman Jan 19 '12 at 21:39
  • @LouisWasserman I added `.nullsLast()` to comparator and now I surrender :) Treat this answer as narrow (works quite fine in ImmutableSortedMap) unconventional use of `Functions.forMap` (it must have some purpose in library) for getting values of map for keys while using comparator. – Grzegorz Rożniecki Jan 20 '12 at 08:03
  • 2
    I _think_ the modified answer is correct, but as a general rule, if you can avoid writing code that can go wrong in many subtle ways, you should always do so. – Louis Wasserman Jan 20 '12 at 15:32
2

This can now be done in a single line using Java 8 Streams:

map.entrySet().stream()
    .sorted(Comparator.comparing(Map.Entry::getValue))
    .forEach(...);
ejain
  • 3,456
  • 2
  • 34
  • 52
1

Simple method to get an immutable copy of your map sorted by descending value. Remove the call to reverse() if you want ascending order. Requires Google Guava.

private Map<String, String> mapSortedByValues(Map<String, String> theMap) {
    final Ordering<String> ordering =
            Ordering.natural().reverse().nullsLast().onResultOf(Functions.forMap(theMap, null));

    return ImmutableSortedMap.copyOf(theMap, ordering);
}
matt burns
  • 24,742
  • 13
  • 105
  • 107
0

I think the DualTreeBidiMap of Apache Commons Collections should make this possible, probably by iterating over the return from inverseBidiMap().

But I don't think this allows for duplicate values - as the name says, the structure is simply based on keeping two trees, which is really the only thing that makes sense, since values in a map have no meaning to the map structure.

Michael Borgwardt
  • 342,105
  • 78
  • 482
  • 720
  • You don't need two trees. You could have one `Map` for the key-values, and an ordered `List` of values (or of `Map.Entry`s if you need that). – yshavit Jan 17 '12 at 15:25
  • @yshavit: you could, if you have a list structure that is both auto-ordered and allows efficient insertion and deletion, which is not simple (I think it can be done with a skip list). Why the extra complexity if you already have one tree? – Michael Borgwardt Jan 17 '12 at 15:29
  • because, as you point out, it doesn't allow for duplicate values. If that's a requirement, the two-`Map` approach is disqualified. And you don't need a `List` that manages the order for you, you can just manually ensure the ordering on inserts as an invariant. But anyway, I agree it's more complexity, and should only be done if you need those duplicate values. – yshavit Jan 17 '12 at 16:01
0

What about putting the values also in TreeSet ?

for(;;) { yourMap.put(key,value); }
SortedSet sortedValues = new TreeSet(yourMap.values());

or

SortedSet sortedValues = new TreeSet();
for(;;) 
{
yourMap.put(key,value);
sortedValued.add(value);
}
Rostislav Matl
  • 4,294
  • 4
  • 29
  • 53
0

I'd put entries on the list and sort it. I don't recall any map that can be ordered by values, only by keys. You could use BiMap from Guava, but it requires values uniqueness.

Example:

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<String, String>() {{
            put("key1", "value1");
            put("key2", "value3");
            put("key3", "value4");
            put("key4", "value2");
        }};

        List<Map.Entry<String, String>> entries = new ArrayList<>(map.entrySet());
        Collections.sort(entries, new Comparator<Map.Entry<String, String>>() {

            @Override
            public int compare(Entry<String, String> o1,
                    Entry<String, String> o2) {
                if (o1.getValue() == null && o2.getValue() == null) return 0;
                if (o1.getValue() == null) return -1; //Nulls last
                return - o1.getValue().compareTo(o2.getValue());
            }
        });

    }
Piotr Gwiazda
  • 12,080
  • 13
  • 60
  • 91
0

I think you have to roll your own implementation of such a map. Fortunately, it shouldn't be much of an issue with Guava:

public class SortedValueMap<K, V> extends ForwardingMap<K, V> {

  private Map<K, V> delegate = newHashMap();
  private Comparator<V> valueComparator;

  public static <K, V extends Comparable<V>> SortedValueMap<K, V> reverse() {
    return new SortedValueMap<K, V>(Ordering.<V> natural().reverse());
  }

  public static <K, V> SortedValueMap<K, V> create(Comparator<V> valueComparator) {
    return new SortedValueMap<K, V>(valueComparator);
  }

  protected SortedValueMap(Comparator<V> valueComparator) {
    this.valueComparator = checkNotNull(valueComparator);

  }

  @Override
  protected Map<K, V> delegate() {
    return delegate;
  }

  @Override
  public Set<K> keySet() {
    return new StandardKeySet();
  }

  @Override
  public Set<Map.Entry<K, V>> entrySet() {
    TreeSet<Map.Entry<K, V>> result = newTreeSet(new Comparator<Map.Entry<K, V>>() {
      @Override
      public int compare(Map.Entry<K, V> o1, Map.Entry<K, V> o2) {
        return ComparisonChain.start()
            .compare(o1.getValue(), o2.getValue(), valueComparator)
            .compare(o1.getKey(), o2.getKey(), Ordering.arbitrary())
            .result();
      }

    });
    result.addAll(Collections.unmodifiableMap(delegate).entrySet());
    return result;
  }

  @Override
  public Collection<V> values() {
    return new StandardValues();
  }

  public static void main(String[] args) {
    SortedValueMap<String, String> svm = SortedValueMap.reverse();
    svm.put("foo", "1");
    svm.put("bar", "3");
    svm.put("baz", "2");

    System.out.println(Joiner.on(", ").withKeyValueSeparator("=").join(svm));
    System.out.println(Joiner.on(", ").join(svm.values()));
    System.out.println(Joiner.on(", ").join(svm.keySet()));
  }
}

Fail-fast iterators are not present in this implementation; please add them for yourself if required. Please also note that setting a value via Map.Entry.setValue would cause havoc with the sort order, which is why I used unmodifyableMap in entry set.

nd.
  • 8,699
  • 2
  • 32
  • 42