Codementor Events

Sortring a Java 8 stream and notes on time-based comparison

Published Feb 09, 2017

I recently stumbled upon this little feature where I needed to reverse a stream based on a property and then sort after another property. The solution is quite simple but I want to share the idea with you.

Naturally you can look at StackOverflow (via Google) to see how you can reverse a stream. There are some solutions which work for primitive-type streams because the question is based on Primitive types.

But my problem occurred with a stream of a custom type: a class of my own.

Background

I was writing a simple code for a word challenge game. I has a given task with a code-base to finish (I do not write down the details because you may encounter it as an interview question / assignment and it won't be fair if you would solve the task without knowing why you code what you do).

I solved the task as most people would do: I have broken down the requirements into simple tasks and finished the project. After that I went a step further and tried to improve my solution.

Then it came into my mind to abandon lists and iteration but use a set and sort every time it was needed. Naturally, sorting every time can take time but for the idea it is worth trying -- and after that you can decide which solution to use.

The Score class

The following code snippet shows a part of the Score class I used to keep game information, and instances of this class were stored in the Set:

class Score {
    private final int score;
    private final String word;
    private final String playerName;
    private final LocalDateTime added;

    public Score(final int score, final String word, final String playerName, final LocalDateTime added) {
        this.score = score;
        this.word = word;
        this.playerName = playerName;
        this.added = added;
    }
    // getters and setters omitted
    @Override
    public boolean equals(Object obj) {
        // method body omitted
    }
    
    @Override
    public int hashCode() {
        // method body omitted
    }

As you can see I have added a hashCode implementation because it is required to store elements efficiently in a HashSet and the equals method is required by the contract too.

Solution

As you can see below, the solution is easy. You do not need to have a comparable object (this means that the Score class does not need to implement different interfaces to enable comparison).

this.leaderBoard.stream()
        .sorted(Comparator.comparing(Score::getScore)
                 .reversed()
                 .thenComparing(Score::getAdded)
        );

The idea behind all this is that you can chain comparisons too. As you can see first I sort the objects by their score, then I reverse the list and after that I sort by their timestamp.

Remarks on tests with time comparison

If you think about the solution and want to write unit tests you may end-up in some inconsistencies. This is because of the comparison based on time. Although LocalDateTime objects use nanoseconds as the smallest comparison unit, it can happen that method calls are executed at the same time (literally at the same time) and therefore you cannot rely on the order. This can be verified with a basic test:

@Test
public void testFullLeaderboard() {
    this.service.submitWord("player1", "all");
    this.service.submitWord("player2", "word");
    this.service.submitWord("player5", "woolly");
    this.service.submitWord("player1", "aa");
    this.service.submitWord("player2", "arrow");
    this.service.submitWord("player5", "world");
    this.service.submitWord("player1", "ally");
    this.service.submitWord("player2", "wood");
    this.service.submitWord("player5", "wooer");
    this.service.submitWord("player1", "wool");

    assertEquals("player5", this.service.getPlayerNameAtPosition(0));
    assertEquals("player2", this.service.getPlayerNameAtPosition(1));
    assertEquals("player5", this.service.getPlayerNameAtPosition(2));
    assertEquals("player5", this.service.getPlayerNameAtPosition(3));
    assertEquals("player2", this.service.getPlayerNameAtPosition(4));
    assertEquals("player1", this.service.getPlayerNameAtPosition(5));
}

This test case may work. But if you have a slow computer or are just sheer lucky. Well, I have run it 20 times and from these 20 times only 7 were successful. And if I include more test cases (there are 10 different words and therefore 10 different positions) this ratio gets even worse.

Solution 2

After this pitfall I had to change the timestamp-comparison to something more stable, where I can rely on the order but I do not have to give-up the Set-approach. So my solution was to use a long field which is incremented in a thread-safe manner every time it is accessed. And the AtomicLong class is ideal for this.

After this change the comparison code looks like this:

this.leaderBoard.stream()
        .sorted(Comparator.comparing(Score::getScore)
                 .reversed()
                 .thenComparing(Score::getOrder)
        );

Now the second comparison is based on the long order field of each Score object.

Conclusion

Java 8 gives you methods to do complex sorting on non-primitive datatypes even without implementing a specialized interface. This gives you the opportunity to create "SQL-like" queries.

Comparison based on timestamps is OK unless you do your work on very fast machines or concurrency is an option and multiple threads do the same task at the same time.

Discover and read more posts from Gábor László Hajba
get started
post commentsBe the first to share your opinion
Show more replies