ConcurrentModificationException, возникающее при получении размера списка

Для проекта в моем классе Data Structures мне было поручено создать трехмерное дерево диапазонов, где каждое измерение является BST. Я прочитал этот вопрос, но это вопрос Android и причины нашего вопросы кажутся разными, и единственный ответ не был принят.

Стена кода скоро появится. Извините.

Задействованные классы:

  • Point3D<T>: Общий класс, содержащий координаты точки. T extends Comparable<T> и Point3D также расширяют его
  • RangeTree<T>: универсальный класс, содержащий корень всего дерева и методы для построения и поиска по дереву.

Классов больше, чем эти 2, но они, похоже, не связаны с тем, почему я получаю исключение ConcurrentModificationException (ссылка на CME). Вот что я получаю, когда запускаю свой драйвер:

Read in 10 points //Number of points read in driver, this is okay
5//first median, 10 / 2
2//second median 5 / 2
first[0.082  0.791  0.832  , 0.366  0.136  0.009  ]//correct
second[0.138  0.968  0.391  , 0.414  0.785  0.883  , 0.546  0.044  0.133  ]//correct
merged[0.082  0.791  0.832  , 0.366  0.136  0.009  ]//not correct
first[0.082  0.791  0.832  , 0.366  0.136  0.009  ]//printed again. why?
2//secondHalf being sorted
first[0.415  0.64  0.099  , 0.513  0.612  0.466  ]//correct
second[0.091  0.719  0.772  , 0.199  0.999  0.086  , 0.896  0.835  0.86  ]//correct
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1231)
at java.util.ArrayList$SubList.size(ArrayList.java:1040)
at RangeTree.mergeList(RangeTree.java:189)
at RangeTree.sortAscending(RangeTree.java:175)
at RangeTree.sortAscending(RangeTree.java:173)
at RangeTree.build3DTree(RangeTree.java:126)
at RangeTree.build(RangeTree.java:11)
at Main.main(Main.java:35)

Когда вызывается RangeTree.build(Main.java:35), он принимает List<Point3D<T>> и передает его в RangeTree.build3DTree(RangeTree.java:11), который также принимает # оставшихся для построения измерений и аргумент, указывающий, отсортировано ли уже List. RangeTree.java:126 в этом методе — это строка, в которой я вызываю sortAscending в build3DTree.

sortAscending (это рекурсия) делает то, на что это похоже, сравнивая точки в определенном измерении, начиная с x (x = 0, y = 1, z = 2), если они равны, он проверяет следующее измерение и т. д. На основе на приведенном выше, он явно работает нормально, за исключением того странного сбоя, когда Line 172 каким-то образом печатается дважды. Ниже приведен код sortAscending (все строки печати предназначены исключительно для отладки):

private List<Point3D<T>> sortAscending(List<Point3D<T>> pointArr){

    if(pointArr.size() <= 3){//minimum sublist size is 3
        int median = findMedian(pointArr);
        Point3D<T> medianElement = pointArr.get(median);//retrieves coordinate to sort on
        int compareLeft = medianElement.compareTo(pointArr.get(median - 1));
        if(compareLeft < 0){//determine if median is less than element to its left. There will always be an element to the left
            pointArr.add(0, pointArr.remove(median));
            medianElement = pointArr.get(median);
        }
        try{//check median against element to its right. May not be valid if sublist is size 2
            int compareRight = medianElement.compareTo(pointArr.get(median + 1));
            if(compareRight > 0){
                Point3D<T> rightEnd = pointArr.get(median + 1);
                Point3D<T> leftEnd = pointArr.get(median - 1);
                if(rightEnd.compareTo(leftEnd) < 0)
                    pointArr.add(0, pointArr.remove(median + 1));
                else
                    pointArr.add(median, pointArr.remove(median + 1));
            }
        } catch (IndexOutOfBoundsException e){

        }
        return pointArr;
    }
    int median = findMedian(pointArr);//returns pointArr.size() / 2
    System.out.println(median);//for debugging
    List<Point3D<T>> firstHalf = sortAscending(pointArr.subList(0, median));
    System.out.println("first" + firstHalf); //prints twice, once when it should, and again after Line 176 prints
    List<Point3D<T>> secondHalf = sortAscending(pointArr.subList(median, pointArr.size()));//Line 173
    System.out.println("second" + secondHalf);//debugging
    List<Point3D<T>> merged = mergeList(firstHalf, secondHalf);//Line 175
    System.out.println("merged" + merged);//debugging
    return merged;//mergeList(firstHalf, secondHalf);
}

Итак, окончательный источник CME, кажется, находится в mergeList и не прерывается до вызова со второй половиной данных. mergeList (итеративный) берет два List<Point3D<T>> и объединяет их в один список, отсортированный в порядке возрастания, и возвращает его. А именно, он принимает первый аргумент, преобразует его в объединенный список и возвращает его. Код:

