LinkedHashSet - порядок вставки и дубликаты - держать самые новые сверху

Мне нужна коллекция, которая сохраняет порядок вставки и имеет уникальные значения. LinkedHashSet выглядит как способ, но есть одна проблема — когда два элемента равны, он удаляет самый новый (что имеет смысл), вот пример:

set.add("one");
set.add("two");
set.add("three");
set.add("two");

LinkedHashSet напечатает:

one, two, three

Но мне нужно:

one, three, two

Что было бы лучшим решением здесь? Есть ли какой-либо метод сбора/коллекции, который может это сделать, или я должен реализовать его вручную?


person rafakob    schedule 04.04.2016    source источник
comment
Как насчет метода ArrayList и использования метода contains перед вставкой?   -  person ortis    schedule 04.04.2016
comment
Я подозреваю, что вам придется реализовать свой собственный алгоритм для этого, вероятно, подкрепленный HashSet.   -  person Mena    schedule 04.04.2016
comment
@AvihooMamka TreeSet сохраняет элементы, отсортированные в соответствии с естественным или заданным порядком, сам по себе порядок вставки не сохраняется.   -  person Mena    schedule 04.04.2016
comment
Обратите внимание, что LinkedHashSet не final, но у вас не будет доступа к резервной карте, используемой для хранения/извлечения элементов.   -  person Mena    schedule 04.04.2016
comment
Вы можете использовать карту. Каждый раз, когда пользователь вводит букву, вы будете обновлять эту карту.   -  person dneranjan    schedule 04.04.2016
comment
Как говорили другие, вам нужна собственная реализация. Следует полагаться на LinkedHashSet, просто удалите элемент при добавлении, чтобы получить желаемое поведение.   -  person duduamar    schedule 04.04.2016
comment
Неправильно понял вопрос и подумал, что напечатано one, three, two, а вам нужно one, two, three. Потерпеть неудачу.   -  person bcsb1001    schedule 04.04.2016
comment
Спасибо, парни. Как я и думал, нет коллекции, обеспечивающей такое поведение.   -  person rafakob    schedule 04.04.2016
comment
HashMap заменяет старое значение, но в случае с HashSet элемент не вставляется.   -  person dneranjan    schedule 04.04.2016


Ответы (4)


Большинство коллекций Java можно расширить для настройки.

Подкласс LinkedHashSet, переопределяющий add.

class TweakedHashSet<T> extends LinkedHashSet<T> {

    @Override
    public boolean add(T e) {
        // Get rid of old one.
        boolean wasThere = remove(e);
        // Add it.
        super.add(e);
        // Contract is "true if this set did not already contain the specified element"
        return !wasThere;
    }

}
person OldCurmudgeon    schedule 04.04.2016
comment
Он работает так, как я хочу. То же самое можно применить к ArrayList. В таком случае есть ли причина, по которой мне следует использовать LinkedHashSet? - person rafakob; 04.04.2016
comment
@rafakob - ArrayList разрешает дубликаты. - person OldCurmudgeon; 04.04.2016
comment
@OldCurmudgeon: нет, если вы удалите перед вставкой - person ytoledano; 04.04.2016
comment
@rafakob удаление должно быть более эффективным в LinkedHashSet, но вам нужно будет профилировать, чтобы быть уверенным (зависит, среди прочего, от качества типичной реализации hashCode ваших T и типичного размера вашей коллекции) - person Hulk; 04.04.2016
comment
@hulk, помимо функций remove и contains, ArrayList, несомненно, превзойдет LinkedHashSet во всем остальном. Наличие всей структуры в непрерывном блоке памяти помогает с доступом к памяти и оптимизацией JIT. Тем не менее, вы на 100% правы в том, что профилирование - это путь - я подозреваю, что повышение производительности будет в 3 или 4 раза, чего, честно говоря, мне недостаточно, если только это не очень важный блок кода. . В большинстве случаев я гораздо больше забочусь о правильности, чем о 3-4-кратной скорости поиска в структуре данных. - person corsiKa; 04.04.2016
comment
@corsiKa: разница не в коэффициенте 3 или 4, а в масштабировании, поэтому при достаточно большой коллекции разница в производительности будет болезненной. Но, конечно, мы ничего не знаем о максимальном размере коллекции OP и о том, как часто будет происходить поиск/модификация по сравнению с итерациями… - person Holger; 04.04.2016
comment
@Holger Я когда-либо рассматривал только большую коллекцию (потому что в небольшой коллекции, кого это волнует?) В поведении основной коллекции, но LinkedHashSet будет быстрее в remove и contains и медленнее в add и iterator, независимо от масштаба. Это правда, что по мере увеличения масштабов вредность удаления в ArrayList хуже, чем неудача итерации в LinkedHashSet. Как я и договорился с Халком, профилирование является ключевым. - person corsiKa; 04.04.2016
comment
@corsiKa: в принципе, мы согласны с этим. Для конкретного приложения ключевое значение имеет профилирование и анализ вопроса о том, может ли значительно большее количество элементов появиться в другом сценарии/в производственной среде/у другого клиента/и т. д. Если вы знайте, что в вашей задаче, тесте и профиле есть внутренний предел с ожидаемым максимумом… - person Holger; 04.04.2016
comment
Вы можете упростить метод до return !remove(e) && super.add(e);, так как super.add(e); всегда будет возвращать true - person Holger; 10.05.2016
comment
@Holger - && не будет добавлять, если он уже есть, просто удалит объект. вместо этого следует использовать &. - person niksvp; 07.07.2016
comment
@niksvp - & побитовое - хотим логично и здесь. - person OldCurmudgeon; 07.07.2016
comment
@OldCurmudgeon - logical не будет добавлять (оценивать второй оператор), если элемент уже присутствует в наборе. он просто удалит элемент. проверить образец данных в вопросе. вы останетесь с one, three в выводе. в то время как ожидается, что элемент будет добавлен обратно после удаления, если он уже присутствует. побитового оператора будет достаточно. - person niksvp; 07.07.2016
comment
@niksvp - Хороший вызов логике - ИМХО, использование тонких языковых хаков, таких как побитовая логика, не является решением - это работает сейчас? - person OldCurmudgeon; 07.07.2016
comment
@OldCurmudgeon - использование логического ИЛИ снова приведет к другой проблеме. он никогда не добавит никаких значений, если его нет. Таким образом, коллекция остается пустой все время. Чтобы упростить bitwise, это единственное решение, но, как вы утверждали выше, версия 4 является альтернативным правильным решением. - person niksvp; 07.07.2016
comment
@niksvp: правильно, здесь должно быть return !remove(e) & super.add(e);, небольшая опечатка, большое влияние. - person Holger; 07.07.2016
comment
@OldCurmudgeon: тот факт, что оператор & можно использовать в качестве побитового оператора для значений int, не делает его основной ролью. Для аргументов boolean это логический оператор, как официально заявлено, см. JLS §15.22.2. На самом деле оператор && описывается так: «Оператор условного И && похож на & (§15.22.2), но оценивает свой правый операнд только в том случае, если значение его левого операнда равно true». - person Holger; 07.07.2016
comment
@Holger - Подтверждено - настоящим я смиренно призываю аргумент ясности / краткости в этом случае. - person OldCurmudgeon; 07.07.2016

