Лекция 9
Last updated
Was this helpful?
Last updated
Was this helpful?
В Java имеется несколько способов хранения объектов (или, точнее, ссылок на объекты). Самый простой вариант хранения объектов - массивы. Массивы обеспечивают самый эффективный способ хранения групп объектов. Однако массив имеет фиксированный размер, а, в общем случае, во время написания программы разработчик не знает точное количество объектов. К тому же массивы неэффективны при большой объеме элементов. Для таких случаев, в Java и в других языках реализован механизм коллекций объектов.
Коллекция - это хранилище, поддерживающее различные способы накопления и упорядочения объектов с целью обеспечения возможностей эффективного доступа к ним. Они представляют собой реализацию абстрактных типов (структур) данных, поддерживающих три основные операции:
добавление нового элемента в коллекцию;
удаление элемента из коллекции;
изменение элемента в коллекции.
Коллекции в Java объединены в библиотеке java.util
и представляют собой контейнеры для хранения и манипулирования объектами. Основные интерфейсы коллекций:
Map<K, V>
- карата отображения вида "ключ-значение" (такие структуры называют словарями или ассоциативными массивами);
Collection<E>
- вершине иерархии, базовый интерфейс для коллекций;
List<E>
- специализирует коллекции для обработки списков;
Set<E>
- специализирует коллекции для обработки множеств, содержащих уникальные элементы.
Ниже представлены основные интерфейсы и классы коллекций.
Этот интерфейс служит основанием, на котором построен весь каркас коллекций, поскольку он должен быть реализован почти всеми классами коллекций (кроме коллекций, реализующих интерфейс Map
). Интерфейс Collection
является обобщенным, ссылочная переменная типа Collection объявляется следующим образом
Интерфейс Collection
расширяет интерфейс Iterable
. Это означает, что все коллекции можно перебирать, организовав цикл foreach
. В интерфейсе Collection
определяются основные методы, которые должны иметь все коллекции. Перечислим некоторые основные методы
Метод
Описание
add(Object o)
Добавляет указанный объект в коллекцию
remove(Object o)
Удаляет указанный объект из коллекции
clear()
Удаляет все элементы из коллекции
size()
Возвращает количество элементов в коллекции
iterator()
Возвращает объект, который используется для доступа к элементам коллекции
Объекты вводятся в коллекции методом add()
. Следует, однако, иметь ввиду, что метод add()
принимает аргумент типа E
. Следовательно, добавляемые в коллекцию объекты должны быть совместимы с предполагаемым типом данных в коллекции. Вызвав метод addAll()
, можно ввести все содержимое одной коллекции в другую.
Вызвав метод remove()
, можно удалить из коллекции отдельный объект. Чтобы из коллекции удалить группу объектов, достаточно вызвать метод removeAll()
. А для того чтобы удалить из коллекции все элементы, кроме указанных, следует вызвать метод retainAll()
. Вызвав метод removeIf()
, можно удалить из коллекции элемент, если он удовлетворяет условию, которое задается в качестве параметра predicate. И наконец, для полной очистки коллекции достаточно вызвать метод clear()
.
Имеется также возможность определить, содержит ли коллекция определенный объект, вызвав метод contains()
. Чтобы определить, содержит ли одна коллекция все члены другой, следует вызвать метод containsAll()
. А определить, пуста ли коллекция, можно с помощью метода isEmpty()
. Количество элементов, содержащихся в данный момент в коллекции, возвращает метод size()
.
Оба метода toArray()
возвращают массив, который содержит элементы, хранящиеся в коллекции. Первый из них возвращает массив класса Object
, а второй - массив элементов того же типа, что и массив, указанный в качестве параметра этого метода. Обычно второй метод более предпочтителен, поскольку он возвращает массив элементов нужного типа. Эти методы оказываются важнее, чем может показаться не первый взгляд. Ведь обрабатывать содержимое коллекции, используя синтаксис массивов, иногда оказывается очень выгодно. Обеспечив связь коллекции с массивом, можно извлечь выгоду из обоих языковых средств Java.
Две коллекции можно сравнить на равенство, вызвав метод equals()
. Точный смысл равенства может зависеть от конкретной коллекции. Например, метод equals()
можно реализовать таким способом, чтобы он сравнивал значения элементов, хранимых в коллекции. В качестве альтернативы методу equals()
можно сравнивать ссылки на эти элементы.
Еще один очень важный метод iterator()
возвращает итератор, а метод spliterator()
- итератор разделитесь для коллекции. Итераторы очень часто используются для обращения с коллекциями. И наконец, методы stream()
и parallelStream()
возвращают поток данных типа Stream
, использующий коллекцию для своих элементов.
Список (List) представляет собой динамический массив - упорядоченный набор элементов и может содержать повторяющиеся элементы. Вы можете получить доступ к любому элементу по индексу. Список является одним из наиболее используемых типов коллекций.
Основные свойства коллекций, реализующих интерфейс List
:
список может включать одинаковые элементы;
элементы в списке хранятся в том порядке, в котором они помещались;
можно получить доступ к любому элементу по его порядковому номеру (индексу) внутри списка.
Начиная с версии Java 9, в интерфейс List
внедрен фабричный метод of()
, у которого имеется целый ряд перегруженных вариантов, возвращающих неизменяемую коллекцию на основе значений, составленную из переданных аргументов.
Особое назначение фабричного метода of()
- предоставить удобный, эффективный способ для создания небольшой коллекции типа List
. Во всех перегружаемых вариантах данного метода не допускается указывать пустые (null) элементы создаваемого списка. И в любом случае конкретная реализация интерфейса List
не указывается.
Существуют два основных класса, реализующих List
.
Класс ArrayList
с превосходной скоростью произвольного доступа к элементам, но относительно медленными операциями вставки и удаления элементов в середине. Пожалуй, самая часто используемая коллекция. ArrayList
инкапсулирует в себе обычный массив, длина которого автоматически увеличивается при добавлении новых элементов.
Так как ArrayList
использует массив, то время доступа к элементу по индексу выполняется за константное время (в отличие от класса LinkedList
). При удалении произвольного элемента из списка, все элементы находящиеся "правее", смещаются на одну ячейку влево, при этом реальный размер массива (его емкость, capacity) не изменяется. Если при добавлении элемента массив будет полностью заполнен, будет создан новый массив размером n * 3 / 2 + 1
, в него будут помещены все элементы из старого массива плюс новый, добавляемый элемент.
Несмотря на то, что емкость объектов типа ArrayList
наращивается автоматически, ее можно увеличивать и вручную. вызывая метод ensureCapacity()
. Это может потребоваться в том случае, если заранее известно, что в коллекции предполагается сохранить намного больше элементов, чем она содержит в данный момент. Увеличив емкость списочного массива в самом начале его обработки, можно избежать дорогостоящей операции постепенного наращивания списка.
С другой стороны, если потребуется уменьшить размер базового массива, на основе которого строится объект типа ArrayList
до текущего количества хранящихся объектов, следует вызвать метод trimToSize()
.
Связанный список LinkedList
представляет собой коллекцию с оптимальным последовательным доступом и низкозатратными операциями вставки и удаления в середине списка. Операция произвольного доступа LinkedList
выполняет относительно медленно, но обладает более широкой функциональностью, чем ArrayList
.
LinkedList
состоит из узлов, каждый из которых содержит как собственно данные, так и две ссылки ("связки") на следующий и предыдущий узел списка. Доступ к произвольному элементу осуществляется за линейное время (но доступ к первому и последнему элементу списка всегда осуществляется за константное время - ссылки постоянно хранятся на первый и последний элементы, так что добавление элемента в конец списка вовсе не значит, что придется перебирать весь список в поисках последнего элемента).
В целом, в абсолютных величинах LinkedList
проигрывает ArrayList
и по потребляемой памяти и по скорости выполнения операций.
Рассмотрим основные методы интерфейса List
Метод
Описание
void add(int index, E obj)
Добавляет в список объект obj
по индексу index
boolean addAll(int index, Collection<? extends E> col)
Добавляет в список все элементы коллекции col
по индексу index
. Если в результате добавления список был изменен, то возвращается true
, иначе возвращается false
E get(int index)
Возвращает из списка объект по индексу index
int indexOf(Object obj)
Возвращает индекс первого экземпляра заданного объекта в списке. Если заданный объект в списке отсутствует, возвращается значение -1
E remove(int index)
Удаляет объект по индексу index
из списка, при этом метод возвращает удаленный объект
E set(int index, E obj)
Присваивает элементу по индексу index
объект obj
(фактически, записывает новую ссылку на объект поверх старой), метод возвращает старый объект
List<E> sublist(int start, int end)
Возвращает список элементов, которые находятся между индексами start
и end
Рассмотрим работу основных методов, заявленных в интерфейсе List
Обратите внимание, что класс ArrayList
преобразуется в List
посредством восходящего преобразования. В идеале, большая часть кода должна взаимодействовать с этими интерфейсами (хотя это не всегда возможно), а точный тип указывается только в точке создания контейнера.
Интерфейс Queue (очередь) расширяет интерфейс Collection
и определяет поведение очереди, которая действует как список по принципу "первым вошел - первый вышел". Иначе говоря, объекты помещаются в один "конец" очереди, а извлекаются из другого "конца". Таким образом, порядок занесения объектов в контейнер будет совпадать с порядком его извлечения оттуда. Очереди также играют важную роль в параллельном программировании, потому что они обеспечивают безопасную передачу объектов между задачами.
Существуют разные виды очередей, порядок организации в которых основывается на некотором критерии. Интерфейс Queue
является обобщенным, ссылочная переменная типа Queue
объявляется следующим образом
Перечислим основные методы, определенные в интерфейсе Queue
Метод
Описание
E element()
Возвращает элемент из головы очереди. Возращаемый элемент не удаляется. Если очередь пуста, генерируется исключение типа NoSuchElementException
boolean offer(E object)
Пытается ввести заданный object
в очередь. Возвращает логическое значение true, если object
введен, иначе - false
E peek()
Возвращает элемент из головый очереди. Если очередь пуста, возвращает пустое значение null
. Возвращаемый элемент не удаляется из очереди
E poll()
Возвращает элемент из головый очереди и удаляет его. Если очередь пуста, возвращает значение null
E remove()
Удаляет элемент из головы очереди, возвращая его. Генерирует исключение типа NoSuchElementException
, если очередь пуста
Класс LinkedList
, который мы рассматривали в связи с интерфейсом List
, также реализует интерфейс Deque
и поддерживает поведение очередей. Восходящее преобразование LinkedList
в Queue
позволяет использовать объект как очередь.
Класс PriorityQueue
расширяет класс AbstractQueue
и реализует интерфейс Queue
. Он служит для сздания очереди по приоритетам на основании компаратора очереди.
В приоритетной очереди следующим извлекается элемент, обладающий наивысшим приоритетом.
Если при построении очереди компаратор не указан, то применяется компаратор, выбираемый по умолчанию для того типа данных, который сохраняется в очереди. Таким образом, в начале (голове) очереди окажется элемент с наименьшим значением.
С помощью компаратора можно задать другую схему сортировки элементов в очереди. Например, когда в очереди сохраняются элементы, содержащие метку времени, для этой очереди можно задать приоритеты таким образом, чтобы самые давние элементы располагались в начале очереди.
Класс PriorityQueue
имеет одну особенность. Так как очередь с приоритетами реализована с помощью кучи (бинарное дерево с определенными свойствами), то попытка вывести элементы с помощью цикла foreach
, скорее всего, приведет к некорректному выводу элементов.
В интерфейсе Set определяется множество. Он расширяет интерфейс Collection и определяет поведение коллекций, не допускающих дублирования элементов. Таким образом, метод add() возвращает false при попытке ввести в множество дублирующий элемент. В этом интерфейсе не определяется никаких дополнительных методов.
Интерфейс Set объявляется следующим образом
Интерфейс Set не добавляет никакой функциональности по сравнению с интерфейсом Collection, поэтому в Set нет дополнительный функциональности. Вместо этого Set представляет собой разновидность Collection.
Хеш-таблица хранит информацию, используя так называемый механизм хеширования, в котором содержимое ключа используется для определения уникального значения, называемого хеш-кодом. Хеш-код затем применяется в качестве индекса, с которым ассоциируются данные, доступные по этому ключу. Преобразование ключа в хеш-код выполняется автоматически - вы никогда не узнаете самого хеш-кода. Также ваш код не может напрямую индексировать хеш-таблицу.
Если вы хотите использовать HashSet для хранения объектов собственных пользовательских классов, то вы ДОЛЖНЫ переопределить методы hashCode() и equals(), иначае два логически-одинаковых объекта будут считаться разными, так как при добавлении элемента в коллекцию будет вызываться метод hashCode() класса Object (который вернет разный хеш-код для двух логически одинаковых объектов).
Важно отметить, что класс HashSet не гарантирует упорядоченности элементов, поскольку процесс хеширования сам по себе обычно не порождает сортированных наборов. Если вам нужны сортированные наборы, то лучшим выбором может быть класс TreeSet.
TreeSet - коллекция, которая хранит свои элементы в виде упорядоченного дерева. TreeSet использует сбалансированное красно-черное дерево для хранения элементов.
Интерфейс Map описывает коллекцию, состоящую из пар "ключ-значение". У каждого ключа только одно значение, что соответствует математическому понятию однозначной функции или отображения. Такую коллекцию часто называют словарем (dictionary) или ассоциативным массивом (associative array). Несмотря на то, что интерфейс Map входит в список коллекций Java, он не расширяет интерфейс Collection.
Интерфейс Map соотносит уникальные ключи со значениями. Ключ - это объект, который вы используете для последующего извлечения данных. Задавая ключ и значение, вы можете помещать значения в объект Map. После того как это значение сохранено, вы можете получить его по ключу. Интерфейс Map - это обобщенный интерфейс, объявленный так, как показано ниже:
Здесь в качестве K указывается тип ключа, а в качестве V - тип значения.
Рассмотрим основные методы, объявленные в интерфейсе Map
Обращение с отображениями опирается на две основные операции, выполняемые методами get() и put(). Чтобы ввести значение в отображение, следует вызвать put(), указав ключ и значение, а для того чтобы получить значение из отображения - вызвать метод get(), передав ему ключ в качестве аргумента. По этому ключу будет возвращено связанное с ним значение.
Как упоминалось ранее, Map не реализует интерфейс Collection, хотя является частью каркаса коллекций. Тем не менее, можно получить представление отображения в виде коллекции. Для этого можно воспользоваться методом entrySet(), возвращающим множество, содержащее элементы отображения (для получения множества ключей можно использовать метод keySet(), для получения множества значений можно использовать метод values()).
Этот интерфейс позволяет обращаться с отдельными записями в отображении. Напомним, что метод entrySet(), объявляемый в интерфейсе Map, возвращает множество типа Set, содержащее записи из отображения. Каждый элемент этого множества представляет собой объект типа Map.Entry. Интерфейс Map.Entry является обобщенным и объявляется следующим образом:
где K обозначает тип ключей, а V - тип хранимых в отображении значений.
Этот класс расширяет класс AbstractMap и реализует интерфейс Map. В нем используется хеш-таблица для хранения отображений, и благодаря этому обеспечивается постоянное время выполнения методов get() и put(), даже в случае отображения с большим количеством элементов. Класс HashMap является обобщенным и объявляется приведенным ниже образом, где K обозначает тип ключей, а V - тип хранимых в отображении значений.
Класс TreeMap расширяет класс AbstractMap и реализует интерфейс NavigableMap. В нем создается отображение, размещаемое в древовидной структуре. В классе TreeMap предоставляются эффективные средства для хранения пар "ключ-значение" в отсортированном порядке и обеспечивается их быстрое извлечение. Следует заметить, что в отличие от HashMap, древовидное отображение гарантирует, что его элементы будут отсортированы по порядку возрастания ключей. Класс TreeMap является обобщенным и объявляется следующим образом
где K обозначает тип ключей, а V - тип хранимых в отображении значений.
Классы WeakHashMap, LinkedHashMap и интерфейс NavigableMap выносятся на самостоятельное изучение.
В фреймворке коллекций определяется ряд алгоритмов, которые можно применять к коллекциям и отображениям. Эти алгоритмы определены в виде статических методов класса Collections
. По сути, класс Collections предоставляет огромный набор методов для различных операций с коллекциями.
Приведем список наиболее полезных методов класса Collections
Также, в библиотеке java.util присутствует класс Arrays. Класс Arrays предоставляет различные удобные методы для работы с массивами. Эти методы помогает восполнить функциональный пробел между коллекциями и массивами. Например, метод asList() возвращает список, исходя из указанного массива, а метод binarySearch() использует алгоритм двоичного поиска для обнаружения заданного значения.
Приведем список наиболее полезных методов класса Arrays
Ниже приведена таблица, которая позволит вам облегчить процесс выбора нужной коллекции.