Получение элемента из набора - PullRequest
278 голосов
/ 02 сентября 2011

Почему 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.

Ответы [ 21 ]

338 голосов
/ 22 августа 2013

Чтобы ответить на точный вопрос « Почему не Set не предоставляет операцию для получения элемента, равного другому элементу?», Ответ будет: потому что разработчики каркаса коллекции не были очень жду. Они не ожидали вашего вполне законного варианта использования, наивно пытались «смоделировать абстракцию математического набора» (из javadoc) и просто забыли добавить полезный метод get().

Теперь перейдем к подразумеваемому вопросу " как тогда вы получите элемент": я думаю, что лучшее решение - использовать Map<E,E> вместо Set<E>, чтобы сопоставить элементы самим себе. Таким образом, вы можете эффективно извлечь элемент из "набора", потому что метод get () Map найдет элемент с использованием эффективной хеш-таблицы или алгоритма дерева. Если вы хотите, вы можете написать собственную реализацию Set, которая предлагает дополнительный метод get(), инкапсулирующий Map.

Следующие ответы, на мой взгляд, плохие или неправильные:

«Вам не нужно получать элемент, потому что у вас уже есть равный объект»: утверждение неверно, как вы уже показали в вопросе. Два равных объекта все же могут иметь различное состояние, которое не имеет отношения к равенству объектов. Цель состоит в том, чтобы получить доступ к этому состоянию элемента, содержащегося в Set, а не к состоянию объекта, используемого в качестве «запроса».

«У вас нет другого выбора, кроме как использовать итератор»: это линейный поиск по коллекции, который совершенно неэффективен для больших наборов (по иронии судьбы, внутренне Set организован как хэш-карта или дерево, к которому можно обратиться эффективно). Не делай этого! Я видел серьезные проблемы с производительностью в реальных системах с использованием этого подхода. На мой взгляд, что ужасного в пропущенном get() методе, это не столько то, что его обходить немного громоздко, но в том, что большинство программистов будут использовать подход линейного поиска, не задумываясь о последствиях.

98 голосов
/ 02 сентября 2011

Не было бы смысла получать элемент, если он равен. A Map лучше подходит для этого варианта использования.


Если вы все еще хотите найти элемент, у вас нет другого выбора, кроме как использовать итератор:

public static void main(String[] args) {

    Set<Foo> set = new HashSet<Foo>();
    set.add(new Foo("Hello"));

    for (Iterator<Foo> it = set.iterator(); it.hasNext(); ) {
        Foo f = it.next();
        if (f.equals(new Foo("Hello")))
            System.out.println("foo found");
    }
}

static class Foo {
    String string;
    Foo(String string) {
        this.string = string;
    }
    @Override
    public int hashCode() { 
        return string.hashCode(); 
    }
    @Override
    public boolean equals(Object obj) {
        return string.equals(((Foo) obj).string);
    }
}
20 голосов
/ 25 мая 2015

Преобразовать в список, а затем использовать get метод списка

Set<Foo> set = ...;
List<Foo> list = new ArrayList<Foo>(set);
Foo obj = list.get(0);
12 голосов
/ 02 сентября 2011

Если у вас есть равный объект, зачем вам тот из набора?Если он «равен» только по ключу, то 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);
12 голосов
/ 19 июля 2016

Набор по умолчанию в Java, к сожалению, не предназначен для обеспечения операции get, как точно объяснил jschreiner .

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

Переопределение реализации equals, так что неравные объекты являются «равными», как правильно сказано David Ogren , может легко вызвать проблемы с обслуживанием.

И использование Map в качестве явной замены (как предлагают многие), imho, делает код менее элегантным.

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


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

Передача объекта через Интернет означала, что у клиента все равно были разные экземпляры этого объекта. Чтобы сопоставить этот «скопированный» экземпляр с исходным, я решил использовать Java UUID.

Итак, я создал абстрактный класс UniqueItem, который автоматически присваивает случайный уникальный идентификатор каждому экземпляру его подклассов.

Этот UUID используется совместно клиентом и экземпляром сервера, поэтому таким образом можно легко сопоставить их, просто используя Map.

Однако непосредственное использование Карты в аналогичном сценарии использования было все еще не элегантным. Кто-то может возразить, что использование карты может быть более сложным в обслуживании и обработке.

По этим причинам я реализовал библиотеку MagicSet, которая делает использование карты "прозрачным" для разработчика.

https://github.com/ricpacca/magicset


Как и исходный Java HashSet, MagicHashSet (который является одной из реализаций MagicSet, представленных в библиотеке) использует вспомогательный HashMap, но вместо того, чтобы использовать элементы в качестве ключей и фиктивное значение в качестве значений, он использует UUID элемент как ключ и сам элемент как значение. Это не вызывает накладных расходов на использование памяти по сравнению с обычным HashSet.

Более того, MagicSet можно использовать именно как набор, но с некоторыми другими методами, обеспечивающими дополнительные функции, такими как 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
}
10 голосов
/ 20 августа 2014

Если ваш набор на самом деле NavigableSet<Foo> (например, TreeSet) и Foo implements Comparable<Foo>, вы можете использовать

Foo bar = set.floor(foo); // or .ceiling
if (foo.equals(bar)) {
    // use bar…
}

(Спасибо за комментарий @ eliran-malka за подсказку.)

9 голосов
/ 20 июля 2016

С Java 8 вы можете сделать:

Foo foo = set.stream().filter(item->item.equals(theItemYouAreLookingFor)).findFirst().get();

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

4 голосов
/ 10 февраля 2014
Object objectToGet = ...
Map<Object, Object> map = new HashMap<Object, Object>(set.size());
for (Object o : set) {
    map.put(o, o);
}
Object objectFromSet = map.get(objectToGet);

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

2 голосов
/ 22 апреля 2017

Почему:

Кажется, что Set играет полезную роль в обеспечении средств сравнения. Он предназначен для хранения дубликатов элементов.

Из-за этого намерения / дизайна, если кто-то получит () ссылку на сохраненный объект, а затем изменит его, вполне возможно, что конструктивные намерения Set могут быть сорваны и могут вызвать неожиданное поведение.

Из JavaDocs

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

Как:

Теперь, когда Стримы были введены, можно сделать следующее

mySet.stream()
.filter(object -> object.property.equals(myProperty))
.findFirst().get();
1 голос
/ 02 сентября 2011

Для этой цели лучше использовать объект Java HashMap http://download.oracle.com/javase/1,5.0/docs/api/java/util/HashMap.html

...