在Java中为可变对象设置集合

时间:2015-02-20 13:19:02

标签: java collections set mutable generic-collections

在Java中,集合仅在插入时仅检查对象与已在集合中的对象的相等性。这意味着如果在对象已经存在于集合中之后,它变得等于集合中的另一个对象,则该集合将保持两个相等的对象而不会抱怨。

编辑:例如,考虑一个简单的对象,并假设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.

如何为可变对象设计一个集合类?我期望的行为如下:如果集合中的一个对象在任何时候变得等于集合中的另一个对象,则该集合中的该对象将被删除。有没有?是否有一种编程语言可以使这更容易实现?

10 个答案:

答案 0 :(得分:8)

我不认为这在一般意义上可以在Java中可靠地完成。没有一般机制来确保对象的变异采取某种行动。

有一些解决方案可能足以满足您的使用需求。

<强> 1。观察更改元素

  • 您需要控制进入集合
  • 的类型的实现
  • 只要您的集中的对象更新
  • ,性能就会降低

您可以尝试强制执行observer类似的构造,其中您的Set类已注册为其所有项目的Observer。这意味着您需要控制可以放入Set(仅Observable个对象)的对象类型。此外,您需要确保Observable通知观察者 每次 更改,这些更改可能会影响hashcode和equals。我不知道这样的任何类已经存在。就像下面的Ray提到的那样,你也需要注意潜在的并发问题。 例如:

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's 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。使用设置时检查更改

  • 适用于您要放入集合中的任何现有对象
  • 每当您需要有关该集合的信息时,需要扫描整个集合(如果您的集合变得非常大,性能成本可能会变得很大。)

如果您的集合中的对象经常更改,但很少使用集合本身,您可以尝试下面的Joe解决方案。他建议每当你调用一个方法时检查Set是否仍然正确。作为奖励,他的方法将在任何对象集合上工作(不必将其限制为可观察对象)。在性能方面,他的方法对于大型集合或经常使用的集合会有问题(因为需要在每次方法调用时检查整个集合)。

可能实施Joe的方法:

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);
    }
}

另一种check always方法的实现(这个方法基于现有的set)。如果您想尽可能多地重用现有集合,这就是您的选择。

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中查看该集合(我认为可以从Java调用Scala,但我不是100%肯定):http://www.scala-lang.org/api/current/scala/collection/immutable/IndexedSeq.html

答案 1 :(得分:3)

您可以使用其他集合(例如ArrayList)来获取您所追求的行为。 contains的{​​{1}}和remove方法不会假设对象保持不变。

由于任何时候都可能发生变化,因此优化的空间不大。任何操作都需要对所有内容执行完全扫描,因为自上次操作以来任何对象都可能已更改。

您可能希望也可能不希望覆盖List来检查当前对象是否存在。然后,在使用或打印时,使用add来消除当前重复的对象。

答案 2 :(得分:3)

您的问题是对象与州的身份 身份随着时间的推移不可变,状态是。在您的集合中,您最好应该依赖身份,因为这是不通过变异引入重复的唯一保证,或者每次有元素变异时您必须重建Set。从技术上讲,equals()hashCode()应该保持不变,以反映身份

正如@assylias评论的那样,如果你需要一个具有身份状态组合的集合,肯定有另一种选择。

  • 拥有Map<TheObject, List<State>>而不是Set<TheObjectWithState>
  • 从变异前的Set中删除对象,然后检查变异后是否存在,如果没有重复则添加它。

答案 3 :(得分:2)

您将找不到可用于此目的的任何对象的通用数据结构。这种集合必须不断监视其元素,这会导致很多关于并发性的问题。

但是,我可以想象基于几乎未知的类java.util.Observable的东西。你可以,例如写一个class ChangeAwareSet implements Set<? extends Observable>, Observer。将一个元素添加到此Set时,它将注册为Observer,因此会通知该对象的所有更改。 (但是不要期望这样做非常有效,并且在这种情况下也可能遇到并发问题。)

答案 4 :(得分:2)

你在这里有两个广泛的策略,我希望它们都不会有很好的表现(但这对你的使用可能不是一个问题)。

  1. 注册设置为以进行更改
  2. 不是经常修改设置,而是在使用时更新
  3. 请注意,这些解决方案的行为会略有不同。

    注册更改

    这涉及向存储在集合中的所有对象添加Observable模式(或者一个侦听器)。

    当某个对象位于Set时,Set会注册更改。当一个对象发生变化时,它会发出Set已发生变化的信号,Set也会相应变化。

    最天真的实现是删除所有equals对象,然后在任何更改时重新添加对象。天真的实现总是一个好的开始,所以你可以编写一个合适的测试集,从那里你可以逐步提高性能。

    线程安全

    使用此Set或多个线程中的对象时要小心。这样的解决方案存在很多死锁风险,因此对于Set和存储在其中的对象,最终可能只有一个ReadWriteLock

    使用时更新

    替代方案是一种惰性策略:仅在使用时更新该组。当对象有很多更改但是不经常使用该集时,这非常有用。

    它使用了以下设定的想法(这让我想起了薛定谔的猫):

      

    如果没有人在看集合,它的内容是否重要?

    对象只能通过它在其界面上的行为来定义。因此,您可以在使用信息时评估您的集合(并相应地更新它)。

    一般性评论

    以下是一些适用于这两种选择的评论。

    期望的行为

    注意你可能会遇到像这样的一组非常奇怪的行为。当您从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.
    

    作为替代方案,您可以将存储在列表中的对象转换为在您使用时将其转换为设置。

