Kolekcje
84. Opowiedz nam o iteratorach i sposobie ich użycia
Kolekcje to ulubiony temat każdej rozmowy kwalifikacyjnej z programistą Java. Odpowiadając na pytania dotyczące hierarchii kolekcji, kandydaci często mówią, że zaczyna się od interfejsu Kolekcji . Ale tak nie jest. Poziom wyżej istnieje inny interfejs: Iterable . Interfejs ten składa się z metody iterator() , która umożliwia dostęp do obiektu Iterator dla bieżącej kolekcji. A czym właściwie jest ten obiekt Iteratora ? Obiekt Iterator umożliwia poruszanie się po kolekcji i iterację po jej elementach, a użytkownik nie musi znać konkretnych szczegółów implementacji kolekcji. Inaczej mówiąc, jest to swego rodzaju wskaźnik do elementów kolekcji, jakby zaglądał do środka, na któryś z nich. Iterator posiada metody takie jak:-
hasNext() — zwraca wartość true , jeśli iteracja zawiera inny element (ta metoda informuje, kiedy doszedłeś do końca kolekcji);
-
next() — zwraca następny element w iteracji. Jeśli takowego nie ma, zgłaszany jest wyjątek NoSuchElementException . Oznacza to, że przed użyciem tej metody najlepiej jest użyć metody hasNext() , aby upewnić się, że istnieje następny element;
-
Remove() — usuwa z kolekcji ostatni otrzymany element metodą next() . Jeśli funkcja next() nigdy nie została wywołana, wywołanie metody Remove() spowoduje zgłoszenie wyjątku IllegalStateException ;
-
forEachRemaining(<Consumer>) — wykonuje przekazaną akcję na każdym elemencie kolekcji (metoda ta pojawiła się w Javie 8).
List<String> list = new ArrayList<>();
list.add("Hello ");
list.add("World, ");
list.add("It's ");
list.add("Amigo!");
Iterator iterator = list.iterator();
while(iterator.hasNext()) {
iterator.next();
iterator.remove();
}
System.out.println(list.size());
Konsola wyświetli następujące informacje:
iterator.forEachRemaining(x -> System.out.print(x));
Ale kiedy to zrobimy, iterator staje się nieodpowiedni do dalszego użycia: przeszedł całą listę, a zwykły iterator nie ma metod iteracji wstecz. To stanowi miłe przejście do dyskusji na temat LinkedList , a konkretnie na temat jej metody listIterator() , która zwraca ulepszony typ iteratora: ListIterator . Oprócz metod zwykłego (standardowego) iteratora, ten rodzaj ma następujące cechy:
-
add(<Element>) — dodaje nowy element do listy;
-
hasPrevious() — zwraca wartość true , jeśli przed kolejnym elementem znajduje się element (jeśli istnieje element poprzedni);
-
nextIndex() — zwraca indeks następnego elementu;
-
poprzedni() — zwraca element poprzedni (ten przed kolejnym elementem);
-
poprzedniIndeks zwraca indeks poprzedniego elementu.
-
set(<Element>) — zastępuje ostatni element zwrócony przez next() lub poprzedni() .
85. Jaka hierarchia kolekcji istnieje w Java Collection Framework?
W Javie istnieją dwie hierarchie kolekcji. Pierwszą hierarchią jest hierarchia Kolekcji, która ma następującą strukturę: Jest podzielona na następujące podkolekcje:-
Zestaw to interfejs opisujący zestaw, strukturę danych zawierającą nieuporządkowane, unikalne (niepowtarzające się) elementy. Ten interfejs ma kilka standardowych implementacji: TreeSet , HashSet i LinkedHashSet .
-
Lista to interfejs opisujący strukturę danych przechowującą uporządkowaną sekwencję obiektów. Obiekty na liście można wstawiać i usuwać na podstawie ich indeksu na liście (podobnie jak tablica, ale z dynamiczną zmianą rozmiaru). Ten interfejs ma również kilka standardowych implementacji: ArrayList , Vector ( przestarzałe i faktycznie nieużywane ) i LinkedList .
-
Kolejka to interfejs opisujący strukturę danych, która przechowuje elementy w kolejce „pierwsze weszło, pierwsze wyszło” (FIFO) . Interfejs ten ma następujące standardowe implementacje: LinkedList (zgadza się, implementuje również Queue ) i PriotityQueue .
86. Jaka jest wewnętrzna struktura listy ArrayList?
ArrayList przypomina tablicę, ale może się dynamicznie rozwijać . Co to znaczy? Pod maską ArrayList wykorzystuje zwykłą tablicę, tj. przechowuje swoje elementy w wewnętrznej tablicy, której domyślny rozmiar wynosi 10 komórek. Po zapełnieniu tablicy wewnętrznej tworzona jest nowa tablica. Rozmiar nowej tablicy określa się za pomocą następującego wzoru:<size of the current array> * 3 / 2 + 1
Zatem jeśli rozmiar naszej tablicy wynosi 10, to rozmiar nowej będzie wynosił: 10 * 3 / 2 + 1 = 16. Następnie kopiowane są do niej wszystkie wartości z oryginalnej (starej) tablicy za pomocą wbudowanej funkcji System.arraycopy() , a oryginalna tablica zostanie usunięta. W skrócie, w ten sposób ArrayList implementuje dynamiczną zmianę rozmiaru. Rozważmy najpopularniejsze metody ArrayList : 1. add(<Element>) — dodaje element na koniec tablicy (w ostatniej pustej komórce), po uprzednim sprawdzeniu, czy w tablicy jest dostępna komórka. Jeśli nie, tworzona jest nowa tablica i elementy są do niej kopiowane. Złożoność czasowa tej operacji wynosi O(1). Istnieje podobna metoda add(<Index>, <Elelement>) . Dodaje element nie na końcu listy (tablicy), ale do konkretnej komórki wskazanej przez indeks, który przyszedł jako argument. W takim przypadku złożoność czasowa będzie się różnić w zależności od tego, gdzie dodasz:
- jeśli dodanie będzie blisko początku listy, to złożoność czasowa będzie bliska O(N), ponieważ wszystkie elementy znajdujące się na prawo od nowego będą musiały zostać przesunięte o jedną komórkę w prawo;
- jeśli element zostanie wstawiony na środku, będzie to O(N/2), ponieważ wystarczy przesunąć połowę pozycji listy o jedną komórkę w prawo.
87. Jaka jest wewnętrzna struktura LinkedList?
ArrayList zawiera elementy w tablicy wewnętrznej, ale LinkedList przechowuje je na podwójnie połączonej liście . Oznacza to, że każdy element zawiera łącze do poprzedniego elementu i do następnego elementu. Pierwszy element nie łączy się z poprzednim elementem (w końcu jest pierwszym). Jest również uważany za głowę listy, a obiekt LinkedList ma do niego bezpośrednie odniesienie. Podobnie ostatni element nie ma następnego elementu, ponieważ jest końcem listy. Obiekt LinkedList również odwołuje się do niego bezpośrednio. Oznacza to, że złożoność czasowa dostępu do początku lub końca listy wynosi O(1). W ArrayList , jeśli lista rośnie, rośnie także tablica wewnętrzna. Tutaj wszystko jest prostsze: dodanie referencji jest tak proste, jak zmiana kilku linków. Przyjrzyjmy się niektórym najczęściej stosowanym metodom LinkedList : 1. add(<Element>) — dodaje element na koniec listy, czyli po ostatnim elemencie (5) jako następny zostanie dodany link do nowego elementu . Poprzednie odwołanie w nowym elemencie będzie wskazywało element (5), który teraz go poprzedza na liście. Złożoność czasowa tej operacji wynosi O(1), ponieważ potrzebujemy tylko połączenia z ostatnim elementem, a jak pamiętasz, obiekt LinkedList ma bezpośrednie odniesienie do ogona, a dostęp do niego ma minimalną stałą złożoność czasową. 2. add(<Index>, <Element>) — dodaje element według indeksu. Podczas dodawania elementu, powiedzmy, na środku listy, metoda ta najpierw iteruje po elementach od początku i końca (w obu kierunkach), aż do znalezienia żądanego miejsca. Jeżeli pomiędzy trzecim i czwartym elementem dodamy element (na powyższym obrazku), to kolejne łącze trzeciego elementu będzie wskazywało nowy element. A poprzedni nowo dodanego elementu będzie wskazywał trzeci. Z kolei poprzedni link starego czwartego elementu będzie teraz wskazywał nowy element, a następny link nowego elementu będzie wskazywał nowy czwarty element: Złożoność czasowa tej metody zależy od indeksu nowego elementu:- jeśli jest blisko głowy lub ogona, operacja zbliży się do O(1), ponieważ w rzeczywistości nie będzie konieczne iterowanie po elementach;
- jeśli będzie blisko środka, to będziemy mieli O(N/2), gdyż metoda będzie przeszukiwać jednocześnie od głowy i ogona, aż do znalezienia żądanego elementu.
88. Jaka jest wewnętrzna struktura HashMap?
To może być jedno z najpopularniejszych pytań na rozmowach kwalifikacyjnych, które zadaje się kandydatom na programistów Java. HashMap działa z parami klucz -wartość . W jaki sposób są przechowywane w samej HashMap ? HashMap ma wewnętrzną tablicę węzłów:Node<K,V>[] table
Domyślnie rozmiar tablicy wynosi 16 i podwaja się przy każdym wypełnieniu elementami (to znaczy po osiągnięciu LOAD_FACTOR ; określa próg zapełnienia tablicy — domyślnie jest to 0,75 ). . Każdy z węzłów przechowuje skrót klucza, klucz, wartość i odniesienie do następnego elementu: W tym przypadku „odwołanie do następnego elementu” oznacza, że mamy do czynienia z listą jednopołączoną, gdzie każdy element zawiera łącze do następnego. Innymi słowy, HashMap przechowuje swoje dane w tablicy pojedynczo połączonych list. Ale od razu powiem, że gdy komórka tablicy tabeli zawiera pojedynczo połączoną listę składającą się z więcej niż jednego elementu, nie jest to dobre. Zjawisko to nazywa się kolizją . Ale najpierw najważniejsze. Zobaczmy, jak zapisuje się nową parę za pomocą metody put . Najpierw metoda pobiera hashCode() klucza . Oznacza to, że aby HashMap działał poprawnie, używane klucze muszą być klasami, które zastępują tę metodę. Ten kod skrótu jest następnie używany w wewnętrznej metodzie hash() w celu określenia indeksu jakiejś komórki w tablicy tabeli. Wynikowy indeks umożliwia nam dostęp do określonej komórki tablicy tabeli. Mamy tu dwa przypadki:
- Komórka jest pusta — w tym przypadku zapisywana jest w niej nowa wartość Node .
- Komórka nie jest pusta — w tym przypadku porównywane są wartości kluczy. Jeśli są równe, nowa wartość Node zastępuje starą; jeśli nie jest równa, wówczas uzyskiwany jest dostęp do następnej i porównywany jest jej klucz... I tak dalej, aż nowa wartość albo nadpisze jakąś starą wartość, albo dotrzemy do końca pojedynczo połączonej listy i zapiszemy tam nową wartość jako ostatni element.
GO TO FULL VERSION