У меня есть древовидная карта с некоторыми строками и частотами этих строк. Я хочу сортировать по частотам, поэтому решил написать собственный компаратор. Что не так с этим фрагментом кода? Это вызывает исключение.
TreeMap<String, Integer> map = new TreeMap(new Comparator<Map.Entry<String, Integer>>(){
public int compare(Map.Entry<String, Integer> m1, Map.Entry<String, Integer> m2) {
return m2.getValue() - m1.getValue();
}
});
Исключение
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.util.Map$Entry (java.lang.String and java.util.Map$Entry are in module java.base of loader 'bootstrap')
(простое объявление, позволяющее компилятору выводить [почти все] типы, используя <>
: TreeMap<String, Integer> map = new TreeMap<>(new Comparator<>(){ @Override public int compare(String o1, String o2) { ...
- но менее полезно, поскольку вы не хотите сортировать по ключу, поскольку компаратор может использоваться до добавления ключа/значения)
Чтобы прекратить погоню: на самом деле ваш вопрос: «Как мне отсортировать карту на основе ее значений», а ответ: «Вы не можете». По крайней мере, нет TreeMap
. Ни одна карта в пакете java.*
не справится с этой задачей. Обычно вы переписываете свой код так, чтобы значения теперь были ключами или частью ключа.
Вы используете неправильный тип для компаратора. Конструктор выглядит следующим образом:
public TreeMap(Comparator<? super K> comparator)
где K
— тип ключа Map
.
Это означает, что вам нужно передать конструктору Comparator<String>
.
Вы хотите отсортировать Map
, используя значения, это невозможно с помощью конструктора. Вам нужно будет отсортировать записи вручную с помощью entrySet()
и компаратора, который вы сейчас используете.
Как я могу отсортировать HashMap/TreeMap на основе значений? Все ответы, которые я видел в Интернете, заключаются сначала в добавлении Map.entrySet() в список, а затем в его сортировке по значениям. Можем ли мы сделать это, не используя дополнительную структуру данных/пространство? Я имею в виду просто использование компаратора.
TreeMap
в Java специально разработан для сортировки записей по ключам, а не по значениям. Компаратор, который вы предоставляете TreeMap
, используется только для сравнения ключей. Если вы попытаетесь использовать компаратор, который ожидает другой тип (например, Map.Entry<String, Integer>
), TreeMap
попытается привести ключ к этому типу. В результате появится ClassCastException
, например java.lang.String cannot be cast to java.util.Map$Entry
, поскольку ключи не ожидаемого типа.
Одна из проблем, как описано в JavaDoc для TreeMap, заключается в том, что карта сортируется по ключам, а не по значениям.
public TreeMap (Comparator<? super K> компаратор) Создает новую пустую древовидную карту, упорядоченную в соответствии с заданным компаратором. Все ключи, вставленные в карту, должны быть взаимно сопоставимы с помощью данного компаратора: comparator.compare(k1, k2) не должен генерировать исключение ClassCastException для каких-либо ключей k1 и k2 на карте. Если пользователь попытается поместить в карту ключ, который нарушает это ограничение, вызов put(Object key, Object value) выдаст исключение ClassCastException.
Чтобы решить эту проблему, сначала создайте обычную карту, а затем отсортируйте ее.
Map<String, Integer> map = new HashMap<>();
map.put("A", 30);
map.put("B", 20);
map.put("D", 50);
map.put("C", 50);
После заполнения карты вы можете выполнить сортировку. Я также сортирую по key
, если значения равны. Если вы этого не хотите, просто удалите оператор comp = comp.thenComparing...
.
Я поместил отсортированные значения в LinkedHashMap
, чтобы сохранить отсортированный порядок.
Comparator<Entry<String, Integer>> comp = Entry
.comparingByValue(Comparator.reverseOrder());
comp = comp.thenComparing(Entry.comparingByKey());
map = map.entrySet().stream()
.sorted(comp).collect(Collectors.toMap(Entry::getKey,
Entry::getValue, (a, b) -> a, LinkedHashMap::new));
map.entrySet().forEach(System.out::println);
принты
C=50
D=50
A=30
B=20
Другой подход — использовать частоты в качестве ключей, а значения — список тех строк, которые соответствуют частотам.
Map<Integer, List<String>> map2 = new TreeMap<>(Comparator.reverseOrder());
map2.computeIfAbsent(30, k-> new ArrayList<>()).add("A");
map2.computeIfAbsent(20, k-> new ArrayList<>()).add("B");
map2.computeIfAbsent(50, k-> new ArrayList<>()).add("D");
map2.computeIfAbsent(50, k-> new ArrayList<>()).add("C");
map2.entrySet().forEach(System.out::println);
принты
50=[D, C]
30=[A]
20=[B]
Метод Map.computeIfAbsent
создаст ArrayList
для значения, если ключ не существует. Независимо от того, только что созданный или уже существующий, он будет возвращен, чтобы можно было добавить string
в список для данного ключа.
Наконец, вы можете просто не использовать карту, а использовать TreeSet
с тем же компаратором comp
, что и выше. Заполните набор Map.entry
, домашним классом или record
.
SortedSet<Entry<String, Integer>> sortedSet = new TreeSet<>(comp);
sortedSet.forEach(System.out::println);
объявление new
TreeMap(...
использует необработанный тип и, таким образом, запрещает любую проверку универсального типа, иначе этот код не будет компилироваться - Спецификация языка Java 4.8. Необработанные типы: «Использование необработанных типов разрешено только как уступка совместимости устаревшего кода. Использование необработанных типов в коде, написанном после введения дженериков в язык программирования Java, настоятельно не рекомендуется».