Strona główna » Jak działa HashMap w Javie? (3 poziomy wtajemniczenia)

Jak działa HashMap w Javie? (3 poziomy wtajemniczenia)

by Grzegorz Sowa

Oprócz oczywistej wartości praktycznej, znajomość odpowiedzi na to pytanie często przydaje się na rozmowach rekrutacyjnych na Java Developera. Jak działa HashMap w Java? Aby w pełni odpowiedzieć na to pytanie, omówmy sobie HashMap na trzech „poziomach wtajemniczenia”. Najczęściej poziom pierwszy będzie wystarczył do „zaliczenia” rozmowy na poziomie juniora natomiast poziomy drugi i trzeci to już raczej poziom regular (i może jakaś rozgrzewka dla seniora).

Poziom 1 – główne założenia HashMap

Wkraczamy w pierwszy poziom wtajemniczenia, co to jest ogólnie HashMap? W skrócie jest to kolekcja, do której wkładamy pary klucz-wartość, a potem, mając klucz, możemy wyjąć konkretną wartość.

Jak działa insert do HashMap

Kiedy „wkładamy” do HashMap parę klucz-wartość metodą put(), para jest dodawana do kolekcji. Jeżeli dodamy inną wartość z identycznym kluczem, nadpisze ona wartość poprzednią. Natomiast jeśli dodamy parę z kluczem null – para również zostanie dodana. Oczywiście, możemy ją potem „wyjąć” podając jako klucz null. HashMap nie zachowuje jednak kolejności dodawanych par.

Jak działa szukanie w HashMap

Gdy chcemy wyciągnąć jakąś wartość z HashMap, używamy metody get(). Mapa porównuje wartość klucza z już posiadanymi kluczami, i w zależności, czy znajdzie, zwraca wartość lub null. Więc jak mapa określa, czy klucze są równe? No właśnie – używając metod hashCode() i equals().

Bardzo podobny zestaw czynności wystąpi przy każdej metodzie wymagającej wyszukania elementu, jak remove() czy containsKey().

Poziom 2 – wewnętrzne mechanizmy i Big O notation

Schodzimy poziom niżej, zobaczmy więc jak HashMap w Java zachowuje się „pod maską”. Otóż pary klucz-wartość mapa przechowuje razem, w obiekcie Node<K, V>, który implementuje interfejs Map.Entry<K, V>. Obiekty te są zorientowane w kolekcję, która odpowiada jednokierunkowej LinkedList – każde Entry ma referencję do następnego.

hashmap java diagram

Insert pod maską i buckety

HashMap to tak naprawdę tablica java o stałej długości składająca się umownie z tzw. bucketów. Każdy bucket to właśnie LinkedList złożony z obiektów Entry. Gdy wywołujemy metodę put() na mapie, HashMap z tablicą o długości n w pierwszej kolejności wylicza indeks bucketu, do którego trafi nowe Entry. A robi to, używając bitowej operacji AND na hashu klucza.

index = key.hashCode() & (n - 1)

Gdy już określi index bucketu, dodaje do niego parę. Warto tutaj zauważyć, że jeśli w buckecie już coś jest, mapa będzie porównywać metodą equals() klucze, które już buckecie są, z nowym kluczem. Jeśli któryś z nich okaże się równy to zamiast dodawać nową parę, podmieni wartość w tej z identycznym kluczem. Natomiast jeśli klucz jest null – cały algorytm przebiega tak samo, tyle że wartość trafia zawsze do bucketu z indeksem 0.

Złożoność dodawania nowej pary do HashMap jest taka sama, jak do LinkedList, czyli O(1).

Lookup pod maską – uważaj na hashCode() i equals()

Gdy wywołujemy metodę wymagającą wyszukania po kluczu, HashMap wykonuje to samo wyliczenie, co przy put(), obliczając indeks bucketu odpowiadającego kluczowi. Jeśli jest tam tylko jedno Entry – nie ma problemu, mapa od razu zwraca wartość. Jednak jeśli będzie tam więcej obiektów, mapa porówna klucz z każdym w buckecie operacją equals(). Dopiero, jeśli dla któregoś z nich equals() zwróci true, mapa zwróci odpowiadającą mu wartość.

Złożoność wyciągania obiektów z HashMap to również 0(1). Załóżmy jednak, że źle zdefiniowaliśmy metodę hashCode(). Na przykład, jakiś niedoświadczony developer uznał że może tam zawsze zwracać taką samą wartość. Wtedy złożoność szukania osiągniemy taką samą, jak w LinkedList – O(n). A to dlatego, że wszystkie Entry będą trafiać do jednego bucketu.

Synchronizacja

HashMapa nie jest bezpieczna dla równoległego przetwarzania przez wiele wątków – używając jej, musimy więc zapewnić synchronizację jakimś innym mechanizmem. Alternatywnie możemy użyć ConcurrentHashMap. Więcej o użyciu kolekcji w środowisku wielowątkowym możesz poczytać tutaj

Poziom 3 – optymalizacja

Gdyby HashMap utworzone z ośmioma bucketami miało ich już na zawsze 8, to w miarę dodawania kolejnych obiektów złożoność szukania w niej dążyłaby szybko do O(n). Dlatego aby pozostać przy pożądanym O(1) potrzebujemy pewnych optymalizacji.

Pojemność

Startową liczbę bucketów możemy określić, kiedy tworzymy HashMap – parametrem initialCapacity. Jeśli go nie podamy, otrzyma on domyślna wartość, czyli 16. Możemy też zdefiniować dodatkowy parametr loadFactor typu float w zakresie od 0 do 1. Parametr ten odpowiada za moment, w którym mapa podwoi liczbę swoich bucketów, wywołująć metodę resize(). Dokładniej, mając loadFactor równy 0.75 (wartość domyślna), HashMap wywoła resize() w momencie, gdy ponad 75% bucketów będzie miała co najmniej jedno Entry. Podczas powiększania jest tworzona nowa tablica bucketów, dwukrotnie większa, a indeks dla każdego Entry jest wyliczany ponownie.

Złożoność operacji dodawania, przy której HashMap będzie się powiększać, to już nie O(1), a O(n). Dlatego, jeśli znamy przybliżoną liczbę obiektów, jakie będziemy obsługiwać – warto od razu zainicjalizować większą mapę.

treeify/untreeify

Jest jeszcze jedna optymalizacja działania HashMap w Java – jeśli dany bucket „urośnie” i będzie miał 8 Entry – zostaje na nim wywołana metoda treeifyBin() i wszystkie instancje Node<K, V> w tym buckecie zostaną zamienione na TreeNode<K, V>, efektywnie tworząc drzewo binarne.  Dodatkowym warunkiem, jaki musi być spełniony jest osiągnięcie liczby bucketów większej od 64. Jeśli jest ich mniej, mapa wywołuje metodę resize(). Natomiast jeśli liczba Entry w buckecie zmniejszy się do 6 – zostanie wywołana metoda untreeify() i bucket znowu będzie listą.

Optymalizacja ta ma konkretną korzyść – złożoność wyszukiwania elementów w danym buckecie z O(n) zmienia się na O(log(n)), jeśli ma on wiele elementów.

Podsumowanie

Mam nadzieję, że we w miarę przystępny sposób przybliżyłem zasadę działania HashMap w Java. Prawdopodobnie z wiedzą z tego artykułu nawet pytania najdociekliwszego rekrutera nie powinny stanowić problemu. 🙂

You may also like

Leave a Comment