我对Java比较陌生,经常发现需要对值进行Map<Key,Value>排序。

由于这些值不是唯一的,我发现自己将keySet转换为一个数组,并使用自定义比较器通过数组排序对该数组进行排序,该比较器根据与该键关联的值进行排序。

有没有更简单的方法?


当前回答

三个单行答案。。。

我会使用GoogleCollectionsGuava来实现这一点-如果你的价值观是可比较的,那么你可以使用

valueComparator = Ordering.natural().onResultOf(Functions.forMap(map))

这将为地图创建一个函数(对象)[将任何键作为输入,返回相应的值],然后对它们应用自然(可比较)排序[值]。

如果它们不具有可比性,那么您需要按照

valueComparator = Ordering.from(comparator).onResultOf(Functions.forMap(map)) 

这些可以应用于TreeMap(因为Ordering扩展了Comparator),或者在排序后应用于LinkedHashMap

注意:如果要使用TreeMap,请记住,如果比较==0,则该项已在列表中(如果有多个值进行比较,则会发生这种情况)。为了缓解这种情况,您可以像这样将键添加到比较器中(假设键和值是可比较的):

valueComparator = Ordering.natural().onResultOf(Functions.forMap(map)).compound(Ordering.natural())

=对键映射的值应用自然排序,并将其与键的自然排序组合

请注意,如果您的键与0比较,这仍然不起作用,但这对于大多数可比较的项来说应该足够了(因为hashCode、equals和compareTo通常是同步的…)

请参见Ordering.onResultOf()和Functions.forMap()。

实施

现在我们有了一个比较器,它可以满足我们的需要,我们需要从中得到一个结果。

map = ImmutableSortedMap.copyOf(myOriginalMap, valueComparator);

现在,这很可能奏效,但:

需要完成一张完整的地图不要在TreeMap上尝试上面的比较器;当插入的键在put之后才有值时,尝试比较它是没有意义的,也就是说,它会很快断开

第1点对我来说有点破坏交易;google集合非常懒惰(这很好:你几乎可以在一瞬间完成所有操作;真正的工作是在你开始使用结果时完成的),这需要复制整个地图!

“完整”答案/按值排序的实时地图

不过别担心;如果你痴迷于以这种方式对“实时”地图进行排序,那么你可以用以下疯狂的方式解决上述问题,而不是其中一个,而是两个(!):

