After looking at the code of ComparableTimSort I am not quite sure. Let's analyze it. Here is the only method that throws it (there is a similar method that does the same only with exchanged roles, so analyzing one of them is enough).

private void mergeLo(int base1, int len1, int base2, int len2) {
        assert len1 > 0 && len2 > 0 && base1 + len1 == base2;

        // Copy first run into temp array
        Object[] a = this.a; // For performance
        Object[] tmp = ensureCapacity(len1);

        int cursor1 = tmpBase; // Indexes into tmp array
        int cursor2 = base2;   // Indexes int a
        int dest = base1;      // Indexes int a
        System.arraycopy(a, base1, tmp, cursor1, len1);

        // Move first element of second run and deal with degenerate cases
        a[dest++] = a[cursor2++];
        if (--len2 == 0) {
            System.arraycopy(tmp, cursor1, a, dest, len1);
            return;
        }
        if (len1 == 1) {
            System.arraycopy(a, cursor2, a, dest, len2);
            a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
            return;
        }

        int minGallop = this.minGallop;  // Use local variable for performance
    outer:
        while (true) {
            int count1 = 0; // Number of times in a row that first run won
            int count2 = 0; // Number of times in a row that second run won

            /*
             * Do the straightforward thing until (if ever) one run starts
             * winning consistently.
             */
// ------------------ USUAL MERGE
            do {
                assert len1 > 1 && len2 > 0;
                if (((Comparable) a[cursor2]).compareTo(tmp[cursor1]) < 0) {
                    a[dest++] = a[cursor2++];
                    count2++;
                    count1 = 0;
                    if (--len2 == 0)
                        break outer;
                } else {
                    a[dest++] = tmp[cursor1++];
                    count1++;
                    count2 = 0;
                    if (--len1 == 1)
                        break outer;
                }
            } while ((count1 | count2) < minGallop);

// ------------------ GALLOP
            /*
             * One run is winning so consistently that galloping may be a
             * huge win. So try that, and continue galloping until (if ever)
             * neither run appears to be winning consistently anymore.
             */
            do {
                assert len1 > 1 && len2 > 0;
                count1 = gallopRight((Comparable) a[cursor2], tmp, cursor1, len1, 0);
                if (count1 != 0) {
                    System.arraycopy(tmp, cursor1, a, dest, count1);
                    dest += count1;
                    cursor1 += count1;
                    len1 -= count1;
// -->>>>>>>> HERE IS WHERE GALLOPPING TOO FAR WILL TRIGGER THE EXCEPTION
                    if (len1 <= 1)  // len1 == 1 || len1 == 0
                        break outer;
                }
                a[dest++] = a[cursor2++];
                if (--len2 == 0)
                    break outer;

                count2 = gallopLeft((Comparable) tmp[cursor1], a, cursor2, len2, 0);
                if (count2 != 0) {
                    System.arraycopy(a, cursor2, a, dest, count2);
                    dest += count2;
                    cursor2 += count2;
                    len2 -= count2;
                    if (len2 == 0)
                        break outer;
                }
                a[dest++] = tmp[cursor1++];
                if (--len1 == 1)
                    break outer;
                minGallop--;
            } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
            if (minGallop < 0)
                minGallop = 0;
            minGallop += 2;  // Penalize for leaving gallop mode
        }  // End of "outer" loop
        this.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to field

        if (len1 == 1) {
            assert len2 > 0;
            System.arraycopy(a, cursor2, a, dest, len2);
            a[dest + len2] = tmp[cursor1]; //  Last elt of run 1 to end of merge
        } else if (len1 == 0) {
            throw new IllegalArgumentException(
                "Comparison method violates its general contract!");
        } else {
            assert len2 == 0;
            assert len1 > 1;
            System.arraycopy(tmp, cursor1, a, dest, len1);
        }
    }

The method performs a merging of two sorted runs. It does a usual merge but starts "gallopping" once it encounters that one side starts "winning" (I.e., being always less than the other) all the time. Gallopping tries to make things faster by looking ahead more elements instead of comparing one element at a time. Since the runs should be sorted, looking ahead is fine.

