Установить коллекцию для изменяемых объектов в Java

В Java набор проверяет только на равенство объекта с объектами, уже установленными только во время вставки. Это означает, что если после того, как объект уже присутствует в наборе, он становится равным другому объекту в наборе, набор будет содержать оба одинаковых объекта без жалоб.

EDIT: например, рассмотрим простой объект и предположим, что hashCode и equals определены в соответствии с лучшими методами /

class Foo {
    int foo;

    Foo(int a){ foo = a; }
    //+ equals and hashcode based on "foo"
}

Foo foo1 = new Foo(1);
Foo foo2 = new Foo(2);
Set<Foo> set = new HashSet<Foo>();
set.add(foo1);
set.add(foo2);
//Here the set has two unequal elements.
foo2.foo = 1;
//At this point, foo2 is equal to foo1, but is still in the set 
//together with foo1.

Как может быть задан класс для изменяемых объектов? Поведение, которое я ожидал, будет следующим: если в любой момент один из объектов в наборе становится равным другому объекту в наборе, этот объект удаляется из набора набором. Уже есть? Есть ли язык программирования, который облегчит выполнение этого?

Ответы

Ответ 1

Я не думаю, что это можно надежно сделать на Java в общем смысле. Нет общего механизма для обеспечения определенного действия на мутацию объекта.

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

1. Наблюдайте за элементами изменений

  • Вам нужно иметь контроль над реализацией типов, которые входят в набор
  • Небольшая производительность при каждом обновлении объекта в вашем наборе обновлений.

Вы можете попытаться выполнить observer, как конструкция, где ваш класс Set зарегистрирован как Observer во все его элементы. Это означает, что вам нужно будет контролировать типы объектов, которые можно поместить в объекты Set (только Observable). Кроме того, вам необходимо убедиться, что Observables уведомит наблюдателя о каждом изменении, которое может повлиять на hashcode и равно. Я не знаю такого класса, который уже существует. Как упоминает Рэй ниже, вам нужно также следить за потенциальными проблемами concurrency. Пример:

package collectiontests.observer;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;

public class ChangeDetectingSet<E extends Observable> implements Set<E>, Observer {

    private HashSet<E> innerSet;

    public void update(Observable o, Object arg) {
        innerSet.remove(o);
        innerSet.add((E)o); 
    }
    public int size() {
        return innerSet.size();
    }
    public boolean isEmpty() {
        return innerSet.isEmpty();
    }
    public boolean contains(Object o) {
        return innerSet.contains(o);
    }
    public Iterator<E> iterator() {
        return innerSet.iterator();
    }
    public Object[] toArray() {
        return innerSet.toArray();
    }
    public <T> T[] toArray(T[] a) {
        return innerSet.toArray(a);
    }
    public boolean add(E e) {
        e.addObserver(this);
        return innerSet.add(e);
    }
    public boolean remove(Object o) {
        if(o instanceof Observable){
            ((Observable) o).deleteObserver(this);
        }
        return innerSet.remove(o);
    }
    public boolean containsAll(Collection<?> c) {
        return innerSet.containsAll(c);
    }
    public boolean addAll(Collection<? extends E> c) {
        boolean result = false;
        for(E el: c){
            result = result || add(el);
        }
        return result;
    }
    public boolean retainAll(Collection<?> c) {
        Iterator<E> it = innerSet.iterator();
        E el;
        Collection<E> elementsToRemove = new ArrayList<E>();
        while(it.hasNext()){
            el = it.next();
            if(!c.contains(el)){
                elementsToRemove.add(el); //No changing the set while the iterator is going. Iterator.remove may not do what we want.
            }
        }
        for(E e: elementsToRemove){
            remove(e);
        }
        return !elementsToRemove.isEmpty(); //If it empty there is no change and we should return false
    }
    public boolean removeAll(Collection<?> c) {
        boolean result = false;
        for(Object e: c){
            result = result || remove(e);
        }
        return result;
    }
    public void clear() {
        Iterator<E> it = innerSet.iterator();
        E el;
        while(it.hasNext()){
            el = it.next();
            el.deleteObserver(this);
        }
        innerSet.clear();
    }
}

