Весь ArrayList обновляется, когда я пытаюсь обновить один элемент Java 21

В свободное время я занимался алгоритмами и столкнулся со странной проблемой. Я не могу понять, почему это происходит. У меня есть два списка, и в обоих я обновляю элементы одинаково, но в «ленивом» списке при обновлении элемента обновляются все элементы.

Это важная часть кода:

public class SegmentTree {

    public static void main(String[] args) {
        List<Integer> a = List.of(1, 2, 3, 4, 5, 6, 7, 8);
        SegmentTreeObject segmentTreeObject = new SegmentTreeObject(a);
        segmentTreeObject.build(a);
        segmentTreeObject.list.forEach(s -> System.out.println(s.leftPosition + " " + s.rightPosition + " " + s.value));
        segmentTreeObject.updateLazy(new Point(0, 7), 5);
        segmentTreeObject.lazy.forEach(s -> System.out.println(s.leftPosition + " " + s.rightPosition + " " + s.value));
    }
}

class SegmentTreeObject {

    List<Node> list;
    List<Integer> original;

    //Lazy tree
    List<Node> lazy;

    int size;

    SegmentTreeObject(List<Integer> obj) {
        this.list = new ArrayList<>(Collections.nCopies(4 * obj.size(), new Node(100, 100, 0)));
        this.lazy = new ArrayList<>(Collections.nCopies(4 * obj.size(), new Node(0, 0, 0)));
        this.original = new ArrayList<>(obj);
        this.size = obj.size();
    }

    void updateLazyTree(int start, int end, int left, int right, int node, int newValue) {
        if (left > end || right < start) {
            return;
        }
        if (start >= left && end <= right) {
            this.list.get(node).updateNode(this.list.get(node).value + (end-start+1) * newValue);
            this.lazy.get(2 * node + 1).updateNode(this.lazy.get(2*node+1).value + newValue);
            this.lazy.get(2 * node + 2).updateNode(this.lazy.get(2*node+2).value + newValue);

            return;
        }

        int mid = start + (end-start) / 2;
        updateLazyTree(start, mid, left, right, 2 * node + 1, newValue);
        updateLazyTree(mid + 1, end, left, right, 2 * node + 2, newValue);
    }

    void updateLazy(Point range, int value) {
        updateLazyTree(0, this.size - 1, range.x, range.y, 0, value);
    }
}

class Node {
    int leftPosition;
    int rightPosition;
    int value;

    // All-args constructor omitted for brevity

    public void updateNode(int value){
        this.value = value;
    }
}

и вывод ленивых выглядит так:

0 0 10
0 0 10
0 0 10
0 0 10
0 0 10
0 0 10
0 0 10

Я также проверил с помощью отладчика, и да, «ленивый» список обновляет объект после одного вызова «updateNode».

Спасибо за ваше время!

Пожалуйста, дайте мне знать, если у вас есть идеи; Возможно, я ошибаюсь, создавая «ленивый» список.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
77
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Посчитайте new. Именно столько у вас объектов. Таким образом, в:

this.list = new ArrayList<>(Collections.nCopies(4*obj.size(),new Node(100,100,0)));

Я считаю.. 1 new.

Итак, у вас есть список с множеством записей, но только один объект Node. Как это может быть? Как у вас может быть массив, чьи .size() отчеты в этом примере составляют 32 (obj — это 8, и вы умножаете его на 4) — но только один Node объект?

Ну представьте себе такую ​​ситуацию:

Я строю один-единственный дом.

Затем я покупаю адресную книгу в магазине. В нем 32 страницы. На каждой странице я пишу один и тот же адрес. Адрес того дома, который я построил.

ТАДА! 32 дома!

Не совсем. Конечно, у меня 32 адреса. И они все одинаковы. 1 дом, 32 страницы.

Здесь происходит та же самая ситуация. У вас есть один объект узла и список массивов с 32 ссылками, все на этот один объект узла. Измените одну, и вы измените их все, по той же причине, по которой вы возьмете адресную книгу с ее 32 страницами, выберете случайную страницу, подойдете к дому, бросите кирпич в окно, а затем: независимо от того, какую страницу вы откроете. ваша адресная книга, если вы подойдете к адресу, который найдете там, угадайте, что? Окно разбито.