注意:这在2012年6月发生了重大变化-以前的代码永远无法工作:需要内部HashMap来查找值,而不需要在TreeMap.get()->compare()和compare(()->get()之间创建无限循环

import static org.junit.Assert.assertEquals;

import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import com.google.common.base.Functions;
import com.google.common.collect.Ordering;

class ValueComparableMap<K extends Comparable<K>,V> extends TreeMap<K,V> {
    //A map for doing lookups on the keys for comparison so we don't get infinite loops
    private final Map<K, V> valueMap;

    ValueComparableMap(final Ordering<? super V> partialValueOrdering) {
        this(partialValueOrdering, new HashMap<K,V>());
    }

    private ValueComparableMap(Ordering<? super V> partialValueOrdering,
            HashMap<K, V> valueMap) {
        super(partialValueOrdering //Apply the value ordering
                .onResultOf(Functions.forMap(valueMap)) //On the result of getting the value for the key from the map
                .compound(Ordering.natural())); //as well as ensuring that the keys don't get clobbered
        this.valueMap = valueMap;
    }

    public V put(K k, V v) {
        if (valueMap.containsKey(k)){
            //remove the key in the sorted set before adding the key again
            remove(k);
        }
        valueMap.put(k,v); //To get "real" unsorted values for the comparator
        return super.put(k, v); //Put it in value order
    }

    public static void main(String[] args){
        TreeMap<String, Integer> map = new ValueComparableMap<String, Integer>(Ordering.natural());
        map.put("a", 5);
        map.put("b", 1);
        map.put("c", 3);
        assertEquals("b",map.firstKey());
        assertEquals("a",map.lastKey());
        map.put("d",0);
        assertEquals("d",map.firstKey());
        //ensure it's still a map (by overwriting a key, but with a new value) 
        map.put("d", 2);
        assertEquals("b", map.firstKey());
        //Ensure multiple values do not clobber keys
        map.put("e", 2);
        assertEquals(5, map.size());
        assertEquals(2, (int) map.get("e"));
        assertEquals(2, (int) map.get("d"));
    }
 }

当我们放入时,我们确保哈希映射具有比较器的值,然后将其放入TreeSet进行排序。但在此之前,我们检查哈希图,看看该键实际上不是重复的。此外,我们创建的比较器还将包括关键字,这样重复的值就不会删除非重复的关键字(由于==比较)。这两项对于确保地图合同得到遵守至关重要;如果你认为你不想这样,那么你几乎就要完全颠倒地图了(地图<V,K>)。

构造函数需要调用为

 new ValueComparableMap(Ordering.natural());
 //or
 new ValueComparableMap(Ordering.from(comparator));

其他回答

这个问题已经有了很多答案,但没有一个能为我提供我想要的,一个返回按关联值排序的键和条目的映射实现,并在映射中修改键和值时维护这个属性。另外两个问题对此提出了具体要求。

我编写了一个通用友好的示例来解决这个用例。此实现不遵守Map接口的所有约定,例如反映原始对象中keySet()和entrySet()返回的集合中的值更改和删除。我觉得这样的解决方案太大,无法包含在堆栈溢出的答案中。如果我成功地创建了一个更完整的实现,也许我会将其发布到Github,然后在这个答案的更新版本中链接到它。

import java.util.*;

/**
 * A map where {@link #keySet()} and {@link #entrySet()} return sets ordered
 * by associated values based on the the comparator provided at construction
 * time. The order of two or more keys with identical values is not defined.
 * <p>
 * Several contracts of the Map interface are not satisfied by this minimal
 * implementation.
 */
public class ValueSortedMap<K, V> extends HashMap<K, V> {
    protected Map<V, Collection<K>> valueToKeysMap;

    // uses natural order of value object, if any
    public ValueSortedMap() {
        this((Comparator<? super V>) null);
    }

    public ValueSortedMap(Comparator<? super V> valueComparator) {
        this.valueToKeysMap = new TreeMap<V, Collection<K>>(valueComparator);
    }

    public boolean containsValue(Object o) {
        return valueToKeysMap.containsKey(o);
    }

    public V put(K k, V v) {
        V oldV = null;
        if (containsKey(k)) {
            oldV = get(k);
            valueToKeysMap.get(oldV).remove(k);
        }
        super.put(k, v);
        if (!valueToKeysMap.containsKey(v)) {
            Collection<K> keys = new ArrayList<K>();
            keys.add(k);
            valueToKeysMap.put(v, keys);
        } else {
            valueToKeysMap.get(v).add(k);
        }
        return oldV;
    }

    public void putAll(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
            put(e.getKey(), e.getValue());
    }

    public V remove(Object k) {
        V oldV = null;
        if (containsKey(k)) {
            oldV = get(k);
            super.remove(k);
            valueToKeysMap.get(oldV).remove(k);
        }
        return oldV;
    }

    public void clear() {
        super.clear();
        valueToKeysMap.clear();
    }

    public Set<K> keySet() {
        LinkedHashSet<K> ret = new LinkedHashSet<K>(size());
        for (V v : valueToKeysMap.keySet()) {
            Collection<K> keys = valueToKeysMap.get(v);
            ret.addAll(keys);
        }
        return ret;
    }

    public Set<Map.Entry<K, V>> entrySet() {
        LinkedHashSet<Map.Entry<K, V>> ret = new LinkedHashSet<Map.Entry<K, V>>(size());
        for (Collection<K> keys : valueToKeysMap.values()) {
            for (final K k : keys) {
                final V v = get(k);
                ret.add(new Map.Entry<K,V>() {
                    public K getKey() {
                        return k;
                    }

                    public V getValue() {
                        return v;
                    }

                    public V setValue(V v) {
                        throw new UnsupportedOperationException();
                    }
                });
            }
        }
        return ret;
    }
}

以下是通用友好版本:

public class MapUtil {
    public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) {
        List<Entry<K, V>> list = new ArrayList<>(map.entrySet());
        list.sort(Entry.comparingByValue());

        Map<K, V> result = new LinkedHashMap<>();
        for (Entry<K, V> entry : list) {
            result.put(entry.getKey(), entry.getValue());
        }

        return result;
    }
}