Это приводит к поражению производительности при каждом изменении изменяемых объектов.

2. Проверка изменений при использовании Set

  • Работает с любым существующим объектом, который вы хотите поместить в свой набор.
  • Необходимо сканировать весь набор каждый раз, когда вам требуется информация о наборе (стоимость исполнения может стать значимой, если ваш набор становится очень большим).

Если объекты в вашем наборе часто меняются, но сам набор используется редко, вы можете попробовать решение Joe ниже. Он предлагает проверить, действительно ли Set все равно, когда вы вызываете метод на нем. В качестве бонуса его метод будет работать над любым набором объектов (без ограничения его на наблюдаемые). По его мнению, его метод будет проблематичным для больших наборов или часто используемых наборов (так как весь набор необходимо проверять при каждом вызове метода).

Возможная реализация метода Джо:

package collectiontests.check;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

public class ListBasedSet<E> {

    private List<E> innerList;

    public ListBasedSet(){
        this(null);
    }

    public ListBasedSet(Collection<E> elements){
        if (elements != null){
            innerList = new ArrayList<E>(elements);
        } else {
            innerList = new ArrayList<E>();
        }
    }

    public void add(E e){
        innerList.add(e);
    }

    public int size(){
        return toSet().size();
    }

    public Iterator<E> iterator(){
        return toSet().iterator();
    }

    public void remove(E e){
        while(innerList.remove(e)); //Keep removing until they are all gone (so set behavior is kept)
    }

    public boolean contains(E e){
        //I think you could just do innerList.contains here as it shouldn't care about duplicates
        return innerList.contains(e);
    }

    private Set<E> toSet(){
        return new HashSet<E>(innerList);
    }
}

И еще одна реализация метода проверки всегда (эта основана на существующем наборе). Это путь, если вы хотите как можно больше использовать существующие наборы.

package collectiontests.check;

import java.util.Collection;
import java.util.Comparator;
import java.util.Iterator;
import java.util.NavigableSet;
import java.util.SortedSet;
import java.util.TreeSet;

public class ChangeDetectingSet<E> extends TreeSet<E> {

    private boolean compacting = false;

    @SuppressWarnings("unchecked")
    private void compact(){
        //To avoid infinite loops, make sure we are not already compacting (compact also gets called in the methods used here)
        if(!compacting){ //Warning: this is not thread-safe
            compacting = true;
            Object[] elements = toArray();
            clear();
            for(Object element: elements){
                add((E)element); //Yes unsafe cast, but we're rather sure
            }
            compacting = false;
        }
    }
    @Override
    public boolean add(E e) {
        compact();
        return super.add(e);
    }
    @Override
    public Iterator<E> iterator() {
        compact();
        return super.iterator();
    }
    @Override
    public Iterator<E> descendingIterator() {
        compact();
        return super.descendingIterator();
    }
    @Override
    public NavigableSet<E> descendingSet() {
        compact();
        return super.descendingSet();
    }
    @Override
    public int size() {
        compact();
        return super.size();
    }
    @Override
    public boolean isEmpty() {
        compact();
        return super.isEmpty();
    }
    @Override
    public boolean contains(Object o) {
        compact();
        return super.contains(o);
    }
    @Override
    public boolean remove(Object o) {
        compact();
        return super.remove(o);
    }
    @Override
    public void clear() {
        compact();
        super.clear();
    }
    @Override
    public boolean addAll(Collection<? extends E> c) {
        compact();
        return super.addAll(c);
    }
    @Override
    public NavigableSet<E> subSet(E fromElement, boolean fromInclusive, E toElement, boolean toInclusive) {
        compact();
        return super.subSet(fromElement, fromInclusive, toElement, toInclusive);
    }
    @Override
    public NavigableSet<E> headSet(E toElement, boolean inclusive) {
        compact();
        return super.headSet(toElement, inclusive);
    }
    @Override
    public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
        compact();
        return super.tailSet(fromElement, inclusive);
    }
    @Override
    public SortedSet<E> subSet(E fromElement, E toElement) {
        compact();
        return super.subSet(fromElement, toElement);
    }
    @Override
    public SortedSet<E> headSet(E toElement) {
        compact();
        return super.headSet(toElement);
    }
    @Override
    public SortedSet<E> tailSet(E fromElement) {
        compact();
        return super.tailSet(fromElement);
    }
    @Override
    public Comparator<? super E> comparator() {
        compact();
        return super.comparator();
    }
    @Override
    public E first() {
        compact();
        return super.first();
    }
    @Override
    public E last() {
        compact();
        return super.last();
    }
    @Override
    public E lower(E e) {
        compact();
        return super.lower(e);
    }
    @Override
    public E floor(E e) {
        compact();
        return super.floor(e);
    }
    @Override
    public E ceiling(E e) {
        compact();
        return super.ceiling(e);
    }
    @Override
    public E higher(E e) {
        compact();
        return super.higher(e);
    }
    @Override
    public E pollFirst() {
        compact();
        return super.pollFirst();
    }
    @Override
    public E pollLast() {
        compact();
        return super.pollLast();
    }
    @Override
    public boolean removeAll(Collection<?> c) {
        compact();
        return super.removeAll(c);
    }
    @Override
    public Object[] toArray() {
        compact();
        return super.toArray();
    }
    @Override
    public <T> T[] toArray(T[] a) {
        compact();
        return super.toArray(a);
    }
    @Override
    public boolean containsAll(Collection<?> c) {
        compact();
        return super.containsAll(c);
    }
    @Override
    public boolean retainAll(Collection<?> c) {
        compact();
        return super.retainAll(c);
    }
    @Override
    public String toString() {
        compact();
        return super.toString();
    }
}