private List<Point3D<T>> mergeList(List<Point3D<T>> firstList, List<Point3D<T>> secList){
    int sListSize = secList.size();
    int fListSize = firstList.size();//Line 188
    Point3D<T> firstListElement = firstList.get(fListSize - 1);
    Point3D<T> secListElement = secList.get(0);

    if(secListElement.compareTo(firstListElement) >= 0){//check to see if secList can be appended to end of firstList e.g. firstList = {1, 2} secList = {3, 4, 5}
        firstList.addAll(secList);
        return firstList;
    }

    secListElement = secList.get(secList.size() - 1);
    firstListElement = firstList.get(0);

    if(secListElement.compareTo(firstListElement) <= 0){//check to see if firstList can be appended to the end of secList e.g. secList = {1, 2} firstList = {3,4,5}
        secList.addAll(firstList);
        return secList;
    }

    for(int a = 0; a < secList.size(); a++){//take an element from secList and insert it into firstList
        secListElement = secList.get(a);
        int minIdx, maxIdx, median = findMedian(firstList);
        int mComp = secListElement.compareTo(firstList.get(median));//compare to the median of firstList to choose side to start from
        if(mComp < 0){
            minIdx = 0;
            maxIdx = median;
        }
        else if(mComp > 0){
            minIdx = median;
            maxIdx = firstList.size();
        }
        else{//if mComp is 0, insert it at median index
            firstList.add(median, secList.get(a));
            continue;
        }

        for(; minIdx < maxIdx; minIdx++){
            firstListElement = firstList.get(minIdx);
            int compare = secListElement.compareTo(firstListElement);
            if(compare <= 0){
                firstList.add(minIdx, secList.get(a));
                break;
            }
        }
    }
    return firstList;
}

Так почему-то CME приходит, когда я пытаюсь получить доступ к размеру firstList. Я понятия не имею, почему это происходит. Я вручную проследил каждый шаг своего кода с набором данных (опубликованным ниже), и я не вижу, откуда берется CME. Данные:

10
0.366   0.136   0.009
0.082   0.791   0.832//beautiful 3D points
0.138   0.968   0.391
0.414   0.785   0.883
0.546   0.044   0.133
0.513   0.612   0.466
0.415   0.640   0.099
0.199   0.999   0.086
0.896   0.835   0.860
0.091   0.719   0.772
0.25 0.75 0.25 0.75 0.25 0.75//range queries. Code fails before we get to this
0.75 0.25 0.8 0.1 0.9 0.1
0.95 1.75 0.1 0.01 0.1 0.2
exit//no more queries

person Ungeheuer    schedule 28.03.2017    source источник
comment
И просто для протокола: прочтите «Чистый код» Роберта Мартина. Ваш код не так уж ужасен; но читать тоже не очень приятно. Много тонких вещей, которые можно было бы улучшить... и, как правило, легко читаемый код также облегчает обнаружение ошибок...   -  person GhostCat    schedule 28.03.2017
comment
Где твоя основная функция?   -  person muzzlator    schedule 28.03.2017
comment
@GhostCat Я подумал, что это должно быть основано на документации. Но когда я все это нарисовал, вызовы рекурсивной сортировки завершаются, и списки, которые они возвращают, не затрагиваются до момента слияния. Далее, первая половина данных сливается без исключения (очевидно, сливается неправильно), вторая половина ломается, когда я обращаюсь к размеру списка, который больше не изменяется и не повторяется.   -  person Ungeheuer    schedule 28.03.2017
comment
Ваша трассировка стека говорит: в RangeTree.build(RangeTree.java:11) в Main.main(Main.java:35), поэтому мы должны посмотреть, что здесь происходит :)   -  person muzzlator    schedule 28.03.2017
comment
@muzzlator В моем драйвере, но это не имеет значения. Строка в драйвере, из которой начинается трассировка стека, — это метод build() в RangeTree, который отслеживает методы, которые я опубликовал.   -  person Ungeheuer    schedule 28.03.2017
comment
@muzzlator RangeTree.java:11 вызывает RangeTree.build3DTree, а строка 126 в этом методе вызывает sortAscending. Возможно, мне следует явно указать это в задаче. Я пытался не публиковать кучу кода.   -  person Ungeheuer    schedule 28.03.2017
comment
@Ungeheuer Хм, конечно, я пропустил вызовы ваших функций в этой трассировке стека, поэтому предположил, что проблема была выше. Во всяком случае, похоже, что у Ghostcat есть это   -  person muzzlator    schedule 28.03.2017


Ответы (2)


Проблема в том, что функция List#subList не возвращает копию списка, который вам разрешено изменять. Вы захотите сделать изменяемую копию этих списков. Не совсем уверен, почему параллельная проверка выполняется только в size(), но взгляните на ConcurrentModificationException, созданное подсписком для обсуждения

Изменить: Возможная причина, по которой проверка происходит только в size(): вам разрешены изменения, которые не нарушают структуру подсписка, например, операции обмена с Collections.swap. Они должны поставить галочку только в size(), чтобы эти "сохраняющие структуру" операции могли быть реализованы без немедленного сбоя, и поэтому ваш вызов add() мог остаться в покое.

Редактировать 2: Один из способов исправить это может заключаться в том, чтобы всегда передавать исходный список как есть и использовать индексы для определения диапазонов подсписков для вашей рекурсии.

person muzzlator    schedule 28.03.2017
comment
Это, вероятно, объясняет, почему первая попытка слияния не сработала, подсписок нельзя редактировать структурно. Я не понял, что это было так, прочитав API списка. Я попробую. - person Ungeheuer; 28.03.2017

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

Я думаю, что можно было бы отладить и решить вашу проблему, но это не решило бы основную проблему, а именно рекурсивные функции с побочными эффектами.

Я бы изменил и сортировку, и слияние, чтобы работать с неизменяемыми аргументами, не менять списки ввода, создавать новые.

person minus    schedule 28.03.2017