Java TreeMap сортирует по компаратору значений – почему это вызывает ClassCastException

У меня есть древовидная карта с некоторыми строками и частотами этих строк. Я хочу сортировать по частотам, поэтому решил написать собственный компаратор. Что не так с этим фрагментом кода? Это вызывает исключение.

        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')

объявление new TreeMap(... использует необработанный тип и, таким образом, запрещает любую проверку универсального типа, иначе этот код не будет компилироваться - Спецификация языка Java 4.8. Необработанные типы: «Использование необработанных типов разрешено только как уступка совместимости устаревшего кода. Использование необработанных типов в коде, написанном после введения дженериков в язык программирования Java, настоятельно не рекомендуется».

user85421 17.08.2024 15:14

(простое объявление, позволяющее компилятору выводить [почти все] типы, используя <>: TreeMap<String, Integer> map = new TreeMap<>(new Comparator<>(){ @Override public int compare(String o1, String o2) { ... - но менее полезно, поскольку вы не хотите сортировать по ключу, поскольку компаратор может использоваться до добавления ключа/значения)

user85421 17.08.2024 15:22

Чтобы прекратить погоню: на самом деле ваш вопрос: «Как мне отсортировать карту на основе ее значений», а ответ: «Вы не можете». По крайней мере, нет TreeMap. Ни одна карта в пакете java.* не справится с этой задачей. Обычно вы переписываете свой код так, чтобы значения теперь были ключами или частью ключа.

rzwitserloot 17.08.2024 15:27
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
3
3
77
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Вы используете неправильный тип для компаратора. Конструктор выглядит следующим образом:

public TreeMap(Comparator<? super K> comparator)

где K — тип ключа Map.

Это означает, что вам нужно передать конструктору Comparator<String>.

Вы хотите отсортировать Map, используя значения, это невозможно с помощью конструктора. Вам нужно будет отсортировать записи вручную с помощью entrySet() и компаратора, который вы сейчас используете.

Как я могу отсортировать HashMap/TreeMap на основе значений? Все ответы, которые я видел в Интернете, заключаются сначала в добавлении Map.entrySet() в список, а затем в его сортировке по значениям. Можем ли мы сделать это, не используя дополнительную структуру данных/пространство? Я имею в виду просто использование компаратора.

Gauraang Khurana 17.08.2024 15:08
Ответ принят как подходящий

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);

Другие вопросы по теме