3. Используйте Scala устанавливает

Вы можете обманывать и уничтожать изменяемые объекты (в том смысле, что вместо того, чтобы мутировать, вы должны создать новый с одним измененным свойством) в своем наборе. Вы можете посмотреть набор в Scala (я думал, что можно вызвать Scala из Java, но я не уверен на 100%): < а3 >

Ответ 2

Вы можете получить свое поведение после использования другой коллекции, например ArrayList. Методы contains и remove для a List не делают предположений об оставшихся неизменных объектах.

Поскольку изменения могут произойти в любое время, оптимизация не так много. Любые операции должны выполнять полное сканирование по всему содержимому, поскольку любой объект мог быть изменен с момента последней операции.

Вы можете или не захотите переопределить add, чтобы проверить, присутствует ли в данный момент объект. Затем при использовании или печати используйте new HashSet(list) для устранения дублируемых объектов.

Ответ 3

Ваша проблема - это идентичность объекта vs state. Точность не изменена с течением времени, состояние есть. В вашем наборе вы должны полагаться на личность, потому что это единственная гарантия на то, чтобы не вводить дублирование путем мутации, или вы должны перестроить Set каждый раз, когда есть мутация элемента. Технически, equals() и hashCode() должны быть постоянными во времени, чтобы отражать идентичность.

Как комментирует @assylias, существует определенная альтернатива, если вам нужно иметь коллекцию с объединенной идентификацией и состоянием.

  • имеют Map<TheObject, List<State>>, а не Set<TheObjectWithState>
  • удалите объект из Set перед мутацией, затем проверьте, существует ли он после мутации, добавьте его, если нет дубликата.

Ответ 4

Вы не найдете общую структуру данных, которая может использовать только любой объект для этой цели. Такой набор должен будет постоянно контролировать свои элементы, что, помимо прочего, приведет к большому количеству вопросов по concurrency.

Однако я могу представить что-то, основанное на практически неизвестном классе java.util.Observable. Вы можете, например, напишите a class ChangeAwareSet implements Set<? extends Observable>, Observer. Когда элемент добавляется в этот набор, он регистрируется как Наблюдатель и поэтому должен быть уведомлен обо всех изменениях этого объекта. (Но не ожидайте, что это будет очень эффективно, и вы можете столкнуться с проблемами concurrency в этом сценарии.)

