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

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

有没有更简单的方法?


当前回答

在TreeMap中,键按自然顺序排序。例如,如果您对数字进行排序,(注意4的排序)

{0=0, 10=10, 20=20, 30=30, 4=4, 50=50, 60=60, 70=70}

要解决这个问题,在Java8中,首先检查字符串长度,然后进行比较。

Map<String, String> sortedMap = new TreeMap<>Comparator.comparingInt(String::length)
.thenComparing(Function.identity()));

{0=0, 4=4, 10=10, 20=20, 30=30, 50=50, 60=60, 70=70}

其他回答

我建议使用Arrays.sort,而不是像某些人那样使用Collections.ort。实际上Collections.ort的作用是这样的:

public static <T extends Comparable<? super T>> void sort(List<T> list) {
    Object[] a = list.toArray();
    Arrays.sort(a);
    ListIterator<T> i = list.listIterator();
    for (int j=0; j<a.length; j++) {
        i.next();
        i.set((T)a[j]);
    }
}

它只调用列表上的array,然后使用Arrays.sort。这样,所有映射条目将被复制三次:一次从映射复制到临时列表(无论是LinkedList还是ArrayList),然后复制到临时数组,最后复制到新映射。

我的解决方案省略了这一步,因为它不会创建不必要的LinkedList。以下是代码,通用友好,性能最佳:

public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) 
{
    @SuppressWarnings("unchecked")
    Map.Entry<K,V>[] array = map.entrySet().toArray(new Map.Entry[map.size()]);

    Arrays.sort(array, new Comparator<Map.Entry<K, V>>() 
    {
        public int compare(Map.Entry<K, V> e1, Map.Entry<K, V> e2) 
        {
            return e1.getValue().compareTo(e2.getValue());
        }
    });

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

    return result;
}

重要说明:

此代码可以以多种方式中断。如果您打算使用提供的代码,请务必阅读注释以了解其含义。例如,值不能再通过其键检索。(get始终返回null。)


这似乎比前面所有的都容易得多。按如下方式使用TreeMap:

public class Testing {
    public static void main(String[] args) {
        HashMap<String, Double> map = new HashMap<String, Double>();
        ValueComparator bvc = new ValueComparator(map);
        TreeMap<String, Double> sorted_map = new TreeMap<String, Double>(bvc);

        map.put("A", 99.5);
        map.put("B", 67.4);
        map.put("C", 67.4);
        map.put("D", 67.3);

        System.out.println("unsorted map: " + map);
        sorted_map.putAll(map);
        System.out.println("results: " + sorted_map);
    }
}

class ValueComparator implements Comparator<String> {
    Map<String, Double> base;

    public ValueComparator(Map<String, Double> base) {
        this.base = base;
    }

    // Note: this comparator imposes orderings that are inconsistent with
    // equals.
    public int compare(String a, String b) {
        if (base.get(a) >= base.get(b)) {
            return -1;
        } else {
            return 1;
        } // returning 0 would merge keys
    }
}

输出:

unsorted map: {D=67.3, A=99.5, B=67.4, C=67.4}
results: {D=67.3, B=67.4, C=67.4, A=99.5}

要使用Java 8中的新功能实现这一点,请执行以下操作:

import static java.util.Map.Entry.comparingByValue;
import static java.util.stream.Collectors.toList;

<K, V> List<Entry<K, V>> sort(Map<K, V> map, Comparator<? super V> comparator) {
    return map.entrySet().stream().sorted(comparingByValue(comparator)).collect(toList());
}

条目使用给定的比较器按其值排序。或者,如果您的值可以相互比较,则不需要显式比较器:

<K, V extends Comparable<? super V>> List<Entry<K, V>> sort(Map<K, V> map) {
    return map.entrySet().stream().sorted(comparingByValue()).collect(toList());
}

返回的列表是调用此方法时给定映射的快照,因此两者都不会反映对另一个的后续更改。对于地图的实时可迭代视图:

<K, V extends Comparable<? super V>> Iterable<Entry<K, V>> sort(Map<K, V> map) {
    return () -> map.entrySet().stream().sorted(comparingByValue()).iterator();
}

返回的可迭代对象在每次迭代时都会创建给定映射的新快照,因此除非并发修改,否则它将始终反映映射的当前状态。

一些简单的更改,以便具有具有重复值的对的排序映射。在比较方法(类ValueComparator)中,当值相等时,不返回0,而是返回比较2个键的结果。关键点在地图中是不同的,因此您可以成功地保留重复的值(顺便按关键点排序)。因此,上面的示例可以这样修改:

    public int compare(Object a, Object b) {

        if((Double)base.get(a) < (Double)base.get(b)) {
          return 1;
        } else if((Double)base.get(a) == (Double)base.get(b)) {
          return ((String)a).compareTo((String)b);
        } else {
          return -1;
        }
      }
    }

三个单行答案。。。

我会使用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));