Ревью 5. Generics и Коллекции. Array length и Array. Size
Array.length и Array.Size Студент: Святослав Семенов Итоги ревью по модулю: Generics. Collections. Результаты ревью: Пройдено. Теоретические вопросы для повторения: 1. public class Ex1 { public static void main(String[] args) { List integerList = Arrays.asList(1, 2, 3); Gen gen = new Gen(); gen.m(integerList); } static class Gen { void m(Collection collection) { for (T s : collection) { System.out.println(s); } } void m(List list) { for (String s : list) { System.out.println(s); } } } } Что произойдет? Почему отработает 2 метод? Как это исправить? 2. В каких случаях нужно использовать iterator? И почему? 3. Как работает HashMap? Расскажите подробно, как работает метод put? Что происходит при коллизии? https://javarush.ru/groups/posts/2496-podrobnihy-razbor-klassa-hashmap Замечания по практическим задачам: 1.11 - используй даймонд оператор 1.13 - изначально создать массив на 10 эл-в увеличивать массив в 1,5 - 2 раза не нужно создавать массив dest1) Что такое дженерики? Какую проблему они решают. Дженерики – это параметризованные типы. С их помощью можно объявлять классы, интерфейсы и методы, в которых тип данных указан в виде параметра. Используя дженерики, можно создать единственный класс, который будет автоматически работать с разными типами данных. Эта информация доступна только на этапе компиляции и стирается в runtime, и в байт код попадёт только информация о том, что в программе есть некий список List list вместо List list, например.Плюсы : С помощью дженериков в Java мы можем написать код, который будет работать с разными типами данных. Дженерики позволяют передавать тип объекта компилятору в форме <тип>. Таким образом, компилятор может выполнить все необходимые действия по проверке типов во время компиляции, обеспечивая безопасность по приведению типов во время выполнения. Что можно параметризовать? Метод, интерфейс, класс, поле Сырые типы — это типы без указания типа в фигурных скобках ( List list = new ArrayList<>() ), они использовались до появления дженериков. Не указывая их, под капотом используется Object. Не используйте Raw типы! Если аргумент типа не определен, то используйте wildcard > К чему приводит использование raw type? К минусам до generics. Такой код плох тем, что он пройдёт компиляцию, а ошибка обнаружится только во время выполнения. Таким образом, не указывая параметр явно, мы теряем преимущества проверки типов на этапе компиляции. Они нужны только для поддержки обратной совместимости кода, «сырые типы» являются не «типобезопасными». Если вы не указали тип, например, своей коллекции, то туда можно запихнуть другой объект любого типа Стирание типов - во время компиляции компилятор имеет полную информацию о типе, но эта информация обычно намеренно отбрасывается при создании байтового кода в процессе, известном как стирание типов . Это делается таким образом из-за проблем совместимости: целью разработчиков языка было обеспечение полной совместимости исходного кода и полной совместимости байтового кода между версиями платформы. Если бы он был реализован по-другому, вам пришлось бы перекомпилировать устаревшие приложения при переходе на более новые версии платформы. Компилятор удаляет из байткода класс-файла информацию о типах-дженериках. Этот процесс и называется стирание типов (type erasure). Он появился в Java 5 вместе с самими дженериками. Такое решение позволило сохранить обратную совместимость. Если ты попытаешься положить объект не того типа в свой List, компилятор выдаст ошибку. Этого как раз и добивались создатели языка, создавая дженерики — проверки на этапе компиляции. Но когда весь написанный тобой Java-код превратится в байт-код, в нем не будет информации о типах-параметрах. Стирание состоит из трех действий: 🔘 Если параметры ограничены (bounded), вместо типа-параметра в местах использования подставляется верхняя граница, иначе Object; 🔘 В местах присвоения значения типа-параметра в переменную обычного типа добавляется каст к этому типу; 🔘 Генерируются bridge-методы. Какой механизм обеспечивает обратную совместимость сырых типов и дженериков? - Стирание типов Если поле типизировано дженериком как в байт коде будет представлен этот тип? -.Вместо дженерика будет Object<> - даймонд оператор - синтаксический сахар; когда компилятор сам выводит тип правой части исходя из контекста, но контекстом не всегда является левая часть выражения(может быть возвращаемое значение метода, т.е. то, что должно вернуться из левой части). Дает компилятору 100% гарантию, что будет тот же тип. 2) Wildcard https://ru.stackoverflow.com/questions/1128951/java-generics-%D0%92-%D1%87%D0%B5%D0%BC-%D1%80%D0%B0%D0%B7%D0%BD%D0%B8%D1%86%D0%B0-%D0%BC%D0%B5%D0%B6%D0%B4%D1%83-wildcard-%D0%B8-parameterized-typest Запись вида "? extends ..." или "? super ..." — называется wildcard или символом подстановки , с верхней границей (extends) или с нижней границей (super). Ковариантность — перенос наследования исходных типов на производные от них типы в прямом порядке. Контравариантность — перенос наследования исходных типов на производные от них типы в обратном порядке. Инвариантность — ситуация, когда наследование исходных типов не переносится на производные. Generics инвариантны Wildcards может использоваться в качестве: параметра метода; поля; локальной переменной; иногда возвращаемым типом (однако не очень хорошо так поступать, лучше чтобы тип возвращаемого параметра был известен) Wildcards не может использоваться в качестве: аргумента при вызове Generic метода; создании экземпляра Generic класса. Существует 2 сценария, при которых Unbound Wildcards могут быть полезны: Если Вы пишите метод, для реализации которого достаточно функциональности класса Object. Когда код в методе Generic класса не зависит от параметра типа. Маске(wildcard) можно задать ограничения: -“? extends T” (для получения в методе) - объект, который наследуется от Т, либо сам Т – ковариантность. Если контейнер объявлен ? extends T, то можно только читать значения. В список нельзя ничего добавить, кроме null. -“? super T” (для отдачи в методе) - любой объект подтипа Т, включая Т – контравариантность. Нельзя прочитать элемент из контейнера с wildcard ? super, кроме объекта класса Object. При использовании ? мы сообщаем компилятору, чтобы он игнорировал информацию о типе, т.е. > - неограниченный символ подстановки. > означает то же что и extends Object>, т.е. принимает всё. Это можно обойти, создав обобщенный метод, объявленный с переменной типа T.2.1) Что можно поставить в дженерики, что нельзя? Что можно типизировать? Что нельзя параметризировать? Можно типизировать только ссылочные типы. Нельзя типизировать примитивные типы. Нельзя создавать экземпляры параметров типа в параметризованном классе. public static void append(List list) { E elem = new E(); // нельзя Невозможно объявить статические поля, типы которых являются параметрами типа. Нельзя создать массив параметризованных типов. Невозможно использовать приведение или instanceof с параметризованными типами. public static void rtti(List list) { if (list instanceof ArrayList) { Нельзя перегружать метод так, чтобы формальные параметры типа стирались в один и тот же сырой тип public void print(Set strSet) { } public void print(Set intSet) { } Почему последняя строчка не скомпилируется? List arrayLists = new ArrayList(); ArrayList<List > arrayList = new ArrayList<ArrayList >(); ? Дженерики Инвариантны (отсутствие наследования между производными типами) В чем отличие в записях в параметре метода: (Collection> collection) и ((Collection extends T> collection)? https://ru.stackoverflow.com/questions/1128951/java-generics-%D0%92-%D1%87%D0%B5%D0%BC-%D1%80%D0%B0%D0%B7%D0%BD%D0%B8%D1%86%D0%B0-%D0%BC%D0%B5%D0%B6%D0%B4%D1%83-wildcard-%D0%B8-parameterized-typest Термин Расшифровка Дженерик-типы (generic types) Дженерик-класс или дженерик-интерфейс с одним или несколькими параметрами в заголовке Параметризованный тип (parameterized types) Вызов дженерик-типа. Для дженерик-типа List параметризованным типом будет, например, List Параметр типа (type parameter) Используются при объявлении дженерик-типов. Для Box T — это параметр типа Аргумент типа (type argument) Тип объекта, который может использоваться вместо параметра типа. Например, для Box Paper — это аргумент типа Wildcard Обозначается символом «?» — неизвестный тип Ограниченный wildcard (bounded wildcard) Wildcard, который ограничен сверху — extends Garbage> или снизу — super Garbage> Сырой тип (raw type) Имя дженерик-типа без аргументов типа. Для List сырой тип — это List
3) Принцип PECS The Get and Put Principle или PECS (Producer Extends Consumer Super) Если мы объявили wildcard с extends, то это producer. Он только «продюсирует», предоставляет элемент из контейнера, а сам ничего не принимает. Исключением является возможность записать null. Если же мы объявили wildcard с super — то это consumer. Он только принимает, а предоставить ничего не может. Исключением является возможность прочитать Object. Raw Types (сырые типы) – Это имя универсального класса или интерфейса без каких-либо аргументов типа. их нужно избегать. Пример сырого типа List <> list = new ArrayList<>; Пример нормального типа List list = new ArrayList<>; Стирание типов - во время компиляции компилятор имеет полную информацию о типе, но эта информация обычно намеренно отбрасывается при создании байтового кода в процессе, известном как стирание типов . Это делается таким образом из-за проблем совместимости: целью разработчиков языка было обеспечение полной совместимости исходного кода и полной совместимости байтового кода между версиями платформы. Если бы он был реализован по-другому, вам пришлось бы перекомпилировать устаревшие приложения при переходе на более новые версии платформы. 4) Параметризация статических методов. По аналогии с универсальными классами (дженерик-классами), можно создавать универсальные методы (дженерик-методы), то есть методы, которые принимают общие типы параметров. Универсальные методы не надо путать с методами в дженерик-классе. Универсальные методы удобны, когда одна и та же функциональность должна применяться к различным типам. public static void fill(List list, T val) https://ru.stackoverflow.com/questions/415002/%D0%92%D0%BE%D0%BF%D1%80%D0%BE%D1%81-%D0%BF%D0%BE-%D0%B4%D0%B6%D0%B5%D0%BD%D0%B5%D1%80%D0%B8%D0%BA%D0%B0%D0%BC-%D0%BC%D0%B5%D1%82%D0%BE%D0%B4%D0%B0-%D0%B8-%D0%BE%D0%B3%D1%80%D0%B0%D0%BD%D0%B8%D1%87%D0%B5%D0%BD%D0%B8%D1%8E-%D0%BF%D0%B5%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D1%85-%D1%82%D0%B8%D0%BF%D0%BE%D0%B2 5) Что такое коллекция. Иерархия коллекций. Коллекция – это объект, который содержит набор объектов одного типа. Каждый из этих объектов в коллекции называется элементом. 6) Внутреннее устройство коллекций. Коллекциями/контейнерами в Java принято называть классы, основная цель которых – хранить набор других элементов. Отличие коллекции от массива? Массивы - это простые конструкции фиксированного размера, и поэтому они могут хранить только заданное количество элементов. Массивы встроены в ядро языка Java, и используемый при работе с ними синтаксис Java очень прост и понятен. Например, чтобы получить элемент массива с номером n, вам нужно вызвать функцию array[n]. Коллекции - это более сложный, но в то же время более гибкий тип данных. Прежде всего, размер коллекции можно изменять: вы можете добавлять в коллекцию любое количество элементов. Коллекции автоматически обрабатывают удаление элемента из любой позиции. https://www.youtube.com/watch?v=-S_huEuNJiU 6.1) Методы интерфейса Collection? https://docs.oracle.com/javase/8/docs/api/java/util/Collection.html http://www.smachek.com/post/java_collections/ https://jsehelper.blogspot.com/2016/01/java-collections-framework-1.html Iterator iterator() Возвращает итератор для обращения к элементам набора данных. int size() Возвращает количество элементов в наборе данных. boolean isEmpty() Возвращает значение true, если набор пустой. boolean contains (Object obj) Возвращает true, если набор содержит объект, эквивалентный obj. boolean containsAll (Collection> other) Возвращает true, если текущий набор содержит все объекты набора данных other. boolean add (Object element) Добавляет элемент в набор. Возвращает true, если в результате вызова метода набор данных изменился. boolean addAll (Collection extends E> other) Добавляет все элементы в набор. Возвращает true, если в результате вызова метода набор данных изменился. boolean remove (Object obj) Удаляет объект obj. Возвращает true, если в результате вызова метода набор данных изменился. boolean removeAll (Collection> other) Удаляет из текущего набора данных все элементы, содержащиеся в наборе other. Возвращает true, если в результате вызова метода набор данных изменился. void clear () Удаляет из текущего набора данных все элементы. boolean retainAll (Collection> other) Удаляет из набора данных элементы, не совпадающие с теми, которые содержатся в наборе other. Возвращает true, если в результате вызова метода набор данных изменился. Object[] toArray () Возвращает массив с объектами из набора данных.
6.2) ArrayList Добавление элементов Внутри метода add(value) происходят следующие вещи: 1) проверяется, достаточно ли места в массиве для вставки нового элемента; ensureCapacity(size + 1); 2) добавляется элемент в конец (согласно значению size) массива. elementData[size++] = element; Если места в массиве не достаточно , новая емкость рассчитывается по формуле (oldCapacity * 3) / 2 + 1. Второй момент это копирование элементов. Оно осуществляется с помощью native метода System.arraycopy(), который написан не на Java.// newCapacity - новое значение емкости elementData = (E[])new Object[newCapacity]; // oldData - временное хранилище текущего массива с данными System.arraycopy(oldData, 0, elementData, 0, size); Добавление в «середину» списка list.add(5, "100"); Добавление элемента на позицию с определенным индексом происходит в три этапа: 1) проверяется, достаточно ли места в массиве для вставки нового элемента; ensureCapacity(size+1); 2) подготавливается место для нового элемента с помощью System.arraycopy(); System.arraycopy(elementData, index, elementData, index + 1, size - index); 3) перезаписывается значение у элемента с указанным индексом. elementData[index] = element; size++; С удалением элемента по индексу всё достаточно просто list.remove(5); Сначала определяется какое количество элементов надо скопировать int numMoved = size - index - 1; затем копируем элементы используя System.arraycopy() System.arraycopy(elementData, index + 1, elementData, index, numMoved); уменьшаем размер массива и забываем про последний элемент elementData[--size] = null; // Let gc do its work при удалении элементов текущая величина capacity не уменьшается, что может привести к своеобразным утечкам памяти. Поэтому не стоит пренебрегать методом trimToSize(). Итоги — Быстрый доступ к элементам по индексу за время O(1); — Доступ к элементам по значению за линейное время O(n); — Медленный, когда вставляются и удаляются элементы из «середины» списка; — Позволяет хранить любые значения, в том числе и null; — Не синхронизирован. минимум накладных расходов при хранении. Методы void add(int index, E obj): добавляет в список по индексу index объект obj boolean addAll(int index, Collection extends E> col): добавляет в список по индексу index все элементы коллекции col. Если в результате добавления список был изменен, то возвращается true, иначе возвращается false E get(int index): возвращает объект из списка по индексу index int indexOf(Object obj): возвращает индекс первого вхождения объекта obj в список. Если объект не найден, то возвращается -1 int lastIndexOf(Object obj): возвращает индекс последнего вхождения объекта obj в список. Если объект не найден, то возвращается -1 ListIterator listIterator (): возвращает объект ListIterator для обхода элементов списка static List of(элементы): создает из набора элементов объект List E remove(int index): удаляет объект из списка по индексу index, возвращая при этом удаленный объект E set(int index, E obj): присваивает значение объекта obj элементу, который находится по индексу index void sort(Comparator super E> comp): сортирует список с помощью компаратора comp LinkedList Класс LinkedList содержит три поля:transient int size = 0 ; transient Node first; transient Node last; Для установки ссылок на предыдущий и следующий элементы LinkedList использует объекты своего вложенного класса Node :private static class Node < E > { E item; Node next; Node prev; } Node(Node prev, E element, Node next) { this .item = element; this .next = next; this .prev = prev; } При каждом добавлении объекта в список создается один новый узел, а также изменяются значения полей связанного списка (size , first , last ). вставка в середину Удаление не синхронизирована; позволяет хранить любые объекты, в том числе null и повторяющиеся; за константное время O(1) выполняются операции вставки и удаления первого и последнего элемента, операции вставки и удаления элемента из середины списка (не учитывая время поиска позиции элемента, который осуществляется за линейное время); за линейное время O(n) выполняются операции поиска элемента по индексу и по значению. Методы: addFirst() / offerFirst() : добавляет элемент в начало списка addLast() / offerLast() : добавляет элемент в конец списка removeFirst() / pollFirst() : удаляет первый элемент из начала списка removeLast() / pollLast() : удаляет последний элемент из конца списка getFirst() / peekFirst() : получает первый элемент getLast() / peekLast() : получает последний элемент Map Уникальные ключи HashMap не имеет гарантий упорядочивания Элементы могут меняться со временем В HashMap хеш-таблица реализована на основе массива (а если точнее — динамического, так как таблица может расширяться) односвязных списков. По сути, мы получаем хеш-код ключа в результате работы метода hashCode(), который затем модифицируется (как рассмотрим далее), а внутри с помощью дополнительного метода полученные значения распределяются по нужным ячейкам. Элементы массива (ячейки) еще называются корзинами «buckets», которые используются для хранения отдельно взятых узлов. Каждый из бакетов представляет из себя коллекцию (список или дерево). Узел представляет собой объект вложенного класса Node (или TreeNode при древовидной структуре). По сути, внутри ячейки массива лежит LinkedList, только список односвязный, либо красное-черное дерево, которое лежит в основе реализации другого класса — TreeMap. Node — это вложенный класс внутри HashMap, который имеет следующие поля: Теперь рассмотрим поля самого класса HashMap: transient Node < K, V> [] table – сама хеш-таблица, реализованная на основе массива, для хранения пар «ключ-значение» в виде узлов. Здесь хранятся наши Node; transient int size — количество пар «ключ-значение»; int threshold — предельное количество элементов, при достижении которого размер хэш-таблицы увеличивается вдвое. Рассчитывается по формуле (capacity * loadFactor); final float loadFactor — этот параметр отвечает за то, при какой степени загруженности текущей хеш-таблицы необходимо создавать новую хеш-таблицу, т.е. как только хеш-таблица заполнилась на 75%, будет создана новая хеш-таблица с перемещением в неё текущих элементов (затратная операция, так как требуется перехеширование всех элементов); transient Set< Map.Entry< K,V>> entrySet — содержит кешированный entrySet(), с помощью которого мы можем перебирать HashMap. Добавление объектов Добавление пары "ключ-значение" осуществляется с помощью метода put() Вычисляется хеш-значение ключа введенного объекта. Вычисляем индекс бакета (ячейки массива), в который будет добавлен наш элемент: i = (n - 1) & hash Создается объект Node. Теперь, зная индекс в массиве, мы получаем список (цепочку) элементов, привязанных к этой ячейке. Если в бакете пусто, тогда просто размещаем в нем элемент. Иначе хэш и ключ нового элемента поочередно сравниваются с хешами и ключами элементов из списка и, при совпадении этих параметров, значение элемента перезаписывается. Если совпадений не найдено, элемент добавляется в конец списка. После добавления будет произведена проверка не превышает ли текущее количество элементов параметр threshold. Если превышает, тогда будет вызван метод resize() для увеличения размера хеш-таблицы вдвое. Относительно операции добавления Получение осуществляется довольно просто. Алгоритм (когда в бакете связный список) можно записать так: Вычислить хэш код ключа. Вычислить индекс бакета. Перейти по индексу и сравнить ключ первого элемента с имеющимся значением. Если они равны – вернуть значение, иначе выполнить проверку для следующего элемента, если он существует. Если следующий объект Node равен null, возвращаем null. Если следующий объект Node не равен null, переходим к нему и повторяем первые три шага до тех пор, пока элемент не будет найден или следующий объект Node не будет равен null. удаление по ключу. Алгоритм очень похож: заходим в нужный бакет (опять же он предварительно вычисляется); если в бакете только один объект (проверяем у него указатель null) сравниваем хеши, ссылки и equals (если вдруг хеши не равны). Нашлось совпадение? Отлично, это наш ключ – удаляем его (=null) и возвращаем значение этого ключа. если в бакете больше одного элемента, проверяем каждый элемент в цикле до тех пор, пока не найдем элемент или достигнем конца списка. — Добавление элемента выполняется за время O(1), потому как новые элементы вставляются в начало цепочки; — Операции получения и удаления элемента могут выполняться за время O(1), если хэш-функция равномерно распределяет элементы и отсутствуют коллизии. Среднее же время работы будет Θ(1 + α), где α — коэффициент загрузки. В самом худшем случае, время выполнения может составить Θ(n) (все элементы в одной цепочке); — Ключи и значения могут быть любых типов, в том числе и null. Для хранения примитивных типов используются соответствующие классы-оберки; — Не синхронизирован. Методы: void clear(): очищает коллекцию boolean containsKey(Object k): возвращает true, если коллекция содержит ключ k boolean containsValue(Object v): возвращает true, если коллекция содержит значение v Set> entrySet(): возвращает набор элементов коллекции. Все элементы представляют объект Map.Entry boolean equals(Object obj): возвращает true, если коллекция идентична коллекции, передаваемой через параметр obj boolean isEmpty: возвращает true, если коллекция пуста V get(Object k): возвращает значение объекта, ключ которого равен k. Если такого элемента не окажется, то возвращается значение null V getOrDefault(Object k, V defaultValue): возвращает значение объекта, ключ которого равен k. Если такого элемента не окажется, то возвращается значение defaultVlue V put(K k, V v): помещает в коллекцию новый объект с ключом k и значением v. Если в коллекции уже есть объект с подобным ключом, то он перезаписывается. После добавления возвращает предыдущее значение для ключа k, если он уже был в коллекции. Если же ключа еще не было в коллекции, то возвращается значение null V putIfAbsent(K k, V v): помещает в коллекцию новый объект с ключом k и значением v, если в коллекции еще нет элемента с подобным ключом. Set keySet(): возвращает набор всех ключей отображения Collection values(): возвращает набор всех значений отображения void putAll(Map extends K, ? extends V> map): добавляет в коллекцию все объекты из отображения map V remove(Object k): удаляет объект с ключом k int size(): возвращает количество элементов коллекции Обобщенный интерфейс Map.Entry представляет объект с ключом типа K и значением типа V и определяет следующие методы: boolean equals(Object obj): возвращает true, если объект obj, представляющий интерфейс Map.Entry, идентичен текущему K getKey(): возвращает ключ объекта отображения V getValue(): возвращает значение объекта отображения V setValue(V v): устанавливает для текущего объекта значение v int hashCode(): возвращает хеш-код данного объекта Set HashSet хранит элементы в произвольном порядке, но зато быстро ищет. Подходит, если порядок Вам не важен, но важна скорость. Более того, для оптимизации поиска, HashSet будет хранить элементы так, как ему удобно. LinkedHashSet будет хранить элементы в порядке добавления, но зато работает медленнее. TreeSet хранит элементы отсортированными HashSet работает ровно так же, как и HashMap с поправкой на то, что в качестве значения используется некоторая заглушка – один и тот же объект класса Object, к которому мы никогда не обратимся. Отображение (map) представляет собой объект, сохраняющий связи между ключами и значениями в виде пар «ключ – значение». По заданному ключу можно найти его значение. Ключи и значения являются объектами. Ключи могут быть однозначными, а значения дублированными. По сути, HashSet не имеет собственной реализации и опирается на реализацию HashMap. Так вот в HashSet мы просто отсекаем «значения» и пользуемся тем, что HashMap обеспечивает уникальность ключей (за счет правильной реализации методов equals() и hashCode()), которые и являются, по сути, значениями в нашем множестве. Допускаются null элементы (но только один, так как не допускаются дубликаты).Класс HashSet не добавляет новых методов Как работает HashSet? Почему в HashSet вместо value не null а new Object? Класс HashSet реализует интерфейс Set, основан на хэш-таблице, а также поддерживается с помощью экземпляра HashMap. В HashSet элементы не упорядочены, нет никаких гарантий, что элементы будут в том же порядке спустя какое-то время. Операции добавления , удаления и поиска будут выполняться за константное время при условии, что хэш-функция правильно распределяет элементы по «корзинам», о чем будет рассказано далее. Т.к. класс реализует интерфейс Set, он может хранить только уникальные значения; Может хранить NULL – значения; Порядок добавления элементов вычисляется с помощью хэш-кода; HashSet также реализует интерфейсы Serializable и Cloneable. Хеш используется для быстрого поиска нужного объекта. Согласитесь, что быстрее сравнить два целочисленных значения (особенно, если они еще и как-то отсортированы), чем сравнивать все поля объекта. Но т.к. разные объекты могут дать один и тот же хеш, то при нахождении объекта с нужным хешем затем еще и проводится проверка на равенство этих объектов. HashSet хранит new Object(), потому что в контракте указывается метод remove() который возвращает объект, если указанный объект существовал и был удален. Для этого он использует обёртку, HashMap#remove() который возвращает удаленное значение. Если бы вы удалили null вместо объекта, то вызов HashMap#remove() вернул бы бы null, что было бы неотличимо от результата попытки удалить несуществующий объект, и контракт HashSet.remove() не мог бы быть выполнен. Как работает HashMap? Что происходит при коллизии? Как работает метод get в HashMap? Метод put в HashMap? Почему Map не входит в Collection? Может ли null быть ключём в HashMap? Как расширяется HashMap? Как обойти HashMap через for-each? http://opensourceforgeeks.blogspot.com/2015/02/understanding-how-hashmap-and-hashset.html https://javarush.ru/groups/posts/2496-podrobnihy-razbor-klassa-hashmap https://www.youtube.com/watch?v=kk_9md24Ttk Что такое бинарное дерево? Условия перестраивания HashMap в красно-чёрное дерево? Двоичное де́рево — структура данных, в которой каждый узел (родительский) имеет не более двух потомков (правый и левый наследник). static final int TREEIFY_THRESHOLD = 8 — это «порог» количества элементов в одной корзине, при достижении которого внутренний связный список будет преобразован в древовидную структуру (красно-черное дерево). static final int UNTREEIFY_THRESHOLD = 6 — если количество элементов в одной корзине уменьшится до 6, то произойдет обратный переход от дерева к связному списку; static final int MIN_TREEIFY_CAPACITY = 64 — минимальная емкость (количество корзин) хеш-таблицы, при которой возможен переход к древовидной структуре. Т.е. если в хеш-таблице по крайней мере 64 бакета и в одном бакете 8 или более элементов, то произойдет переход к древовидной структуре. После каждой итерации (добавления нового элемента) в цикле for увеличивается счетчик, который отвечает за количество элементов в бакете, пока их количество не станет равно или больше 7 TREEIFY_THRESHOLD – 1. В таком случае произойдет вызов метода treeifyBin()treeify()для непосредственного построения древовидной структуры. Однако, если количество бакетов в текущей хеш-таблице меньше 64 (MIN_TREEIFY_CAPACITY ) Вместо перехода к древовидной структуре будет вызван метод resize() для увеличения размера хеш-таблицы с перераспределением элементов. Метод resize() перебирает все элементы текущего хранилища, пересчитывает их индексы (с учетом нового размера) и перераспределяет элементы по новому массиву.ListIterator ListIterator - это подинтерфейс of Iterator. Это один из способов обхода (traverse) элементов List . В отличие от Iterator , ListIterator поддерживает перемещение элементов как в прямом, так и в обратном направлениях. ListIterator также поддерживает удаление, обновление или вставку элемента во время итерации. № Метод и описание 1 void add(Object obj) Вставляет obj в список перед элементом, который будет возвращен следующим вызовом next(). 2 boolean hasNext( ) Возвращает true, если есть следующий элемент. В противном случае возвращает false. 3 boolean hasPrevious( ) Возвращает true, если есть предыдущий элемент. В противном случае возвращает false. 4 Object next( ) Возвращает следующий элемент. A NoSuchElementException вызывается, если не существует следующего элемента. 5 int nextIndex( ) Возвращает индекс следующего элемента. Если нет следующего элемента, возвращается размер списка. 6 Object previous( ) Возвращает предыдущий элемент. A NoSuchElementException вызывается, если не существует следующего элемента. 7 int previousIndex( ) Возвращает индекс предыдущего элемента. Если нет предыдущего элемента, возвращается -1. 8 void remove( ) Удаляет текущий элемент из списка. Вызывается IllegalStateException, если вызывается функция remove() вызвана перед next() или previous(). 9 void set(Object obj) Назначает obj текущему элементу. Это последний элемент, возвращаемый вызовом либо next(), либо previous().
Iterator ListIterator Используется для перебора элементов Collection . Используется для перебора элементов List . Поддерживает перемещение элементов в прямом направлении. Поддерживает перемещение элементов в прямом и обратном направлениях. Может поддерживать удаление элемента во время итерации (зависит от типа Collection ) Может поддерживать удаление, обновление или вставку элемента во время итерации (зависит от типа List ). Не гарантирует порядок итераций элементов. Гарантирует порядок итераций элементов.
Как перебрать элементы LinkedList в обратном порядке, не используя медленный get(index)? Для этого в LinkedList есть обратный итератор, который можно получить вызвать метод descendingIterator(). Можно ли с помощью цикла for each изменить поле объектов, которые находятся в массиве? И можно ли изменить сам объект? Элемент из КОЛЛЕКЦИИ в цикле for-each удалить нельзя, т.к. нельзя проводить одновременно итерацию (перебор) коллекции и изменение ее элементов. Итератор этого учесть не может. Значения элементов массива менять можно? Отличие ArrayList от LinkedList? Когда лучше использовать ArrayList, а когда LinkedList? ArrayList - это список на основе массива. LinkedList - связанный список на основе элементов и связи между ними. В качестве LinkedList лучше всего подходит представление вагонов поезда сцепленных последовательно. ArrayList следует использовать, когда в приоритете доступ по индексу, так как эти операции выполняются за константное время. Добавление в конец списка в среднем тоже выполняется за константное время. Кроме того в ArrayList нет дополнительных расходов на хранение связки между элементами. Минусы в скорости вставки/удаления элементов находящихся не в конце списка , так как при этой операции все элементы правее добавляемого/удаляемого сдвигаются. LinkedList удобен когда важнее быстродействие операций вставки/удаления, которые в LinkedList выполняются за константное время. Операции доступа по индексу производятся перебором с начала или конца (смотря что ближе) до нужного элемента. Дополнительные затраты на хранение связки между элементами. Одним словом - если часто вставляете/удаляете - выбирайте в пользу LinkedList, в противном случае ArrayListВ чём разница между Queue и Deque и Stack? Queue (одностороняя очередь) - когда элементы можно получить в том порядке в котором добавляли. FIFO Dequeue (двусторонняя очередь) - можно вставлять/получать элементы из начала и конца. Stack - LIFOРасскажи отличие List от Set? 3 реализации Set -> Упорядоченность в HashSet, LinkedHashSet, TreeSet (Какая упорядоченность в какой и почему)? List хранит объекты в порядке вставки, элемент можно получить по индексу. Set не может хранить одинаковых элементов. К семейству интерфейса Set относятся HashSet, TreeSet и LinkedHashSet. В множествах Set разные реализации используют разный порядок хранения элементов. В HashSet порядок элементов оптимизирован для быстрого поиска. В контейнере TreeSet объекты хранятся отсортированными по возрастанию. LinkedHashSet хранит элементы в порядке добавления. Null в TreeSet? В пустой TreeMap можно положить единственный ключ-null, все остальные операции (кроме size() и clear(), кстати) после этого не работают. В непустой TreeMap положить null-ключ нельзя из-за обязательного вызова compareTo(). Когда мы добавляем ключ null в пустое дерево — он попадает в root. А root, товарищи, он и в Африке root, он равнее всех прочих, для него не происходит вызова compareTo() и null спокойно занимает своё место в корне дерева. А если дерево не пустое — начинаются попытки сравнения с имеющимся содержимым, и мы получаем «законный» NPE. Как работает метод contains в ArrayList, LinkedList, HashSet? public boolean contains(Object o) { return indexOf(o) >= 0; } Метод contains в Arraylist использует метод indexOf (), который использует в свою очередь метод indexOfRange(). Там совершается обход элементов в цикле и если элемент не null, то вызывается стандартный метод equals (сравнение ссылок). Тоже самое для LinkedList. В методе contains HashSet используются «корзины» и поиск объекта происходит сначала по hashcode, а только потом по equals. В реализации: Метод contains вызывает (косвенно) getEntry из HashMap, где ключ - это Object, для которого вы хотите узнать, находится ли он в HashSet. Как вы можете видеть ниже, два объекта могут быть сохранены в HashMap/HashSet, даже если их ключ сопоставляется с тем же значением с помощью хэш-функции. Метод выполняет итерацию по всем ключам, которые имеют одно и то же значение хэша, и выполняет equals на каждом из них, чтобы найти соответствующий ключ. final Entry getEntry(Object key) { int hash = (key == null) ? 0 : hash(key.hashCode()); for (Entry e = table[indexFor(hash, table.length)]; e != null; e = e.next) { Object k; if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } return null; }В чём разница между Iterable и Iterator? An Iterable - Это простое представление ряда элементов,которые могут быть повторены. Он не имеет никакого состояния итерации, такого как "текущий элемент". Вместо этого, он имеет один метод, который производит Iterator. An Iterator - это объект с состоянием итерации. Это позволяет проверить, если он имеет больше элементов с помощью hasNext() и перейти к следующему элементу (если таковые имеются) с помощью next(). как правило, an Iterable должен быть в состоянии произвести любое количество действующих Iterators. Зачем в итераторе метод remove?Скорость вставки элемента в начало середину и конец у ArrayList vs LinkedList? Обе операции при вставке в середину - О (n). ArrayList проигрывает LinkedList-у только при добавлении / удалении элементов из начала списка. (а также потенциально проигрывает при добавлении / удалении с конца списка, если список на миллиарды элементов) На чём основаны основные реализации коллекций. Например, ArrayList построен на Object[ ] LinkedList - на списке со ссылками на предыдущий и следующий элемент HashMap - на массиве LinkedList.Как работает HashMap? Расскажите подробно, как работает метод put? Что происходит при коллизии? Внутри Hashmap имеет вложенный класс Node в котором хранятся пары ключ значения: static class Node implements Map.Entry { final int hash; final K key; V value; Node next; Node(int hash, K key, V value, Node next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } public final K getKey() { return key; } public final V getValue() { return value; } public final String toString() { return key + "=" + value; } Далее внутри HashMap есть массив объектов типа Node transient Node[] table; Это и есть массив bucket. Если возникает коллизия (т.е. hashcode вычисленный) объектов равны, то создается связанный список node в каждой ячейке массива node, где объекты ссылаются друг на друга Как работает HashMap. HashMap состоит из «корзин» (bucket`ов). «Корзины» — это элементы массива, которые хранят ссылки на списки элементов. При добавлении новой пары ключ-значение вычисляет хеш-код ключа, на основании которого вычисляется номер bucket’a(номер ячейки массива), в которую попадёт новый элемент. Если bucket пуст, то в него сохраняется ссылка на вновь добавляемый элемент, если же там уже есть элемент, то происходит последовательный переход по ссылкам между элементами в цепочке, в поисках последнего элемента, от которого и ставится ссылка на вновь добавленный элемент. Если в списке был найден элемент с таким же ключом, то он заменяется. Добавление, поиск и удаление элементов выполняется за константное время. Вроде все здорово, с одной оговоркой, хеш-функций должна равномерно распределять элементы по корзинам, в этом случае временная сложность для этих 3 операций будет не ниже lg N, а в среднем случае как раз константное время. метод put. 1. Вычисляется хеш-значение ключа введенного объекта. Хэш ключа вычисляет метод static final int hash(Object key), который уже обращается к известному методу hashCode() ключа. Для этого используется либо переопределенный метод hashCode(), либо его реализация по умолчанию. Почему бы просто не вычислить с помощью hashCode()? Это сделано из-за того , что hashCode() можно реализовать так, что только нижние биты int'a будут заполнены. Например, для Integer, Float – если мы в HashMap кладем маленькие значения, то у них и биты хеш-кодов будут заполнены только нижние. В таком случае ключи в Hash Map будут иметь тенденцию скапливаться в нижних ячейках, а верхние будут оставаться пустыми, что не очень эффективно. На то, в какой бакет попадёт новая запись, влияют только младшие биты хеша. Поэтому и придумали различными манипуляциями подмешивать старшие биты хеша в младшие, чтобы улучшить распределение по бакетам (чтобы старшие биты родного хеша объекта начали вносить коррективы в то, в какой бакет попадёт объект) и, как следствие, производительность. Потому и придумана дополнительная функция hash внутри HashMap. 1. Вычисляем индекс бакета (ячейки массива), в который будет добавлен наш элемент: i = (n - 1) & hash где n – длина массива. & - побитовое умножение (количество бакетов (16-1) приводится в битовый эквивалент, над ними производится побитовое умножение. 2. Создается объект Node (включает хеш, ключ, значение, ссылка на следующий элемент, (либо null, если его нет)). 3. Теперь, зная индекс в массиве, мы получаем список (цепочку) элементов, привязанных к этой ячейке. Если в бакете пусто, тогда просто размещаем в нем элемент. Иначе хэш и ключ нового элемента поочередно сравниваются с хешами и ключами элементов из списка и, при совпадении этих параметров, значение элемента перезаписывается. Если совпадений не найдено, элемент добавляется в конец списка.