Ответ 5

У вас есть две широкие стратегии здесь, я ожидаю, что они не дадут отличную производительность (но это может быть не проблема для вашего использования).

  • У вашего набора зарегистрировать объекты для изменений.
  • Вместо того, чтобы постоянно изменять набор, только обновлять его, когда он используется.

Обратите внимание, что эти решения будут иметь небольшую разницу в поведении.

Зарегистрировать изменения

Это связано с добавлением шаблона Observable (или, альтернативно, слушателя) ко всем объектам, хранящимся в наборе.

Когда объект находится в Set, Set будет регистрироваться для изменений. Когда объект изменится, он будет сигнализировать об изменении Set, и соответственно изменится Set.

Самая наивная реализация - просто удалить все равные объекты, а затем повторно добавить объект при любом изменении. Наивная реализация всегда является хорошим началом, поэтому вы можете написать правильный набор тестов, и оттуда вы сможете улучшить производительность шаг за шагом.

Безопасность резьбы

Будьте внимательны при использовании этого набора или объектов в нем из нескольких потоков. Какое решение похоже на это, существует множество рисков для взаимоблокировок, поэтому вы, вероятно, в конечном итоге получите одиночный ReadWriteLock как для Set и сохраненные в нем объекты.

Обновить его, когда он используется

Альтернативой является ленивая стратегия: только обновлять набор, когда он используется. Это очень полезно, когда есть много изменений в объектах, но набор не используется так часто.

Он использует следующую заданную идею (это заставляет меня думать о кошке Шредингера):

Если никто не смотрит на Set, не имеет значения, что в нем?

Объект определяется только поведением на нем интерфейса (ов). Поэтому вместо этого вы можете оценить свой набор (и обновить его соответственно) в тот момент, когда используется информация.

Общие замечания

Здесь приведены некоторые замечания, которые применяются к обоим вариантам.

Желаемое поведение

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

См., например, следующее, подавая иск в свой класс Foo:

Foo foo1 = new Foo(1);
Foo foo2 = new Foo(2);
Set<Foo> set = new MySet<Foo>();
set.add(foo1);
set.add(foo2);

foo2.foo = 1; // foo or foo2 is removed from the set.
foo2.foo = 3; // now the set contains with a foo or with 1 or with 3.

В качестве альтернативы вы можете взять объекты, хранящиеся в списке, и преобразовать их в настройках в то время, когда вы используете.

Ответ 6

Использовать безопасную публикацию: не разрешать доступ к набору или его элементам; вместо этого опубликуйте глубокую копию.

Вам нужен способ сделать копию Foo; Я буду использовать конструктор копирования.

private Set<Foo> set;

public Set<Foo> getFoos() {
    // using java 8
    return set.stream().map(Foo::new).collect(Collectors.toSet());
}

Вы также должны сохранить копию Foo, а не сохранять foo, потому что вызывающая сторона будет иметь ссылку на добавленные Foos, поэтому клиент может их мутировать. Добавьте для этого метод доступа:

public boolean addFoo(Foo foo) {
    return set.add(new Foo(foo));
}

Ответ 7

В наборе используется метод hashCode и equals. Но когда вы говорите

он становится равным другому объекту в наборе, набор будет содержать оба равных объекта без жалоб.

Это не так. Если вы запустите метод add, добавив уже существующий элемент, он вернет вам ложь, говоря, что у вас уже есть объект в наборе.

Set - это математический термин, который не допускает дублирования и имеет место с Java Set. Набор агностик о том, является ли объект, который вы вставляете в него, является изменяемым или неизменным. Это как коллекция, в которой хранятся значения.

Изменить: В соответствии с кодом проверки в наборе выполняются, когда вы вставляете элемент в Set, а затем, если он изменяется, он не заботится об этом.

Ответ 8

Это отличный вопрос! Возможно, это источник многих ошибок! Это не просто проблема с дубликатами. Почти все методы возвращают неверные ответы даже без дубликатов. Рассмотрим хеш-набор. Если хеш изменяется даже без создания дубликата, метод contains теперь возвращает неправильные результаты, так как объект находится в неверном хэш-ведре. Аналогичным образом удаление будет работать неправильно. Для отсортированных наборов порядок итератора будет неправильным.