You see that the exception is only throw when len1 is 0 at the end. The first observation is the following: During the usual merge, the exception can never be thrown since the loop aborts directly once len this 1. Thus, the exception can only be thrown as result of a gallop.

This already gives a strong hint that the exception behaviour is unreliable: As long as you have small data sets (so small that a generated run may never gallop, as MIN_GALLOP is 7) or the generated runs always coincidentally generate a merge that never gallops, you will never receive the exception. Thus, without further reviewing the gallopRight method, we can come to the conclusion that you cannot rely on the exception: It may never be thrown no matter how wrong your comparator is.

Answer from gexicide on Stack Overflow
🌐
Reddit
reddit.com › r/programming › the java standard library implementation of timsort has a bug.
r/programming on Reddit: The Java standard library implementation of Timsort has a bug.
August 31, 2018 - See http://www.envisage-project.eu/timsort-specification-and-verification/ As a result the in our opinion sub-optimal first suggestion was chosen. More replies More replies ... This is the test case that breaks the current sort. Apparently requires constructing 4+ GB integer array. ... It's good that this bug has been fixed, but it made me think of the difference between an algorithm bug and a system bug.
🌐
Hacker News
news.ycombinator.com › item
This is wrong. A well-known case is the Timsort bug, discovered by a program ver... | Hacker News
August 27, 2020 - A well-known case is the Timsort bug, discovered by a program verification tool. Also well known is the JDK binary search bug that had been present for many years. (This paper discusses the Timsort bug, and references the binary search bug: http://envisage-project.eu/proving-android-java-a...
Discussions

java - When does TimSort complain about broken comparator? - Stack Overflow
And another question where the bug was that the comparator was not transitive: stackoverflow.com/a/16078601/545127 ... In general, TimSort will only throw if it happens to discover that the Comparator doesn't work in the course of its normal sorting algorithm. More on stackoverflow.com
🌐 stackoverflow.com
On the Worst-Case Complexity of TimSort
There are likely a million ways malformed input can make a java server throw an exception; that's typically how java handles malformed input. It just gets caught and returned as an error to the client · arrayToSort[sum] = 1; This is just blatant programmer error. More on news.ycombinator.com
🌐 news.ycombinator.com
74
204
September 1, 2018
Bypassing a JDK1.8 bug: "Comparison method violates its general contract!" - TimSort
It is a bug in java.util.TimSort, in this case triggered by a com.jogamp.opengl class call. In the context of SNT usage, the bug can be just ‘annoying’ (e.g. linux with jdk1.8.0_172) or catastrophic: with an earlier jdk 1.8 version, the JVM segfaults and Fiji crashes. More on forum.image.sc
🌐 forum.image.sc
1
3
March 12, 2021
java - "Comparison method violates its general contract!" - TimSort and GridLayout - Stack Overflow
I made a color palette with a jPanel and a JLabel array in it. At first it worked well, but then i put some other jLabels out of the JPanel and added them some events. Now I keep getting this error: More on stackoverflow.com
🌐 stackoverflow.com
🌐
GitHub
github.com › abstools › java-timsort-bug
GitHub - abstools/java-timsort-bug: How to break TimSort and how to fix it
How to break TimSort and how to fix it. Contribute to abstools/java-timsort-bug development by creating an account on GitHub.
Starred by 83 users
Forked by 13 users
Languages   Java 100.0% | Java 100.0%
Top answer
1 of 2
7

After looking at the code of ComparableTimSort I am not quite sure. Let's analyze it. Here is the only method that throws it (there is a similar method that does the same only with exchanged roles, so analyzing one of them is enough).

private void mergeLo(int base1, int len1, int base2, int len2) {
        assert len1 > 0 && len2 > 0 && base1 + len1 == base2;

        // Copy first run into temp array
        Object[] a = this.a; // For performance
        Object[] tmp = ensureCapacity(len1);

        int cursor1 = tmpBase; // Indexes into tmp array
        int cursor2 = base2;   // Indexes int a
        int dest = base1;      // Indexes int a
        System.arraycopy(a, base1, tmp, cursor1, len1);

        // Move first element of second run and deal with degenerate cases
        a[dest++] = a[cursor2++];
        if (--len2 == 0) {
            System.arraycopy(tmp, cursor1, a, dest, len1);
            return;
        }
        if (len1 == 1) {
            System.arraycopy(a, cursor2, a, dest, len2);
            a[dest + len2] = tmp[cursor1]; // Last elt of run 1 to end of merge
            return;
        }

        int minGallop = this.minGallop;  // Use local variable for performance
    outer:
        while (true) {
            int count1 = 0; // Number of times in a row that first run won
            int count2 = 0; // Number of times in a row that second run won

            /*
             * Do the straightforward thing until (if ever) one run starts
             * winning consistently.
             */
// ------------------ USUAL MERGE
            do {
                assert len1 > 1 && len2 > 0;
                if (((Comparable) a[cursor2]).compareTo(tmp[cursor1]) < 0) {
                    a[dest++] = a[cursor2++];
                    count2++;
                    count1 = 0;
                    if (--len2 == 0)
                        break outer;
                } else {
                    a[dest++] = tmp[cursor1++];
                    count1++;
                    count2 = 0;
                    if (--len1 == 1)
                        break outer;
                }
            } while ((count1 | count2) < minGallop);

// ------------------ GALLOP
            /*
             * One run is winning so consistently that galloping may be a
             * huge win. So try that, and continue galloping until (if ever)
             * neither run appears to be winning consistently anymore.
             */
            do {
                assert len1 > 1 && len2 > 0;
                count1 = gallopRight((Comparable) a[cursor2], tmp, cursor1, len1, 0);
                if (count1 != 0) {
                    System.arraycopy(tmp, cursor1, a, dest, count1);
                    dest += count1;
                    cursor1 += count1;
                    len1 -= count1;
// -->>>>>>>> HERE IS WHERE GALLOPPING TOO FAR WILL TRIGGER THE EXCEPTION
                    if (len1 <= 1)  // len1 == 1 || len1 == 0
                        break outer;
                }
                a[dest++] = a[cursor2++];
                if (--len2 == 0)
                    break outer;

                count2 = gallopLeft((Comparable) tmp[cursor1], a, cursor2, len2, 0);
                if (count2 != 0) {
                    System.arraycopy(a, cursor2, a, dest, count2);
                    dest += count2;
                    cursor2 += count2;
                    len2 -= count2;
                    if (len2 == 0)
                        break outer;
                }
                a[dest++] = tmp[cursor1++];
                if (--len1 == 1)
                    break outer;
                minGallop--;
            } while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);
            if (minGallop < 0)
                minGallop = 0;
            minGallop += 2;  // Penalize for leaving gallop mode
        }  // End of "outer" loop
        this.minGallop = minGallop < 1 ? 1 : minGallop;  // Write back to field

        if (len1 == 1) {
            assert len2 > 0;
            System.arraycopy(a, cursor2, a, dest, len2);
            a[dest + len2] = tmp[cursor1]; //  Last elt of run 1 to end of merge
        } else if (len1 == 0) {
            throw new IllegalArgumentException(
                "Comparison method violates its general contract!");
        } else {
            assert len2 == 0;
            assert len1 > 1;
            System.arraycopy(tmp, cursor1, a, dest, len1);
        }
    }