Вы можете просто использовать специальную функцию LinkedHashMap:

Set<String> set = Collections.newSetFromMap(new LinkedHashMap<>(16, 0.75f, true));
set.add("one");
set.add("two");
set.add("three");
set.add("two");
System.out.println(set); // prints [one, three, two]

В Oracle JRE LinkedHashSet в любом случае поддерживается LinkedHashMap, поэтому особой функциональной разницы нет, но используемый здесь специальный конструктор настраивает LinkedHashMap для изменения порядка при каждом доступе, а не только при вставке< /эм>. Это может показаться слишком большим, но на самом деле влияет только на вставку уже содержащихся ключей (значений в смысле Set). Другие затронутые операции Map (а именно get) не используются возвращаемым Set.

Если вы не используете Java 8, вам придется немного помочь компилятору из-за ограниченного вывода типов:

Set<String> set
    = Collections.newSetFromMap(new LinkedHashMap<String, Boolean>(16, 0.75f, true));

но функционал тот же.

person Holger    schedule 04.04.2016
comment
+1 Мне нравится такой подход. Но я бы рекомендовал поместить создание этого набора в хорошо названный метод (createAccessOrderSet?) и добавить несколько модульных тестов, которые очень ясно показывают, что он делает. - person Hulk; 04.04.2016
comment
@Hulk: конечно, это правильно для производственного кода. Затем вы, вероятно, предоставите перегрузки для предоставления параметров емкости и коэффициента загрузки. Я использовал здесь те же значения, что и в конструкторе по умолчанию, но без задокументированного фабричного метода они будут выглядеть для читателя как магические литералы… - person Holger; 04.04.2016

При инициализации вы LinkedHashSet вы можете переопределить метод добавления.

Set<String> set = new LinkedHashSet<String>(){
    @Override
    public boolean add(String s) {
        if(contains(s))
            remove(s);
        return super.add(s);
    }
};

Теперь это дает вам:

set.add("1");
set.add("2");
set.add("3");
set.add("1");
set.addAll(Collections.singleton("2"));

// [3, 1 ,2]

даже метод addAll работает.

person Toonijn    schedule 04.04.2016

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

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

public static <T> void addToList(List<T> list, T element) {
    list.remove(element); // Will remove element from list, if list contains it
    list.add(element); // Will add element again to the list 
}

И мы можем вызвать этот метод, чтобы добавить элемент в наш список

List<String> list = new ArrayList<>();

addToList(list, "one");
addToList(list, "two");
addToList(list, "three");
addToList(list, "two");

Единственным недостатком здесь является то, что нам нужно каждый раз вызывать наш пользовательский метод addToList() вместо list.add().

person Naresh Joshi    schedule 20.11.2016
comment
Красиво и просто. Но я не уверен, почему вы используете список вместо LinkedHashSet в соответствии с вопросом. - person shmosel; 12.01.2017
comment
Спасибо @shmosel, я рассмотрел ArrayList для своего примера кода, потому что LinkedHashSet не поддерживал порядок, который хочет пользователь, плюс ArrayList также является самым простым и быстрым способом решения этой проблемы. - person Naresh Joshi; 13.01.2017
comment
LinkedHashSet поддерживает порядок вставки, как и ArrayList. И это более эффективно при удалении. - person shmosel; 13.01.2017
comment
list.remove(element) выполняется за время O(N). По мере того, как ваш список становится большим, производительность вашего метода addToList быстро ухудшается. С другой стороны, HashSet запускает contains(element) за O(1) независимо от размера. - person Danny C; 16.03.2021