Мне нравится шаблон Observable, упомянутый @Thirler. Другие решения кажутся неэффективными. В упомянутом там наблюдаемом подходе существует зависимость того, что этот исполнитель элементов, добавляемых в набор, корректно уведомляет об этом каждый раз, когда происходит обновление. Подход, который я упоминаю здесь, несколько более ограничительный, но передает ответственность за правильную реализацию создателю набора. До тех пор, пока набор будет реализован правильно, он будет работать для всех пользователей набора. (Более подробно о том, почему шаблон наблюдателя трудно реализовать) см. Ниже.

Вот основная идея: предположим, что вы хотите создать набор объектов foo. Мы создадим класс под названием SetFoo. Все аспекты объектов foo поддерживаются самим набором, включая конструкцию и любые изменения в ней. Нет никакого способа, чтобы любой другой пользователь мог создать объект Foo напрямую, потому что он является внутренним классом SetFoo, а конструктор является либо закрытым, либо защищенным. Например, предположим, что мы реализуем класс SetFoo, где Foo имеет методы void setX(int x) и Foo int getX(). Класс SetFoo имел бы такие методы, как:

Foo instance(int x)  //Returns the instance of foo if it exists, otherwise creates a new one and returns it.

Скажем, что внутри SetFoo поддерживает хешсет объектов Foo.

Теперь метод setX для Foo будет определен для удаления и повторного добавления элемента в hashset, если изменяется значение x.

Мы можем расширить идею SetFoo, чтобы содержать любое количество элементов, все из которых поддерживаются набором. Это действительно легко реализовать для любых объектов, однако для этого требуется, чтобы все элементы поддерживались набором (включая конструкцию и все методы настройки). Конечно, чтобы сделать его многопоточным безопасным, потребуется больше работы.

С точки зрения любого пользователя класса SetFoo все будет просто:

 Foo f = setFoo.instance(1);
 ....
 f.setX(2);
 ...
 f.setX(3)

 f = setFoo.instance(1);  // Would internally create a new one since it was changed.
 f= setFoo.instance(3)   // Already in the set so no new one is created.

Теперь мы также можем добавить другие методы в SetFoo, например

boolean contains (int x);
Iterator<Integer> iterator();
boolean remove(int x);
etc...

или мы можем добавить в Foo различные методы:

remove()  // removes foo from the set.
exists()  // if foo still in the set?
add() // add foo back to the set

В случае, когда элементы могут содержать много полей, мы можем иметь класс FooSpec. Предположим, что Foo содержит int x и int y. Тогда FooSpec будет иметь методы getX, SetX, getY, setY и может быть построен с использованием new FooSpec. Теперь setFoo будет иметь такие методы, как:

 Foo instance(FooSpec fooSpec)
 Collection<Foo> instanceAll(Collection<FooSpec> col)
 ...etc

Итак, теперь вам может быть интересно, почему подход шаблона наблюдателя подвержен потенциальным ошибкам. При таком подходе пользователь набора должен правильно уведомить набор, когда он изменится. Это фактически тот же уровень сложности, что и реализация глубоко неизменяемого объекта (что может быть не так просто). Например, если элементы набора сами представляют собой коллекции или коллекции коллекций, тогда вам нужно будет убедиться, что вы уведомляете набор, когда что-либо в коллекции (глубоко) изменяется.

Невзирая на то, что "глубоко" уведомляет о наборе, пользователю набора, возлагает большую нагрузку на разработчика. Лучше реализовать инфраструктуру, которая будет обеспечивать объекты, которые "глубоко" уведомляют.

Ответ 9

Я все еще не уверен, что вы понимаете последствия. Если у вас есть 2 объекта, которые МОЖЕТ быть равны друг другу в любой момент времени, могут не совпадать друг с другом в другой момент времени, поэтому по умолчанию они считаются отдельными объектами, хотя в настоящий момент они могут казаться идентичными.

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