为什么Set不提供获取与另一个元素相等的元素的操作?

Set<Foo> set = ...;
...
Foo foo = new Foo(1, 2, 3);
Foo bar = set.get(foo);   // get the Foo element from the Set that equals foo

我可以问Set是否包含一个等于bar的元素,那么为什么我不能得到那个元素呢?:(

为了澄清,equals方法被重写,但它只检查其中一个字段,而不是所有字段。两个相等的Foo对象可以有不同的值,这就是为什么我不能只用Foo。


当前回答

不幸的是,Java中的Default Set并不是为提供“get”操作而设计的,正如jschreiner所准确解释的那样。

使用迭代器找到感兴趣的元素(dacwe建议)或删除元素并更新其值重新添加元素(KyleM建议)的解决方案可能有效,但效率非常低。

重写等号的实现以使非等号对象“相等”,正如David Ogren所正确指出的那样,很容易导致维护问题。

恕我直言,使用Map作为显式替换(正如许多人建议的那样)会使代码不那么优雅。

如果目标是访问集合中包含的元素的原始实例(希望我正确理解了您的用例),这里有另一种可能的解决方案。


我个人在用Java开发客户端-服务器视频游戏时也有同样的需求。在我的例子中,每个客户机都有存储在服务器中的组件的副本,问题在于客户机何时需要修改服务器的对象。

通过互联网传递一个对象意味着客户端无论如何都有该对象的不同实例。为了将这个“复制”的实例与原始实例相匹配,我决定使用Java uuid。

因此,我创建了一个抽象类UniqueItem,它自动为其子类的每个实例提供一个随机的惟一id。

这个UUID在客户机和服务器实例之间共享,因此通过这种方式,只需使用Map就可以很容易地匹配它们。

然而,在类似的用例中直接使用Map仍然是不优雅的。有人可能会说,使用Map维护和处理可能更加复杂。

出于这些原因,我实现了一个名为MagicSet的库,它使得Map的使用对开发人员来说是“透明的”。

https://github.com/ricpacca/magicset


与原来的Java HashSet一样,MagicHashSet(库中提供的MagicSet的实现之一)使用一个支持HashMap,但是它使用元素的UUID作为键,使用元素本身作为值,而不是将元素作为键和虚拟值作为值。与普通HashSet相比,这不会导致内存使用的开销。

此外,MagicSet可以完全作为Set使用,但有一些提供额外功能的方法,如getFromId()、popFromId()、removeFromId()等。

使用它的唯一要求是您想要存储在MagicSet中的任何元素都需要扩展抽象类UniqueItem。


下面是一个代码示例,设想从MagicSet中检索一个城市的原始实例,给定该城市的另一个实例,该实例具有相同的UUID(甚至只有它的UUID)。

class City extends UniqueItem {

    // Somewhere in this class

    public void doSomething() {
        // Whatever
    }
}

public class GameMap {
    private MagicSet<City> cities;

    public GameMap(Collection<City> cities) {
        cities = new MagicHashSet<>(cities);
    }

    /*
     * cityId is the UUID of the city you want to retrieve.
     * If you have a copied instance of that city, you can simply 
     * call copiedCity.getId() and pass the return value to this method.
     */
    public void doSomethingInCity(UUID cityId) {
        City city = cities.getFromId(cityId);
        city.doSomething();
    }

    // Other methods can be called on a MagicSet too
}

其他回答

不幸的是,Java中的Default Set并不是为提供“get”操作而设计的,正如jschreiner所准确解释的那样。

使用迭代器找到感兴趣的元素(dacwe建议)或删除元素并更新其值重新添加元素(KyleM建议)的解决方案可能有效,但效率非常低。

重写等号的实现以使非等号对象“相等”,正如David Ogren所正确指出的那样,很容易导致维护问题。

恕我直言,使用Map作为显式替换(正如许多人建议的那样)会使代码不那么优雅。

如果目标是访问集合中包含的元素的原始实例(希望我正确理解了您的用例),这里有另一种可能的解决方案。


我个人在用Java开发客户端-服务器视频游戏时也有同样的需求。在我的例子中,每个客户机都有存储在服务器中的组件的副本,问题在于客户机何时需要修改服务器的对象。

通过互联网传递一个对象意味着客户端无论如何都有该对象的不同实例。为了将这个“复制”的实例与原始实例相匹配,我决定使用Java uuid。

因此,我创建了一个抽象类UniqueItem,它自动为其子类的每个实例提供一个随机的惟一id。

这个UUID在客户机和服务器实例之间共享,因此通过这种方式,只需使用Map就可以很容易地匹配它们。

然而,在类似的用例中直接使用Map仍然是不优雅的。有人可能会说,使用Map维护和处理可能更加复杂。

出于这些原因,我实现了一个名为MagicSet的库,它使得Map的使用对开发人员来说是“透明的”。

https://github.com/ricpacca/magicset


与原来的Java HashSet一样,MagicHashSet(库中提供的MagicSet的实现之一)使用一个支持HashMap,但是它使用元素的UUID作为键,使用元素本身作为值,而不是将元素作为键和虚拟值作为值。与普通HashSet相比,这不会导致内存使用的开销。

此外,MagicSet可以完全作为Set使用,但有一些提供额外功能的方法,如getFromId()、popFromId()、removeFromId()等。

使用它的唯一要求是您想要存储在MagicSet中的任何元素都需要扩展抽象类UniqueItem。


下面是一个代码示例,设想从MagicSet中检索一个城市的原始实例,给定该城市的另一个实例,该实例具有相同的UUID(甚至只有它的UUID)。

class City extends UniqueItem {

    // Somewhere in this class

    public void doSomething() {
        // Whatever
    }
}

public class GameMap {
    private MagicSet<City> cities;

    public GameMap(Collection<City> cities) {
        cities = new MagicHashSet<>(cities);
    }

    /*
     * cityId is the UUID of the city you want to retrieve.
     * If you have a copied instance of that city, you can simply 
     * call copiedCity.getId() and pass the return value to this method.
     */
    public void doSomethingInCity(UUID cityId) {
        City city = cities.getFromId(cityId);
        city.doSomething();
    }

    // Other methods can be called on a MagicSet too
}

如果你有一个相等的对象,为什么你需要集合中的一个?如果它仅与键“相等”,则Map将是更好的选择。

不管怎样,下面的方法就可以了:

Foo getEqual(Foo sample, Set<Foo> all) {
  for (Foo one : all) {
    if (one.equals(sample)) {
      return one;
    }
  } 
  return null;
}

在Java 8中,这可以变成一行代码:

return all.stream().filter(sample::equals).findAny().orElse(null);

如果你有一个NavigableSet(例如TreeSet),你可以这样做:

public static <E> E get(NavigableSet<E> set, E key) {
    return set.tailSet(key, true).floor(key);
}

对于HashSet及其后代(如LinkedHashSet)来说,事情有点棘手:

import java.util.*;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Test {
    private static final Field mapField;
    private static final Method hashMethod;
    private static final Method getNodeMethod;
    private static final Field keyField;
    static {
        try {
            mapField = HashSet.class.getDeclaredField("map");
            mapField.setAccessible(true);
            hashMethod = HashMap.class.getDeclaredMethod("hash", Object.class);
            hashMethod.setAccessible(true);
            getNodeMethod = HashMap.class.getDeclaredMethod("getNode",
                    Integer.TYPE, Object.class);
            getNodeMethod.setAccessible(true);
            keyField = Class.forName("java.util.HashMap$Node").getDeclaredField("key");
            keyField.setAccessible(true);
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static <E> E get(HashSet<E> set, E key) {
        try {
            Object map = mapField.get(set);
            Object hash = hashMethod.invoke(null, key);
            Object node = getNodeMethod.invoke(map, hash, key);
            if (node == null)
                return null;
            @SuppressWarnings("unchecked")
            E result = (E)keyField.get(node);
            return result;
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    public static <E> E get(NavigableSet<E> set, E key) {
        return set.tailSet(key, true).floor(key);
    }

    public static void main(String[] args) {
        HashSet<Integer> s = new HashSet<>();
//      HashSet<Integer> s = new LinkedHashSet<>();
//      TreeSet<Integer> s = new TreeSet<>();
        for (int i = 0; i < 100_000; i++)
            s.add(i);
        Integer key = java.awt.event.KeyEvent.VK_FIND;
        Integer hidden = get(s, key);
        System.out.println(key);
        System.out.println(hidden);
        System.out.println(key.equals(hidden));
        System.out.println(key == hidden);
    }
}

看起来合适的使用对象是番石榴中的Interner:

为其他不可变提供与String.intern()相同的行为 类型。常见的实现可以从Interners获得 类。

它也有一些非常有趣的杠杆,比如concurrencyLevel,或者使用的引用类型(可能值得注意的是,它没有提供softinternet,我认为这比weakinternet更有用)。

Why:

Set似乎在提供比较手段方面发挥了有用的作用。它被设计为不存储重复的元素。

由于这种意图/设计,如果要获得()对存储对象的引用,然后更改它,则Set的设计意图可能会受到阻碍,并可能导致意想不到的行为。

来自JavaDocs

如果使用可变对象作为set元素,必须非常小心。当对象是集合中的元素时,如果对象的值以影响相等比较的方式更改,则不指定集合的行为。

How:

现在已经引入了流,我们可以做以下事情

mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();