6. Розкажіть про List
List - це інтерфейс, що представляє впорядковану структуру об'єктів, яка називається списком. "Фішка" цієї структури - те, що елементи, що містяться в List можна вставити, змінити або видалити за індексом, тобто внутрішньому ідентифікатору List . Інакше кажучи, індекс означає: «кілька елементів від початку списку». У першого елемента List індекс 0, у другого - 1, і таке інше. Таким чином, п'ятий елемент знаходиться на відстані чотирьох елементів з початку списку. Як говорилося вище, у списку важливий порядок додавання елементів. Тому структура даних і називається списком . Перерахуємо унікальні для цієї структури методи, спрямовані на роботу з елементами за індексом:- get - повертає елемент у зазначеній позиції (за значенням індексу),
- remove — видаляє елемент у цій позиції,
- set — замінює елемент у зазначеній позиції на вказаний у методі елемент.
- push – додає переданий елемент на вершину стека;
- peek - повертає елемент, що знаходиться на вершині стека;
- pop також повертає елемент, який знаходиться на вершині стека, але при цьому видаляє його;
- empty - перевіряє, чи порожній стек - true , чи ні - false ;
- search - Виконує пошук заданого елемента в стеку. Якщо елемент знайдено, повертається його порядковий номер щодо верхівки стека. Якщо елемент не знайдено, повертається значення -1.
7. Розкажіть про Map
Як сказано вище, Map - це колекція, що має окрему структуру інтерфейсів та їх реалізацій. Окрема вона тому, що значення не зберігаються по одному, а в парі "ключ - значення". Основні методи Map :- put(K key, V value) - додавання елемента в Map;
- get(Object key) - пошук значення за ключем;
- containsKey(Object key) — перевірка Map наявність даного ключа;
- containsValue(Object value) - перевірка Map на наявність цього значення;
- remove(Object key) - видалення значення за його ключем.
- HashMap – призначена для зберігання значень у довільному порядку, але дозволяє швидко шукати елементи картки. Дозволяє ставити ключ ключовим словом null , але з більше разу, т.к. пари з однаковими ключами записуються поверх одне одного. Головною умовою є унікальність ключів: значення можуть повторюватися (може бути кілька null значень).
- LinkedHashMap — аналог HashMap , який зберігає значення як додавання. Відповідно, як і LinkedList , у нього є header - голова двозв'язкового списку. При ініціалізації вказує на себе.
Також LinkedHashMap має accessOrder , який вказує, яким чином буде здійснюватися доступ до елементів під час використання ітератора. При accessOrder false доступ буде здійснюватись у порядку вставки елементів. При значенні true елементи будуть у порядку останнього доступу (елемент, до якого було останнє звернення, буде поміщено в кінець).
- TreeMap - це Map , яка сортує елементи за значеннями ключа. Аналог TreeSet , але для пар з орієнтуванням на значення ключів. Для завдання правил сортування TreeMap ключі повинні реалізовувати інтерфейс Comparable . В іншому випадку повинен бути Comparator , орієнтований на ключі (той, який задається в конструктор TreeMap ), TreeSet - реалізований з об'єктом TreeMap всередині, в якому, власне, і відбувається вся магія.
Докладніше про сортування в TreeMap за допомогою червоно-чорних дерев можна почитати у статті про особливості TreeMap .
- Hashtable - аналогічний HashMap , але не дозволяє зберігати null ні як ключі, ні як значення. Він ретельно синхронізований з погляду багатопоточності, що у свою чергу означає, що він безпечний з точки зору багатопоточності. Але дана реалізація застаріла та повільна, тому зараз ви і не зустрінете Hashtable у більш-менш нових проектах.
8. ArrayList vs LinkedList. Який краще використовувати?
Це питання, мабуть, найпопулярніше за структурами даних і несе в собі деякі підводні камені. Перш ніж відповідати на нього, дізнаємося докладніше про ці структури даних. ArrayList реалізує інтерфейс List , працює за рахунок внутрішнього масиву, який розширюється при необхідності. Коли внутрішній масив повністю заповнюється, і при цьому потрібно вставити новий елемент створюється новий масив, з розміром (oldSize * 1,5) +1. Після цього всі дані зі старого масиву копіюються в новий новий елемент, старий же буде видалений збирачем сміття . Метод addдодає елемент в останню порожню комірку масиву. Тобто, якщо у нас там вже є 3 елементи, він додасть наступний до 4-го осередку. Давайте пройдемося за продуктивністю базових методів:- get(int index) - взяття елемента в масиві за індексом працює найшвидше за O(1) ;
- add(Object obj) - якщо достатньо місця у внутрішньому масиві для нового елемента, то при звичайній вставці буде витрачено час O(1) , так як додавання йде цілеспрямовано в останню комірку.
Якщо ж потрібно створювати новий масив і копіювати вміст, то час у нас буде прямо пропорційно кількості елементів в масиві O(n) ;
- remove(int index) — при видаленні елемента, наприклад, з середини, ми отримаємо час O(n/2), оскільки потрібно буде пересувати елементи праворуч від нього однією комірку тому. Відповідно, якщо видалення з початку списку, то O(n), з кінця - O(1);
- add(int index, Object obj) - ситуація, схожа з видаленням: при додаванні в середину нам потрібно буде пересунути елементи праворуч на одну комірку вперед, тому час - O(n/2). Зрозуміло, спочатку — O(n), з кінця — O(1);
- set(int index, Object obj) - тут ситуація інша, тому що потрібно тільки знайти потрібний елемент і записати поверх нього, не пересуваючи інші, тому O(1).
- get(int index) — під час пошуку елемента, що у середині списку, починається перебір всіх елементів порядку, доки знайдено потрібний. За логікою пошук повинен займати O(n/2) , але LinkedList має ще й хвіст, тому перебір ведеться одночасно з двох сторін. Відповідно час зменшується до O(n/4) .
Якщо елемент буде недалеко від початку списку або кінця, то і час буде O(1) ;
- add(Object obj) — при додаванні нового елемента, елемент-хвост додасть посилання на наступний елемент, а новий отримає посилання на цей попередній елемент і стане новим хвістом. Відповідно, час буде O(1) ;
- remove(int index) - логіка, схожа з методом get(int index) . Щоб видалити елемент із середини списку, його потрібно спочатку знайти. Це знову ж таки O(n/4) , тоді як саме видалення практично нічого не займає, тому що там тільки змінюються покажчик сусідніх об'єктів (вони починають посилатися один на одного). Якщо елемент на початку або наприкінці, то знову ж таки — O(1) ;
- add(int index, Object obj) і set(int index, Object obj) - у методів тимчасова складність буде ідентична get(int index) , оскільки основний час займає пошук елемента. Тому для середини списку – O(n/4) , для початку – O(1).
Операція | ArrayList | LinkedList |
---|---|---|
Взяття за індексом get(index) | O(1) | У середині O(n/4) |
Додати новий елемент add(obj) |
O(1) Якщо потрібно скопіювати масив — O(n) |
O(1) |
Видалити елемент remove(int index) |
З початку - O(n) Із середини - O(n/2) З кінця - O(1) |
У середині - O(n/4) Наприкінці або на початку - O(n) |
Додати елемент add(int index, Object obj) |
На початок - O(n) У середину - O(n/2) В кінець - O(1) |
У середині - O(n/4) Наприкінці або на початку - O(n) |
Замінити елемент set(index, obj) | O(1) |
У середині - O(n/4) Наприкінці або на початку - O(n) |
- найкращий вибір, якщо найчастіша операція - пошук елемента, перезапис елемента;
- Найгірший вибір, якщо операція - вставка та видалення на початку-середині, тому що будуть проходити операції зсуву елементів праворуч.
- найкращий вибір, якщо нашою частою операцією є вставка та видалення на початку-середині;
- Найгірший вибір, якщо найчастіша операція – пошук.
9. Як зберігаються елементи в HashMap?
Колекція HashMap містить у собі внутрішній масив Node [] table , комірки якого ще називають бакетами (кошиками). Node містять у собі:- key - посилання на ключ,
- value - посилання на значення,
- hash - значення hash,
- next - посилання на наступний Node .
- Ключ перевіряється на рівність null . Якщо він null , то key буде збережений в комірці table[0] , тому що хеш-код для null завжди дорівнює 0.
- Якщо ключ не null , то об'єкт key викликає метод hashcode() , який видасть його хеш-код. Цей хеш-код використовується для визначення осередку масиву, де зберігатиметься об'єкт Node.
- Далі цей hashcode міститься у внутрішній метод hash() , який обчислює hashcode, але вже в межах розміру масиву table[] .
- Далі, залежно від значення hash, Node міститься в конкретну комірку в масиві table [] .
- Якщо ж комірка table[] , яка використовується для збереження поточного елемента Node не порожня, а вже має якийсь елемент, відбувається перебір елементів Node за значенням next , поки не буде досягнутий останній елемент. Тобто той, у якого поле next дорівнює null .
Під час даного перебору порівнюються ключ об'єкта Node , що охороняється , з ключами перебираються:
- якщо буде знайдено відповідність, то перебір закінчиться, і новий Node перезапише Node , в якому знайдено відповідність (перезапишеться тільки його поле value );
- якщо відповідності ключів не знайдені, новий Node стане останнім у цьому списку, а попередній буде мати посилання next на нього.
10. Розкажіть про ітератора
У схемі з відображенням ієрархії Collection вище за інтерфейс Collection був тим, з чого починалася вся ієрархія, але на практиці все не зовсім так. Collection успадковується від інтерфейсу з методом iterator() , який повертає об'єкт, що реалізує інтерфейс Iterator<E> . Інтерфейс Iterator має вигляд:public interface Iterator <E>{
E next();
boolean hasNext();
void remove();
}
next() - викликаючи цей метод, можна буде отримати наступний елемент. hasNext() — дає можливість дізнатися, чи є наступний елемент, і чи не досягнуто кінця колекції. І коли елементи є, то hasNext() поверне значення true . Як правило, hasNext() викликається перед методом next() , тому що при досягненні кінця колекції next() викидатиме виняток NoSuchElementException . remove() — видаляє елемент, отриманий останнім викликом next() . Призначенням Iterator є перебір елементів. Наприклад:
Set<Integer> values = new TreeSet<>();
values.add(5);
values.add(3);
values.add(6);
values.add(8);
values.add(2);
values.add(4);
values.add(1);
values.add(7);
Iterator<Integer> iter = values.iterator();
while(iter.hasNext()){
System.out.println(iter.next());
}
Власне цикл for-each loop і реалізований під капотом за допомогою ітератора. Докладніше про це можна почитати тут . List надає свою версію ітератора, але більш круту і наворочену - ListIterator . Цей інтерфейс розширює Iterator , і він має додаткові методы:
- hasPrevious поверне true, якщо колекції є попередній елемент, інакше — false;
- previous повертає поточний елемент та переходить до попереднього; якщо такого немає, то викидається виняток NoSuchElementException;
- add вставить переданий об'єкт перед елементом, який має бути повернутий наступним викликом next() ;
- set надає поточному елементу посилання на переданий об'єкт;
- NextIndex повертає індекс наступного елемента. Якщо цього немає, то повертається розмір списку;
- previousIndex повертає індекс попереднього елемента. Якщо такого немає, то вертається число -1.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