如果您的Map值实现Comparable(例如String),那么这应该会起作用

Map<Object, String> map = new HashMap<Object, String>();
// Populate the Map
List<String> mapValues = new ArrayList<String>(map.values());
Collections.sort(mapValues);

如果映射值本身没有实现Comparable,但您有一个Comparable实例可以对它们进行排序,请将最后一行替换为:

Collections.sort(mapValues, comparable);

我已经查看了给定的答案,但很多答案比需要的要复杂,或者在多个键具有相同值时删除映射元素。

以下是我认为更适合的解决方案:

public static <K, V extends Comparable<V>> Map<K, V> sortByValues(final Map<K, V> map) {
    Comparator<K> valueComparator =  new Comparator<K>() {
        public int compare(K k1, K k2) {
            int compare = map.get(k2).compareTo(map.get(k1));
            if (compare == 0) return 1;
            else return compare;
        }
    };
    Map<K, V> sortedByValues = new TreeMap<K, V>(valueComparator);
    sortedByValues.putAll(map);
    return sortedByValues;
}

请注意,地图是从最高值到最低值排序的。

对键进行排序需要Comparator为每个比较查找每个值。一个更具可扩展性的解决方案将直接使用entrySet,因为这样每次比较都会立即获得该值(尽管我没有用数字来支持)。

这是这样一件事的通用版本:

public static <K, V extends Comparable<? super V>> List<K> getKeysSortedByValue(Map<K, V> map) {
    final int size = map.size();
    final List<Map.Entry<K, V>> list = new ArrayList<Map.Entry<K, V>>(size);
    list.addAll(map.entrySet());
    final ValueComparator<V> cmp = new ValueComparator<V>();
    Collections.sort(list, cmp);
    final List<K> keys = new ArrayList<K>(size);
    for (int i = 0; i < size; i++) {
        keys.set(i, list.get(i).getKey());
    }
    return keys;
}

private static final class ValueComparator<V extends Comparable<? super V>>
                                     implements Comparator<Map.Entry<?, V>> {
    public int compare(Map.Entry<?, V> o1, Map.Entry<?, V> o2) {
        return o1.getValue().compareTo(o2.getValue());
    }
}

有一些方法可以减少上述解决方案的内存旋转。例如,创建的第一个ArrayList可以重新用作返回值;这将需要抑制一些泛型警告,但对于可重用的库代码来说,这可能是值得的。此外,Comparator不必在每次调用时重新分配。

这里有一个更有效但不太吸引人的版本:

public static <K, V extends Comparable<? super V>> List<K> getKeysSortedByValue2(Map<K, V> map) {
    final int size = map.size();
    final List reusedList = new ArrayList(size);
    final List<Map.Entry<K, V>> meView = reusedList;
    meView.addAll(map.entrySet());
    Collections.sort(meView, SINGLE);
    final List<K> keyView = reusedList;
    for (int i = 0; i < size; i++) {
        keyView.set(i, meView.get(i).getKey());
    }
    return keyView;
}

private static final Comparator SINGLE = new ValueComparator();

最后,如果您需要连续访问已排序的信息(而不是偶尔排序一次),可以使用额外的多重映射。如果你需要更多细节,请告诉我。。。