что такое set в java
Интерфейс Java Set
Интерфейс Java Set, java.util.Set, представляет коллекцию объектов, где каждый объект уникален. Другими словами, один и тот же объект не может встречаться более одного раза в наборе. Интерфейс является стандартным и подтипом интерфейса Collection, что означает, что Set наследуется от Collection.
Вы можете добавить любой объект в набор. Если набор не типизирован с использованием Java Generics, то вы можете даже смешивать объекты разных типов (классов) в одном наборе. Однако в действительности смешивание объектов разных типов в одном наборе не часто выполняется.
Сравнение со списком
Интерфейсы Set и Java List очень похожи друг на друга и представляет собой набор элементов. Тем не менее, есть некоторые существенные различия. Эти различия отражены в методах, которые содержат интерфейсы Set и List.
Пример набора
Вот первый простой пример:
В этом примере создается HashSet, который является одним из классов в API Java, которые реализуют интерфейс Set. Затем он добавляет строковый объект в набор и, наконец, проверяет, содержит ли набор только что добавленный элемент.
Установление реализации
Будучи подтипом Collection, все методы в интерфейсе Collection также доступны в интерфейсе Set.
Поскольку Set – это интерфейс, вам нужно создать конкретную реализацию, чтобы использовать его. Вы можете выбрать:
Каждая из этих реализаций Set ведет себя немного по-разному в отношении порядка элементов при итерации набора и времени (большая запись O), необходимого для вставки и доступа к элементам в наборах.
HashSet поддерживается HashMap. Он не дает никаких гарантий относительно последовательности элементов при их итерации.
LinkedHashSet отличается от HashSet тем, что гарантирует, что порядок элементов во время итерации совпадает с порядком их вставки в LinkedHashSet. Повторная вставка элемента, который уже находится в LinkedHashSet, не меняет этот порядок.
TreeSet также гарантирует порядок элементов при повторении, но он является порядком сортировки элементов. Другими словами, порядок, в котором элементы должны быть отсортированы, если вы использовали Collections.sort() для List или массива, содержащего эти элементы. Этот порядок определяется либо их естественным порядком(если они реализуют Comparable), либо конкретной реализацией Comparator.
Вот несколько примеров того, как создать экземпляр Set:
Добавить элемент в набор
Чтобы добавить элементы в Set, вы вызываете его метод add(). Этот метод унаследован от интерфейса Collection. Вот несколько примеров:
Три вызова add() добавляют экземпляр String к набору.
Перебор элементов набора
Есть два способа перебора элементов набора Java:
Обе эти опции описаны в следующих разделах.
При выполнении итерации элементов в Set порядок элементов зависит от того, какую реализацию вы используете.
Итерация множества с помощью итератора
Чтобы выполнить итерацию элементов набора с помощью итератора, сначала необходимо получить его из набора. Вы получаете Iterator из Set, вызывая метод iterator():
Итерация множества с использованием цикла For-Each
Второй способ перебора элементов набора – использование цикла for-each. Вот как выглядит итерация элементов Set с использованием цикла for-each:
Интерфейс Set реализует интерфейс Java Iterable. Вот почему вы можете перебирать элементы набора, используя цикл for-each.
С использованием API Java Stream
Чтобы выполнить итерацию с помощью API-интерфейса Java Stream, необходимо создать поток из набора:
Как удалить элементы
Используется метод remove(Object o):
Невозможно удалить объект на основе индекса в наборе, поскольку порядок элементов зависит от его реализации.
Как удалить все элементы
Используется метод clear():
Как добавить все элементы из другой коллекции
После выполнения set2 будет содержать четыре элемента String, а также три элемента String: один, два и три из набора.
Как удалить все элементы из другой коллекции
После запуска набор будет содержать элементы String один и два. Третий элемент был удален, потому что он присутствовал в set2, который был задан как параметр для set.removeAll(set2).
Как сохранить все элементы, присутствующие в другой коллекции
После запуска набор будет содержать только элемент String три, так как присутствует как в set, так и в set2.
Как установить размер
Используется метод size(). Размер набора – это количество элементов, содержащихся в наборе. Вот пример чтения размера:
После выполнения переменная размера будет иметь значение 3, потому что созданный в примере набор имеет 3 добавленных элемента.
Как проверить, пуст ли Set
Используется метод isEmpty() для Set:
После запуска этого кода переменная isEmpty будет содержать значение true, потому что Set пуст (в нем нет элементов).
Вы также можете проверить, является ли Set пустым, сравнив значение, возвращаемое методом size() с 0:
После выполнения этого кода переменная isEmpty будет содержать значение true, потому что метод Set size() возвращает 0 – в примере не содержится элементов.
Как проверить, содержится ли элемент
Вы можете проверить, содержит ли Set данный элемент (объект), вызвав метод contains():
После выполнения этого кода переменная contains123 будет содержать значение true, потому что Set на самом деле содержит строку 123.
Чтобы определить, содержит ли набор элемент, он будет внутренне выполнять итерации своих элементов и сравнивать каждый с объектом, переданным в качестве параметра. Для сравнения используется метод равенства.
Поскольку можно добавить нулевые значения, также можно проверить, содержит ли набор нулевое значение:
Очевидно, что если входной параметр для contains() имеет значение null, метод contains() не будет использовать метод equals() для сравнения с каждым элементом, а вместо этого использует оператор ==.
Универсальные
По умолчанию вы можете поместить любой объект в набор, но с Java 5 Java Generics позволяет ограничить типы объектов, которые вы можете вставить:
Этот набор теперь может содержать только экземпляры MyObject. Затем вы можете получить доступ к элементам и выполнить итерацию без их приведения:
Как конвертировать в список
Вы можете преобразовать Set в List, создав List и вызвав его метод addAll(), передав Set в качестве параметра:
После запуска этого примера список будет содержать строковые элементы 123 и 456 – так как это были все элементы, присутствующие в наборе при вызове List addAll(set).
Готовимся к собеседованию: что нужно знать о коллекциях в Java
Освежаем знания о коллекциях в Java и закрепляем их на практике.
Коллекции в Java — одна из любимых тем на собеседованиях Java-разработчиков любого уровня. Без них не обходятся и экзамены на сертификат Java Professional.
Вспомним основные типы коллекций, их реализации в Java, проверим понимание на практике.
Что такое коллекции
Коллекции — это наборы однородных элементов. Например, страницы в книге, яблоки в корзине или люди в очереди.
Инструменты для работы с такими структурами в Java содержатся в Java Collections Framework. Фреймворк состоит из интерфейсов, их реализаций и утилитарных классов для работы со списками: сортировки, поиска, преобразования.
Фулстек-разработчик. Любимый стек: Java + Angular, но в хорошей компании готова писать хоть на языке Ада.
Галопом по Европам, или Кратко об интерфейсах
Set — это неупорядоченное множество уникальных элементов.
Например, мешочек с бочонками для игры в лото: каждый номер от 1 до 90 встречается в нём ровно один раз, и заранее неизвестно, в каком порядке бочонки вынут при игре.
List — упорядоченный список, в котором у каждого элемента есть индекс. Дубликаты значений допускаются.
Например, последовательность букв в слове: буквы могут повторяться, при этом их порядок важен.
Queue — очередь. В таком списке элементы можно добавлять только в хвост, а удалять — только из начала. Так реализуется концепция FIFO ( first in, first out) — «первым пришёл — первым ушёл». Вам обязательно напомнят это правило, если попробуете пролезть без очереди в магазине:
А ещё есть LIFO (last in, first out), то есть «последним пришёл — первым ушёл». Пример — стопка рекламных буклетов на ресепшене отеля: первыми забирают самые верхние (положенные последними). Структуру, которая реализует эту концепцию, называют стеком.
Deque может выступать и как очередь, и как стек. Это значит, что элементы можно добавлять как в её начало, так и в конец. То же относится к удалению.
Будет здорово, если на собеседовании вы назовёте Deque правильно: «дэк», а не «д экью», как часто говорят.
Map состоит из пар «ключ-значение». Ключи уникальны, а значения могут повторяться. Порядок элементов не гарантирован. Map позволяет искать объекты (значения) по ключу.
Пример: стопка карточек с иностранными словами и их значениями. Для каждого слова (ключ) на обороте карточки есть вариант перевода (значение), а вытаскивать карточки можно в любом порядке.
Не путайте интерфейс Collection и фреймворк Collections. Map не наследуется от интерфейса Collection, но входит в состав фреймворка Collections.
Соберём всё вместе
Set | List | Queue | Map | |
---|---|---|---|---|
Возможны дубликаты | ❌ | ✅ | ✅ | ✅ для значений |
❌ для ключей
Такие разные реализации
Реализаций интерфейсов так много, что при желании можно организовать вполне себе упорядоченный Map и даже отсортированное множество. Пройдёмся кратко по основным классам.
Реализации List
Класс ArrayList подойдёт в большинстве случаев, если вы уже определились, что вам нужен именно список (а не Map, например).
Строится на базе обычного массива. Если при создании не указать размерность, то под значения выделяется 10 ячеек. При попытке добавить элемент, для которого места уже нет, массив автоматически расширяется — программисту об этом специально заботиться не нужно.
Список проиндексирован. При включении нового элемента в его середину все элементы с б ольшим индексом сдвигаются вправо:
При удалении элемента все остальные с бо́льшим индексом сдвигаются влево:
Класс LinkedList реализует одновременно List и Deque. Это список, в котором у каждого элемента есть ссылка на предыдущий и следующий элементы:
Благодаря этому добавление и удаление элементов выполняется быстро — времязатраты не зависят от размера списка, так как элементы при этих операциях не сдвигаются: просто перестраиваются ссылки.
На собеседованиях часто спрашивают, когда выгоднее использовать LinkedList, а когда — ArrayList.
Правильный ответ таков: если добавлять и удалять элементы с произвольными индексами в списке нужно чаще, чем итерироваться по нему, то лучше LinkedList. В остальных случаях — ArrayList.
В целом так и есть, но вы можете блеснуть эрудицией — рассказать, что под капотом. При добавлении элементов в ArrayList (или их удалении) вызывается нативный метод System.arraycopy. В нём используются ассемблерные инструкции для копирования блоков памяти. Так что даже для больших массивов эти операции выполняются за приемлемое время.
Реализации Queue
Про одну из них, LinkedList, мы рассказали выше.
Класс ArrayDeque — это реализация двунаправленной очереди в виде массива с переменным числом элементов.
Новые значения можно добавлять в начало или конец списка, и удалять оттуда же. Причём эти операции выполняются быстрее, чем при использовании LinkedList.
Класс PriorityQueue — упорядоченная очередь. По умолчанию элементы добавляются в естественном порядке: числа по возрастанию, строки по алфавиту и так далее, либо алгоритм сравнения задаёт разработчик.
Этот класс может быть полезен, например, для нахождения n минимальных чисел в большом неупорядоченном списке:
Такая реализация выгоднее по скорости и объёму памяти, чем подход с сортировкой первоначального списка.
Реализации Set
Класс HashSet использует для хранения данных в хеш-таблице. Это значит, что при манипуляциях с элементами используется хеш-функция — hashCode() в Java.
Хеш-таблица — структура данных, в которой все элементы помещаются в бакеты (buckets), соответствующие результату вычисления хеш-функции.
Например, администратор в гостинице может класть ключ в коробку с номером от 1 до 9, вычисляя его по такому алгоритму: складывать все цифры номера, пока не получится одноразрядное число.
Здесь алгоритм вычисления — хеш-функция, а результат вычисления — хеш-код.
Тогда ключ от номера 356 попадёт в коробку 5 (3 + 5 + 6 = 14; 1 + 4 = 5), а ключ от номера 123 — в коробку с номером 6.
Добавление, поиск и удаление элементов при такой организации происходит за постоянное время, независимо от числа элементов в коллекции.
О классе TreeSet вспоминают в тех случаях, когда множество должно быть упорядочено. Каким образом упорядочивать — определяет разработчик при создании нового TreeSet. По умолчанию элементы располагаются в естественном порядке. Организованы они в виде красно-чёрного дерева.
Реализации Map
Класс HashMap хранит данные в виде хеш-таблицы, как и HashSet. Более того, HashSet внутри использует HashMap. При этом ключом выступает сам элемент.
Класс TreeMap строится тоже на базе красно-чёрного дерева. Элементы здесь упорядочены (в естественном или заданном при создании порядке) в каждый момент времени. При этом вставка и удаление более затратны, чем в случае с HashMap.
Класс LinkedHashMap расширяет возможности HashMap тем, что позволяет итерироваться по элементам в порядке их добавления. Как и в LinkedList, здесь каждая пара-значение содержит ссылку на предыдущий и последующий элементы.
Ещё один хитрый вопрос на собеседовании: в каких коллекциях допускаются null-элементы?
Ответ: почти во всех, но нельзя добавлять null-значения в упорядоченные структуры, которые при добавлении нового элемента используют сравнение.
Обоснование: мух советуют отделять от котлет — иными словами, нельзя сравнивать принципиально разные, несопоставимые вещи. Так же и в Java невозможно понять, что больше: null или число 1, или null или строка «hello».
Поэтому null-значения запрещены в TreeMap и TreeSet.
Ещё они недопустимы в ArrayDeque, так как методы этого класса (например, poll() — удаление элемента из начала очереди) используют null как признак пустоты коллекции.
Попрактикуемся
Чтобы убедиться, что вы не просто вызубрили теорию, а хорошо понимаете предмет, на собеседовании вам могут предложить задания вроде «что произойдёт при выполнении кода»
Разберём типовые задачи на понимание коллекций.
Задачи для ArrayList
Что будет напечатано после выполнения кода ниже:
Правильный ответ: test2:test4:test1:test4:test2:test3:
Элементы в ArrayList нумеруются начиная с нуля. Поэтому элемент с номером 1 — это test2.
Следующим действием мы добавляем строку «test4» в ячейку с индексом 1. При этом элементы с бо́льшим индексом сдвигаются вправо.
Вторая часть вывода ( test4) показывает, что теперь по индексу 1 извлекается именно test4.
Далее мы обходим все элементы списка и убеждаемся, что они выводятся именно в порядке добавления.
Что будет выведено при выполнении кода:
Правильный ответ: 2:2
Первая часть понятна: добавили два элемента, поэтому размер списка равен двум. Остаётся вопрос: почему не был удалён «test1»?
Перед удалением элемента его нужно найти в списке. ArrayList и остальные коллекции, которые не используют алгоритмы хеширования, применяют для поиска метод equals().
Строки сравниваются по значению, поэтому «test3» не эквивалентно «test1» и «test2». А раз ни один элемент не соответствует критерию поиска, ничего не удалится — размер списка останется прежним.
Проверьте себя: подумайте, что произойдёт, если вместо
Задачи для Set
Что выведет фрагмент кода ниже:
Правильный ответ: 3:, а дальше точно не известно.
Так как строки сравниваются по значению, а дубликаты во множествах недопустимы, второй «Иван» не станет частью множества. В итоге размер множества будет равен 3.
В каком порядке будут выведены элементы множества — определённо мы сказать не можем: во множествах порядок добавления не сохраняется.
Что выведет фрагмент кода:
Правильный ответ: 4.
Как же так, ведь во множество должны попадать уникальные элементы?
Прежде чем добавить новый элемент в множество, вычисляется его hashCode() — чтобы определить бакет, куда он может быть помещён.
Если бакет пуст, элемент будет добавлен. Иначе уже добавленные элементы с таким же значением хеша сравниваются с кандидатом при помощи метода equals(). Если дубликат не найден, новый элемент становится частью множества. Он попадёт в тот же бакет.
Мы добавляем в Set объекты типа Person — созданного нами класса. Этот класс, как и все ссылочные типы, наследуется от класса Object.
Так как мы не переопределили метод hashCode(), будет использована родительская реализация. В ней хеш вычисляется на основе данных адреса (реализация зависит от JVM).
Метод equals() тоже не переопределён. В классе-родителе этот метод просто сравнивает ссылки на объекты. Это значит, что даже если хеш случайно совпадёт для каких-то из четырёх элементов, equals() в любом случае вернёт false.
Таким образом, каждый из четырёх кандидатов попадёт в множество.
Проверьте себя: изменится ли что-нибудь, если переопределить hashCode() вот так:
А если ещё и equals() переопределить, как на фрагменте ниже:
Набор данных интерфейса Set
Реализация интерфейса Set представляет собой неупорядоченную коллекцию, которая не может содержать дублирующие данные.
Интерфейс Set включает следующие методы :
Метод | Описание |
---|---|
add(Object o) | Добавление элемента в коллекцию, если он отсутствует. Возвращает true, если элемент добавлен. |
addAll(Collection c) | Добавление элементов коллекции, если они отсутствуют. |
clear() | Очистка коллекции. |
contains(Object o) | Проверка присутствия элемента в наборе. Возвращает true, если элемент найден. |
containsAll(Collection c) | Проверка присутсвия коллекции в наборе. Возвращает true, если все элементы содержатся в наборе. |
equals(Object o) | Проверка на равенство. |
hashCode() | Получение hashCode набора. |
isEmpty() | Проверка наличия элементов. Возвращает true если в коллекции нет ни одного элемента. |
iterator() | Функция получения итератора коллекции. |
remove(Object o) | Удаление элемента из набора. |
removeAll(Collection c) | Удаление из набора всех элементов переданной коллекции. |
retainAll(Collection c) | Удаление элементов, не принадлежащих переданной коллекции. |
size() | Количество элементов коллекции |
toArray() | Преобразование набора в массив элементов. |
toArray(T[] a) | Преобразование набора в массив элементов. В отличии от предыдущего метода, который возвращает массив объектов типа Object, данный метод возвращает массив объектов типа, переданного в параметре. |
К семейству интерфейса Set относятся HashSet, TreeSet и LinkedHashSet. В множествах Set разные реализации используют разный порядок хранения элементов. В HashSet порядок элементов оптимизирован для быстрого поиска. В контейнере TreeSet объекты хранятся отсортированными по возрастанию. LinkedHashSet хранит элементы в порядке добавления.
Набор данных HashSet
Конструкторы HashSet :
Методы HashSet
HashSet содержит методы аналогично ArrayList. Исключением является метод add(Object o), который добавляет объект только в том случае, если он отсутствует. Если объект добавлен, то метод add возвращает значение — true, в противном случае false.
Пример использования HashSet :
В консоли мы должны увидеть только 4 записи. Следует отметить, что порядок добавления записей в набор будет непредсказуемым. HashSet использует хэширование для ускорения выборки.
Следует отметить, что реализация HashSet не синхронизируется. Если многократные потоки получают доступ к набору хеша одновременно, а один или несколько потоков должны изменять набор, то он должен быть синхронизирован внешне. Это лучше всего выполнить во время создания, чтобы предотвратить случайный несинхронизируемый доступ к набору :
Набор данных LinkedHashSet
Класс LinkedHashSet наследует HashSet, не добавляя никаких новых методов, и поддерживает связный список элементов набора в том порядке, в котором они вставлялись. Это позволяет организовать упорядоченную итерацию вставки в набор.
Также, как и HashSet, LinkedHashSet не синхронизируется. Поэтому при использовании данной реализации в приложении с множеством потоков, часть из которых может вносить изменения в набор, следует на этапе создания выполнить синхронизацию :
Набор данных TreeSet
Класс TreeSet создаёт коллекцию, которая для хранения элементов использует дерево. Объекты хранятся в отсортированном порядке по возрастанию.
Конструкторы TreeSet :
Методы TreeSet
В следующем измененном примере с использования TreeSet в консоль будут выведены значения в упорядоченном виде.
Множества: Set, HashSet, LinkedHashSet, TreeSet
HashSet, TreeSet и LinkedHashSet относятся к семейству Set. В множествах Set каждый элемент хранится только в одном экземпляре, а разные реализации Set используют разный порядок хранения элементов. В HashSet порядок элементов определяется по сложному алгоритму. Если порядок хранения для вас важен, используйте контейнер TreeSet, в котором объекты хранятся отсортированными по возрастанию в порядке сравнения или LinkedHashSet с хранением элементов в порядке добавления.
Множества часто используются для проверки принадлежности, чтобы вы могли легко проверить, принадлежит ли объект заданному множеству, поэтому на практике обычно выбирается реализация HashSet, оптимизированная для быстрого поиска.
В Android 11 (R) обещают добавить несколько перегруженных версий метода of(), которые являются частью Java 8.
HashSet
Название Hash. происходит от понятия хэш-функция. Хэш-функция — это функция, сужающая множество значений объекта до некоторого подмножества целых чисел. Класс Object имеет метод hashCode(), который используется классом HashSet для эффективного размещения объектов, заносимых в коллекцию. В классах объектов, заносимых в HashSet, этот метод должен быть переопределен (override).
Имеет два основных конструктора (аналогично ArrayList):
Методы
Методы аналогичны методам ArrayList за исключением того, что метод add(Object o) добавляет объект в множество только в том случае, если его там нет. Возвращаемое методом значение — true, если объект добавлен, и false, если нет.
Итак, создадим множество стран.
Нажав на кнопку, вы получите результат Размер HashSet = 4.
Даже если вы попытаетесь схитрить и дополнительно вставить строку countryHashSet.add(«Кот-Д’Ивуар»); после России, то всё-равно размер останется прежним.
Убедиться в этом можно, если вызвать метод iterator(), который позволяет получить всё множество элементов:
Несмотря на наше упрямство, мы видим только четыре добавленных элемента.
Стоит отметить, что порядок добавления стран во множество будет непредсказуемым. HashSet использует хэширование для ускорения выборки. Если вам нужно, чтобы результат был отсортирован, то пользуйтесь TreeSet.
Преобразовать в массив и вывести в ListView
Продолжим опыты. Поработаем теперь с числами.
Здесь мы ещё раз убеждаемся, что повторное добавление числа не происходит. В цикле случайным образом выбирается число от 0 до 9 тысячу раз. Естественно, многие числа должны были повториться при таком сценарии, но во множество каждое число попадёт один раз.
При этом данные не сортируются, так как расположены как попало.
Специально для Android был разработан новый класс ArraySet, который более эффективен.
LinkedHashSet
Класс LinkedHashSet расширяет класс HashSet, не добавляя никаких новых методов. Класс поддерживает связный список элементов набора в том порядке, в котором они вставлялись. Это позволяет организовать упорядоченную итерацию вставки в набор.
TreeSet
Переделанный пример для вывода случайных чисел в отсортированном порядке. HashSet не может гарантировать, что данные будут отсортированы, так как работает по другому алгоритму. Если сортировка для вас важна, то используйте TreeSet.
Со строками это выглядит нагляднее:
Названия стран выведутся в алфавитном порядке.
Класс TreeSet создаёт коллекцию, которая для хранения элементов применяет дерево. Объекты сохраняются в отсортированном порядке по возрастанию.
SortedSet
В примере с TreeSet использовался интерфейс SortedSet, который позволяет сортировать элементы множества. По умолчанию сортировка производится привычным способом, но можно изменить это поведение через интерфейс Comparable.
Кроме стандартных методов Set у интерфейса есть свои методы.