Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unexpected behavior when using Comparator.comparing(HashMap::get) as a comparator

Doing the exercise 'Literature' on https://java-programming.mooc.fi/part-10/2-interface-comparable I discovered a very strange behavior when trying to sort key-value pairs in a HashMap, without copying anything to a TreeMap. I was supposed to add books, by making a Book class and adding them to a List. However I wanted to try without making a new class, so opted for the HashMap. My code was as follows:

public class MainProgram {

public static void main(String[] args) {
    Scanner scanner = new Scanner(System.in);

    Map<String, Integer> bookshelf = new HashMap<>();
    while (true) {


        System.out.println("Input the name of the book, empty stops: ");
        String bookName = scanner.nextLine();
        if (bookName.equals("")) {
            break;
        }
        System.out.println("Input the age recommendation: ");
        int age = Integer.valueOf(scanner.nextLine());

        bookshelf.put(bookName, age);
    }

    System.out.println(bookshelf.size() + " book" + (bookshelf.size() > 1 ? "s" : "") + " in total.");

    System.out.println("Books:");

    bookshelf.keySet().stream().sorted(Comparator.comparing(bookshelf::get)).forEach((key) -> System.out.println(key + " (recommended for " + bookshelf.get(key) + " year-olds or older)"));
}

}

using .sorted(Comparator.comparing(bookshelf::get)) was my idea of sorting them by the recommended age, which worked.

However, there exists an unexpected behavior that when the book's name is a single character ("A","b"), the program would also sort the keys alphabetically as though i made a comparator like Comparator.comparing(bookshelf::get).thenComparing(/*keys in keyset*/) but would sometimes also sort like aAbB

AA bb give unsorted results
AAA bbb give semi-sorted results in one or two buckets
AAAA bbbb give semi- or completely sorted results
AAAAA bbbbb and onward give unsorted results.

enter image description here

Can anybody explain what is happening here, at compiler level or somehow let me make sense of this?

like image 232
Milos Cupara Avatar asked Jun 04 '20 16:06

Milos Cupara


People also ask

What does Comparator comparing do in Java?

comparing. Accepts a function that extracts a sort key from a type T , and returns a Comparator<T> that compares by that sort key using the specified Comparator . The returned comparator is serializable if the specified function and comparator are both serializable.

How does Comparator compare work?

Compares its two arguments for order. Returns a negative integer, zero, or a positive integer as the first argument is less than, equal to, or greater than the second. That is all there is to this. When you write a Comparator, you define what order you want.

What is Comparator in sorting?

Using a comparator, we can sort the elements based on data members. For instance, it may be on roll no, name, age, or anything else. Method of Collections class for sorting List elements is used to sort the elements of List by the given comparator.

What is Comparator chaining in Java?

The ComparatorChain implements the Comparator interface and is an aggregate of other Comparator objects. It can be used wherever a Comparator is used, including array sorting and as a Comparator for a tree-based Collection implementation. The following example demonstrates the use of a chained Comparator.


3 Answers

bookshelf.keySet().stream().sorted(Comparator.comparing(bookshelf::get))

From the above snippet in your example, we can see that you're trying to sort the keys of bookshelf by their respective value.

The issue with this is that two book names could be mapped to the same age recommendation. Because you only have a single Comparator and because HashMap does not specify a consistent ordering, you have a chance at ending up with different results for the same inputs.

To ameliorate this, you can use thenComparing to handle the case when duplicate value-mappings are encountered:

bookshelf.entrySet()
         .stream()
         .sorted(Map.Entry.<String, Integer>comparingByValue().thenComparing(Map.Entry.comparingByKey()))
         .forEach(entry -> System.out.println(entry.getKey() + " (recommended for " + entry.getValue() + " year-olds or older)"));
like image 115
Jacob G. Avatar answered Oct 13 '22 03:10

Jacob G.


Build the Comparator of Entry and use Entry::getValue and Entry::getKey to sort by value then by key

Comparator<Entry<String, Integer>> cmp = Comparator.comparing(Entry::getValue);

bookshelf.entrySet()
         .stream()
         .sorted(cmp.thenComparing(Entry::getKey))
         .forEach(entry -> System.out.println(entry.getKey() + " (recommended for " + entry.getValue() + " year-olds or older)"));
like image 29
heaprc Avatar answered Oct 13 '22 03:10

heaprc


This is happening since you are only using "key" to compare. You should compare them by both "key" and "value". This should work fine:

bookshelf.entrySet()
        .stream()
        .sorted(Map.Entry.<String,Integer>comparingByValue()
                .thenComparing(Map.Entry.comparingByKey()))
        .map(e -> e.getKey())
        .forEach((key) -> System.out.println(key + " (recommended for " + bookshelf.get(key) + " year-olds or older)"));
like image 4
Aniket Sahrawat Avatar answered Oct 13 '22 04:10

Aniket Sahrawat