Build1 distinct publisher3 min readPublished
Java throws "Comparison method violates its general contract" when TimSort's merge invariants collapse under a comparator that breaks antisymmetry, and the collapse happens only when the data supplies the contradictory pair, which is why the test suite and staging never saw it.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
Call `.sorted(cmp)` on a stream and the comparator is no longer yours to schedule. The stream buffers into an array and hands it to `Arrays.sort` [2], which for objects has been TimSort since Java 7: a merge sort that scans for runs that are already in order and merges them with a galloping search [3]. Galloping is where the trust sits. When TimSort probes ahead inside a run it assumes that an element which compared less stays less from whichever direction it asks [3]. The three laws in the JavaDoc exist to make that assumption safe: antisymmetry, transitivity, and equal elements ordering identically against everything else [4].
The comparator, inherited with an abandoned flight-operations service [18], broke the first law in one line. It split each parking-area code into a digit part and a letter part, compared the numbers, and ended its branch chain with `else if (numPart1.isBlank()) return 1;` [10]. That branch fires without looking at the second argument. For two codes with no digits it therefore returns 1 in both directions: `compare("A","B") == 1` and `compare("B","A") == 1` [11]. It also returns 1 for `compare("A","A")` [12], which is a strong opinion to hold about one piece of tarmac.
Antisymmetry demands that the pairwise matrix mirror-negate across a zero diagonal, and the numeric codes obey [14]. The digit-free corner is a solid block of `+1`: nine cells, three of them on the diagonal [14]. Nine cells with three on the diagonal is a 3x3 block, so exactly three distinct digit-free key values were in play, which matches letter-only `A`, letter-only `B`, and the empty string that an absent parking area collapses to [17][9].
That geometry is also the schedule of the outage. Most stands carry ordinary letter-plus-number codes, and on those the comparator is honest, so the sort completes [15]. The exception needs enough letter-only or parking-less states in a single response, positioned so that a merge compares the same pair from both directions [15]. When the data does not supply that, TimSort returns an order, correct or quietly wrong, and says nothing [5].
Which is why the JVM flag is the wrong door. `-Djava.util.Arrays.useLegacyMergeSort=true` puts back the pre-Java-7 merge sort, the one that would have produced a wrong order in silence [7][8]. The dev.to writeup calls reaching for it the classic wrong fix [8], and on the mechanism that is correct: it changes what the JDK tells you, not what the comparator computes.
What has to be true for this to be your bug rather than one airport's: the sort key is a string parsed into parts, at least one branch returns a constant instead of comparing, and the live population contains two or more values that reach that branch. The first two conditions are visible in review, and in this case the letter comparison written for exactly that case sat one line below, unreachable [13]. The third condition belongs to the data, so the check I trust in my own services is to run the comparator over the cross product of the distinct key values production actually holds, and assert the grid mirror-negates across a zero diagonal [14].
Ranked by verification strength, evidence, and original report placement.
The repair restructured the branches so that every case returns a value.
A production endpoint that lists aircraft stand states ordered by parking-area code aborted with java.lang.IllegalArgumentException: Comparison method violates its general contract!, thrown from a stream's .sorted(comparator) call; it failed only sometimes, on some data, not in tests, not on staging and not reproducibly on demand.
Under the hood, a stream's sorted() buffers into an array and hands it to Arrays.sort, which is TimSort.
Since Java 7, Arrays.sort for objects is TimSort, a merge sort that hunts for already-ordered runs and merges them with a galloping optimization; that machinery relies on the total-order laws, trusting that what compared less stays less from every direction it checks.
The Comparator JavaDoc demands three laws: antisymmetry, sgn(compare(a,b)) == -sgn(compare(b,a)); transitivity, if a beats b and b beats c then a beats c; and consistency, if compare(a,b) == 0 then a and b must agree in how they compare against everything else.
If the input does not expose a lying comparator, the sort completes, perhaps correctly and perhaps subtly misordered; if the input does expose it, TimSort's internal invariants collapse and it throws the contract exception rather than return garbage.
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · September 3, 2026
Follow any of these and your For You feed starts watching them — no settings page required.
build
A green @DataJpaTest can prove only that Hibernate handed back the same object1 distinct publisher
build
An ADR routes refund eligibility to three plain-Java rules the CI can actually assert1 distinct publisher
build
Shared memory in Java: the mmap is the easy half, the descriptor handoff is the work1 distinct publisher
build
Spring's @Scheduled swallows the exception and reports success to the scheduler1 distinct publisher
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Checkable mechanism, unverifiable incident
Two kinds of claim sit in this story and they do not carry equal weight. The JDK half — stream sorting handing off to Arrays.sort, TimSort's galloping merges, the three laws in the Comparator JavaDoc, the legacy-merge-sort flag — is confirmable by anyone with a JDK open. The incident half concerns a codebase nobody else can look at: the broken branch is quoted verbatim, which is the strongest single piece of evidence here, but the fix is explicitly labelled illustrative rather than production source, and there is no timeline, request count or stack trace beyond the exception line. dev.to's account is also the account of the person who wrote the fix.
Nothing to count
There is no uptake to measure. The fix lands inside a private flight-operations service that is never named, with no commit, release, version, deploy date or before-and-after error rate attached, and the follow-on audit of the sibling comparators is described in the same unverified voice. Rather than dress one team's afternoon of work as a trend, we leave this unscored.
Pitched smaller than it is
This could easily have been written as a JDK horror story and wasn't. dev.to walks the usual claim back — 'it isn't a JDK bug, it's the JDK catching you' — and volunteers the inconvenient parts: the second landmine had never actually fired, absence of the exception proves nothing, and the flag that makes the crash vanish is named as the wrong fix rather than a workaround. The one rhetorical stretch is calling this the most misunderstood exception in Java. If anything the general case is undersold: every comparator with a branch that ignores one of its arguments is the same landmine, whatever the domain.
Portfolio piece, no product attached
Worth naming the shape: instalment three of a series in which the author's own algorithms star and the inherited code plays the villain reads as résumé as much as postmortem. But nothing is being sold — no vendor, framework, service or tool whose adoption this reporting would help, and no funding story in the frame. The pressure it does create falls on verifiability, since the platform stays anonymous and the repaired comparator arrives as a sketch.
Confident on the machinery, cautious on the anecdote
We are on firm ground about what TimSort does when handed a comparator that contradicts itself, and on soft ground about whether this endpoint, this data mix and these intermittent failures happened as described. One publisher, no corroboration, no artifacts. The workable reading: take the mechanism as fact and the case study as a plausible, well-told illustration of it.