The method performs a merging of two sorted runs. It does a usual merge but starts "gallopping" once it encounters that one side starts "winning" (I.e., being always less than the other) all the time. Gallopping tries to make things faster by looking ahead more elements instead of comparing one element at a time. Since the runs should be sorted, looking ahead is fine.

You see that the exception is only throw when len1 is 0 at the end. The first observation is the following: During the usual merge, the exception can never be thrown since the loop aborts directly once len this 1. Thus, the exception can only be thrown as result of a gallop.

This already gives a strong hint that the exception behaviour is unreliable: As long as you have small data sets (so small that a generated run may never gallop, as MIN_GALLOP is 7) or the generated runs always coincidentally generate a merge that never gallops, you will never receive the exception. Thus, without further reviewing the gallopRight method, we can come to the conclusion that you cannot rely on the exception: It may never be thrown no matter how wrong your comparator is.

2 of 2
-1

From the documentation:

IllegalArgumentException - (optional) if the natural ordering of the array elements is found to violate the Comparable contract

I didn't find much on the mentioned contract, but IMHO it should represent a total order (ie the relation defined by the compareTo method has to be transitive, antisymmetric, and total). If that requirement isn't met, sort might throw an IllegalArgumentException. (I say might because failure to meet this requirement could go unnoticed.)