Вам нужно 32 объекта узла.

this.list = new ArrayList<>();
for (int i = 0; i < 4 * obj.size(); i++) this.list.add(new Node(100, 100, 0));

Если я подсчитаю, сколько раз new выполняется сейчас, в итоге я получу 32. Действительно, этот код подойдет (вам, конечно, также придется исправить this.lazy).

Если вы чувствуете, что это каким-то образом портит чистоту, то этот метод МОЖЕТ существовать в j.u.Collections:

public static <T> nTimes(int n, Supplier<T> supplier) {
  if (n < 0) throw new IllegalArgumentException("" + n);
  var list = new ArrayList<T>(n);
  for (int i = 0; i < n; i++) list.add(supplier.get());
  return list;
}

Тогда вы можете вызвать:

Collections.nTimes(4 * obj.size() () -> new Node(100, 100, 0));

Но такого метода просто не существует. Возможно, никогда: по большей части nCopies вообще существует потому, что обеспечивает весьма впечатляющую оптимизацию: он может представлять миллион записей практически без памяти, он просто запоминает одну ссылку, а также число «один миллион». nCopies похож на волшебную адресную книгу, в которой всего две страницы, поэтому она очень маленькая: одна страница с одним номером, а вторая страница с одним адресом. Для всех целей эта адресная книга действует так, как будто она большая (столько же страниц, сколько и первое число), с одним и тем же адресом на каждой из этих страниц. Это не очень-то полезная адресная книга, и действительно, nCopies используется редко. Но оно для этого и предназначено. Если вам нужна эта странная «адресная книга с МНОГО одинаковых записей», nCopies с ней очень хорошо справляется.

Хотя здесь совсем не то, что вам нужно. Итак, просто напишите цикл.

Спасибо большое за Ваш комментарий! Я учил, что «this.list = new ArrayList<>(Collections.nCopies(4*obj.size(),new Node(100,100,0)));» каждый раз будет создавать новый объект, как вы упомянули, 32 новых дома. Я учил этому, потому что использовал ту же логику для: «List<Node> list;» и это работает, как вы можете видеть в следующем примере: «0 7 76 0 3 10 4 7 26 0 1 3 2 3 7 4 5 11 6 7 15 0 0 1 1 1 2 2 2 3 3 3 4 4 4 5 5 5 6 6 6 7 7 7 8 100 100 0 100 100 0 100 100 0...» до 32. Теперь я немного не понимаю, почему это работает в «списке», а теперь в основном в «lasy». .

Al3x4ndru1 28.02.2024 14:56

Другой вариант создания экземпляров объектов может быть чем-то вроде Arrays.asList(IntStream.range(0, obj.size()*4) .mapToObj(() -> new Node(100,100,0)) .toArray(Node[]::new)); похожим на ответы, найденные здесь.

Calvin P. 28.02.2024 15:31

@ Al3x4ndru1 Al3x4ndru1 Java в этом смысле очень-очень проста. Метод nCopies не может захватить new Node(100, 100, 0) и запустить его более одного раза. Нет; это выражение вычисляется тут же, прежде чем nCopies даже вызывается, и передается результат вычисления этого выражения. nCopies не могу повторно запустить выражение. В лучшем случае он может попытаться клонировать то, что получает (но практически ничто в основных библиотеках этого не делает, и nCopies тоже). () -> new Node(100, 100, 0) — это «лямбда» — он отражает «идею сделать это» и может вызываться так часто, как вам нравится.

rzwitserloot 28.02.2024 18:04

@CalvinP кажется мне «злоупотреблением лямбдой». Цикл for не больше (если только вы не припишете «все блоки должны быть заключены в скобки», но тогда... почему тогда ваши вызовы mapToObj и co. не заключены в скобки? Если вы применяете непоследовательный стиль, проблема в вашем руководстве по стилю! ) и является должным образом прозрачным (проверенный ex, локальная изменяемая переменная и поток управления), тогда как лямбда-нет. Здесь это ничего не добавляет. Теперь, в примере nTimes, он добавляет кое-что полезное.

rzwitserloot 28.02.2024 18:06

Спасибо вам, ребята. В следующий раз я буду осторожнее.

Al3x4ndru1 28.02.2024 18:41

Другие вопросы по теме