答案 5 :(得分:1)

使用安全发布:不允许访问Set或其元素;发布深层副本。

你需要一种制作Foo副本的方法;我将假设一个复制构造函数。

private Set<Foo> set;

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

您还应该保存Foo的副本,而不是保存foo,因为调用者将引用添加的Foo,因此客户端可以改变它们。为此添加一个访问器方法:

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

答案 6 :(得分:0)

Set确实使用了hashCodeequals方法。但是当你说

  
    

它变得等于集合中的另一个对象,该集合将保持两个相等的对象而不会抱怨。

  

事实并非如此。如果你通过添加已经存在的元素来运行add方法,它将返回false,表示你已经在set中有了一个对象。

Set是一个数学术语,不允许重复,Java Set也是如此。 Set不知道您插入的对象是可变的还是不可变的。它就像一个拥有价值观的集合。

编辑: 根据代码,当您将元素插入Set时,将完成集合中的检查,然后如果它发生更改,它就不会关心它。

答案 7 :(得分:0)

这是一个很好的问题!也许它是许多错误的根源!这不仅仅是重复问题。即使没有重复,几乎所有方法都会返回错误的答案。考虑一个哈希集。如果哈希更改甚至没有创建重复,则contains方法现在将返回不正确的结果,因为该对象位于错误的哈希桶中。同样删除将无法正常工作。对于有序集,迭代器顺序将不正确。

我喜欢@Thirler提到的Observable模式。其他解决方案效率低下。在那里提到的可观察方法中,存在一种依赖性,即每当发生更新时,要添加到集合的元素的实现者正确地通知集合。我在这里提到的方法有点限制性,但将正确实现的责任传递给集合创建者。因此,只要该集合正确实现,它将适用于该集合的所有用户。 (有关观察者模式难以实现的原因,请参见下文)

这是基本思想:假设您要创建一组foo对象。我们将创建一个名为SetFoo的类。 foo对象的所有方面都由set本身维护,包括构造以及对它的任何更改。任何其他用户都无法直接创建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对象的哈希集。

现在,如果x的值发生变化,Foo的setX方法将被定义为删除元素并将其重新添加到哈希集。

我们可以扩展SetFoo的思想,包含任意数量的元素,所有元素都由集合维护。这对于任何类型的对象都非常容易实现,但是,它确实要求元素都由集合维护(包括构造和所有setter方法)。当然,为了使其多线程安全,需要做更多的工作。

从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

所以现在你可能想知道为什么观察者模式方法会遇到潜在的错误。使用该方法,集合的用户必须在更改时正确通知集合。这实际上与实现深度不可变对象(可能不那么容易)的难度相同。例如,如果集合的元素本身就是集合或集合集合,那么您需要确保在集合中的任何内容(深度)发生更改时通知集合。

让责任“深度”通知集合,对集合的用户来说,会给开发人员带来很大的负担。最好实现一个框架,该框架将提供“深度”通知的对象。

答案 8 :(得分:0)

我仍然不确定你理解其含义。如果你有2个物体在任何时间点彼此相等,可能在另一个时间点彼此不相等,因此默认情况下它们被视为单独的物体,即使它们看起来似乎是相同的。

我会以不同的角度进行讨论,并检查该集合是否包含执行更改时对象将变为什么,如果您不希望它在该集合中与另一个对象相同时存在。

答案 9 :(得分:0)

以下是我看到的一种方法的几个方面

使用“动态元素集”

明确区分为不可变元素设置可变集合类和为可变元素设置另一个集合类可能会很好

可变元素的集合类将是“动态元素集”,并且要求每个元素都有一个指向包含集合的指针

元素本身在修改时注册更改

您可能必须为集合中包含的元素提供相应的包装类,以便它可以向包含元素注册

用于快速单线程唯一性检查的哈希表

当向集合中添加元素时,集合将计算元素的哈希值,并将其添加到表中(我确信这就是集合的工作方式)

使用它来检查唯一性并在 O(1) 时间内进行消除

多线程情况下的脏/干净状态

更新元素时​​,将包含集标记为“脏”

当包含集是脏的时,您可以在某个时候重新运行唯一性测试以查看是否所有元素都是唯一的。

在发生这种情况时,它可能应该阻止对元素的任何修改,直到它完成

这样,您可能偏离了确切的唯一性属性

考虑一下:列表中有 3 个元素:A、B 和 C,每个元素都有唯一的值

您将元素 B 更改为与 A 相同的值 标记为脏

将元素 A 更改为不同的唯一值 仍然标记为脏

运行唯一性检查

因此,如果您不需要 absolute 设置属性,而只需要一个近似值,这可能会起作用

否则,如果您需要绝对设置属性,在多线程情况下可能不起作用

更新似乎很便宜,所以你可能会侥幸逃脱

这真的是一个“集合”吗?

所以,这有点假设元素只是从集合提供的接口中修改

当您将元素的基类包装到集合中时,它可能应该制作元素的深层副本,以帮助防止元素从未注册的引用对象中获得修改

所以它不仅仅是一个“集合”,而是对传递的元素类型强加了要求

为元素类增加了一个界面层

因此,从某种意义上说,元素本身是新对象的一部分

其他想法

当然,如果一个元素可以和另一个元素相同,那么将来它也可能再次变得不同

您的意思是在需要该类型属性的特定问题中需要一个正在搜索的解决方案:需要消除暂时重复的元素