EDIT: added links to the properties that make a relation a total order.

🌐
OpenJDK
bugs.openjdk.org › browse › JDK-8234482
timsort failure with exception Comparison method violates ...
ADDITIONAL SYSTEM INFORMATION : ... jdk1.6.0_21 It DOES occur in win7 jdk1.8.0_111 jdk-12.0.2 The problem is in timsort that fails to sort a Vector with 143 elements that are comparable....
🌐
Envisage-project
envisage-project.eu › proving-android-java-and-python-sorting-algorithm-is-broken-and-how-to-fix-it
Proving that Android’s, Java’s and Python’s sorting algorithm is broken (and showing how to fix it) | Envisage: Engineering Virtualized Services
February 24, 2015 - While attempting to verify TimSort, we failed to establish its instance invariant. Analysing the reason, we discovered a bug in TimSort’s implementation leading to an ArrayOutOfBoundsException for certain inputs.
🌐
OpenJDK
bugs.openjdk.org › browse › JDK-8203864
[JDK-8203864] Execution error in Java's Timsort
Carine Pivoteau wrote: While working on a proper complexity analysis of the algorithm, we realised that there was an error in the last paper reporting such a bug (http://envisage-project.eu/wp-content/uploads/2015/02/sorting.pdf). This implies that the correction implemented in the Java source code (changing Timsort stack size) is wrong and that it is still possible to make it break.
🌐
Python
bugs.python.org › issue23515
Issue 23515: Bad logic in timsort's merge_collapse - Python tracker
This issue tracker has been migrated to GitHub, and is currently read-only. For more information, see the GitHub FAQs in the Python's Developer Guide · This issue has been migrated to GitHub: https://github.com/python/cpython/issues/67703
Find elsewhere
🌐
Wikipedia
en.wikipedia.org › wiki › Timsort
Timsort - Wikipedia
February 17, 2026 - In 2015, Dutch and German researchers in the EU FP7 ENVISAGE project found a bug in the standard implementation of Timsort.
🌐
Python
bugs.python.org › file4451 › timsort.txt
timsort.txt
When a method gets significantly below that, it's either astronomically lucky, or is finding exploitable structure in the data. n lg(n!) *sort 3sort +sort ~sort !sort ------- ------- ------ -------- ------- ------- -------- 32768 444255 453084 453604 32908 130484 469132 samplesort 449235 33019 33016 188720 65534 timsort 0.86% 1273.77% -0.33% -30.86% 615.86% %ch from timsort 65536 954037 973111 970464 65686 260019 1004597 963924 65767 65802 377634 131070 0.95% 1375.61% -0.18% -31.15% 666.46% 131072 2039137 2100019 2102708 131232 555035 2161268 2058863 131422 131363 755476 262142 2.00% 1499.97%
🌐
Cwi
cwi.nl › en › news › java-bug-fixed-formal-methods-cwi
Java Bug Fixed with Formal Methods CWI
Researchers from CWI fixed a bug in the widely used object-oriented programming language Java in February 2015. They found an error in a broadly applied sorting algorithm, TimSort, which could crash programs.
🌐
Hacker News
news.ycombinator.com › item
On the Worst-Case Complexity of TimSort | Hacker News
September 1, 2018 - There are likely a million ways malformed input can make a java server throw an exception; that's typically how java handles malformed input. It just gets caught and returned as an error to the client · arrayToSort[sum] = 1; This is just blatant programmer error.
🌐
Image.sc
forum.image.sc › development
Bypassing a JDK1.8 bug: "Comparison method violates its general contract!" - TimSort - Development - Image.sc Forum
March 12, 2021 - Under certain conditions, SNT usage can trigger a bug in the JDK1.8 currently bundled within Fiji. It is a bug in java.util.TimSort, in this case triggered by a com.jogamp.opengl class call. In the context of SNT usage,…
🌐
Envisage-project
envisage-project.eu › timsort-specification-and-verification
OpenJDK’s java.utils.Collection.sort() is broken: The good, the bad and the worst case | Envisage: Engineering Virtualized Services
in particular the test generator, which breaks TimSort, as well as the source and proof of the fixed and verified version. The paper has been submitted to CAV’15 and is currently under review. Update: The paper has now been accepted. The final version can be found here. We reported the bug to Oracle/OpenJDK Developers after submission of the paper.
🌐
YouTube
youtube.com › atbrox
Timsort Bug in Java Walkthrough - YouTube
Shows a reproduction of the Timsort bug in Java.
Published   February 17, 2015
Views   13K
🌐
DEV Community
dev.to › nfrankel › a-sorting-bug-fdp
A sorting bug - DEV Community
July 26, 2020 - To sum it up, it returns o1 minus o2: it's up to the developer to define the implementation of the minus operation in the context of type T. With that, Timsort is able to compare elements in pairs and work its magic.
🌐
OpenJDK
bugs.openjdk.org › browse › JDK-8072909
TimSort fails with ArrayIndexOutOfBoundsException on worst ...
A DESCRIPTION OF THE PROBLEM : As previously reported in bug 8011944, the function mergeCollapse in the TimSort class does not preserve the invariant runLen[i] > runLen[i+1]+runLen[i+2]. This leads to an ArrayIndexOutOfBoundsException in TimSort's pushRun method.
🌐
Apache JIRA
issues.apache.org › jira › browse › LUCENE-6293
[#LUCENE-6293] TimSort bug - ASF JIRA
Robert pointed me to http://envisage-project.eu/proving-android-java-and-python-sorting-algorithm-is-broken-and-how-to-fix-it/ yesterday which explains how most implementations of TimSort are broken.
🌐
Envisage-project
envisage-project.eu › video-finding-the-bug-in-timsort
Finding the bug in TimSort (Video) | Envisage: Engineering Virtualized Services
Stijn de Gouw gave a presentation of his work on deductive software verification with KeY to discover the TimSort bug at Trondheim Developer Conference 2015. The details of this work have previously been presented as a blogpost on this website and as a scientific paper at CAV 2015.
Top answer
1 of 6
44

It seems to me like you've hit a bug in the JDK since the error seems to come from Swing classes.

Options:

  1. Define the property java.util.Arrays.useLegacyMergeSort as true. Either using in your code the line

    System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");
    

    before any Swing code. As the first line in the main method should work.

    Or adding

    -Djava.util.Arrays.useLegacyMergeSort=true
    

    to your starting options (in the console, or in the project properties in an IDE, Ant script, etc.)

  2. Upgrade your JDK and see if the problem goes away

  3. Downgrade to Java 6
2 of 6
15

Report my findings:

-Djava.util.Arrays.useLegacyMergeSort=true

works

but

System.setProperty("java.util.Arrays.useLegacyMergeSort", "true");

does not work.

It is due to the fact that in JDK Arrays.class

 static final class LegacyMergeSort {
    private static final boolean userRequested = ...

It is a static variable which is defined when jvm starts. Setting System property in the program will have no effect if the class has been loaded into jvm.

I have beeing monitoring the LegacyMergeSort.userRequested variable, and the findings confirmed with above statement.

Update: The program must set system properties before java.util.Arrays is loaded to classloader. Otherwise, once it is loaded, setting the properties is not going to be useful due to the reason mentioned above.

Make sure nothing else loaded Arrays.class:

By putting following code to your program to test:

    java.lang.reflect.Method m = ClassLoader.class.getDeclaredMethod("findLoadedClass", new Class[] { String.class });
    m.setAccessible(true);
    ClassLoader cl = ClassLoader.getSystemClassLoader();
    Object test1 = m.invoke(cl, "java.util.Arrays");
    System.out.println("test1 loaded? ->" + (test1 != null));