DevCezz

Programistyczny blog dla Ciebie

dlaczego null nie zawsze może być kluczem w mapie?
Java

Dlaczego null nie zawsze może być kluczem w mapie?

Na jednej z rozmów kwalifikacyjnych otrzymałem ciekawe pytania dotyczące działania mechanizmów dostępnych w Javie. Jedno z nich naprawdę zapadło mi dobrze w pamięci. Brzmiało ono następująco: „Czy null może być kluczem w HashMap oraz ConcurrentHashMap?”. Przyznam szczerze, że nie umiałem odpowiedzieć na to pytanie, bo nigdy nie przyszło mi do głowy, aby coś takiego spróbować zrobić. Jednak jak przystało na programistę żyłka ciekawości nie dała mi spokoju. Postanowiłem to sprawdzić, a przy okazji podzielić się z Wami odpowiedzią na to pytanie.

Oficjalna dokumentacja Javy

HashMap

Wchodząc na stronę oficjalnej dokumentacji Javy znajdziemy wpis dotyczący HashMap. Już pierwszy akapit jest dla nas wartościowy w temacie dzisiejszego artykułu.

Hash table based implementation of the Map interface. This implementation provides all of the optional map operations, and permits null values and the null key. (The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.) This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.

Dostajemy informację, że HashMap pozwala na przechowywanie kluczy oraz wartości w postaci nulla. Jak dodatkowo informują twórcy ta implementacja jest bardzo podobna do rozwiązania Hashtable z wyjątkiem tego, że nie jest synchronizowana oraz zezwala właśnie na nulle.

ConcurrentHashMap

W przypadku ConcurrentHashMap musimy się bardziej wgryźć w to co jest napisane w dokumentacji. Dopiero po pewnym czasie lektury dowiadujemy się, że w przypadku tego rozwiązania trzymanie null jako klucza nie jest dozwolone (także wartość w mapie nie może być null).

Like Hashtable but unlike HashMap, this class does not allow null to be used as a key or value.

Od razu nasuwa się pytanie – skąd takie ograniczenia? Dlaczego jedna implementacja mapy pozwala na trzymanie nulla, a druga nie? Odpowiedź znajdziemy na niezawodnym StackOverflow.

Źródło wiedzy – StackOverflow

Użytkownik Bruno udzielił precyzyjnej odpowiedzi posługując się słowami samego Doug Lea.

The main reason that nulls aren’t allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can’t be accommodated. The main one is that if map.get(key) returns null, you can’t detect whether the key explicitly maps to null vs the key isn’t mapped. In a non-concurrent map, you can check this via map.contains(key), but in a concurrent one, the map might have changed between calls.

Wychodzi na to, że używając map.get(key) i otrzymując null do końca nie wiemy czy to wynika z tego, że wartością dla danego klucza jest null czy po prostu brakuje tego klucza w mapie. To jednak dalej nie tłumaczy faktu, dlaczego w HashMap jest możliwe umieszczanie null. Dopiero ostatnie zdanie daje nam pogląd na całą sytuację. W niewspółbieżnej implementacji możemy wywołać map.contains(key) i sprawdzić czy faktycznie mapa zawiera taki klucz czy nie. Natomiast dla rozwiązań wielowątkowych użycie contains, a potem get nie jest atomowe, więc stan mapy może się zmienić pomiędzy tymi wywołaniami. Z tego powodu twórca ConcurrentHashMap zabronił wstawiania null jako wartości.

No dobra, ale co z kluczem mapy?

Edit: Tak jak zauważył Michaldo to poniższe wyjaśnienie w odniesieniu do klucza jest redundantne. W wątku na StackOverflow, który przytoczyłem właśnie w ten sposób uzasadniali brak możliwości umieszczania null jako klucza oraz wartości w ConcurrentHashMap. Jeśli chodzi o wartość to ma to jak największy sens, ale dla klucza już nie do końca. Postanowiłem jeszcze bardziej zgłębić temat i oto co znalazłem. Jedną z tez jest to, że ConcurrentHashMap miał za zadanie wiernie odtworzyć implementację Hashtable, co w takich słowach opisano w dokumentacji.

This class is fully interoperable with Hashtable in programs that rely on its thread safety but not on its synchronization details.

Dlaczego jednak nie można wrzucić klucza null do Hashtable? Użytkownika Gautam Kumar uzasadnił to tak, że ówcześni inżynierowie Javy chcieli zniechęcić programistów do tego zabiegu bądź też nie zdawali sobie sprawy z jego przydatności. Natomiast użytkownik DaoWen tłumaczy, że ta restrykcja została wprowadzona ze względów wydajnościowych. Z kolei Khalid Nouh skomentował, że skoro dana implementacja mapy jest synchronizowana to nie może trzymać null jako klucza, bo nie można się blokować na braku wartości. Oczywiście jeszcze inni podają uzasadnienie takie, że w kodzie po prostu jest sprawdzenie czy klucz jest null. Jednak to nie tłumaczy intencji za tym stojącej.

Wcześniejsze uzasadnienie

Edit: Po komentarzu Piotrka uznałem, że to wyjaśnienie faktycznie nie ma sensu za co bardzo serdecznie mu dziękuję. 😊 Powyżej napisałem, że dla wartości nie możemy wstawić null, a przecież artykuł miał dotyczyć kluczy mapy. O co, więc chodzi? Uzasadnienie jest praktycznie to samo. Jednak lepiej wyjaśni to przykład użytkownika Alice Purcell, który znajdziemy w tym samym wątku na StackOverflow.

Dla zobrazowania załóżmy taki scenariusz – mapa m przechowuje pod kluczem null wartość równą również null. Jeżeli jako k przekazalibyśmy null to m.containsKey(k) zwróciłoby true. Wtedy m.get(k) zwróciłoby null i to byłby koniec. Natomiast co by się stało, gdy po wywołaniu m.containsKey(k) ktoś usunąłby z mapy klucz o wartości null? Wtedy m.get(k) również zwróciłoby null! Czyli do końca nie wiedzielibyśmy czy otrzymaliśmy wartość null, bo dla klucza null była taka wartość czy może ktoś usunął nasz klucz null i mapa teraz zwraca z tego powodu też null. Mam nadzieję, że to wyjaśnienie jasno tłumaczy dlaczego nie możemy wstawić null jako klucz w ConcurrentHashMap, czyli mapach wielowątkowych.

Podsumowanie

Dzięki rozmowom kwalifikacyjnym możemy nie tylko sprawdzić swój aktualny poziom wiedzy, ale również uświadomić sobie jak wiele jeszcze nie wiemy. Ja programuję już naprawdę sporo w Javie, a nadal potrafię być zaskoczony swoją niewiedzą w niektórych tematach. Mam nadzieję, że ten wpis nie tylko przedstawił Ci ciekawostkę na temat mechanizmów Javy, ale również zachęci Cię do weryfikowania swoich umiejętności poprzez aplikowanie na stanowiska dostępne na rynku pracy.

Podziel się tym z innymi!

4 KOMENTARZE

  1. „No dobra, ale co z kluczem mapy?”

    Tego nie wyjaśniłeś.

    Wyjaśniłeś dlaczego wartość nie może być null i dlaczego klucz i wartości nie może być null. Przy czym drugie wyjaśnienie jest redundantne.
    Ale dlaczego klucz nie może być null nie wyjaśniłeś.

    • Cześć Michaldo!

      Dzięki za czujność! Bije się w pierś, ponieważ już w trakcie pisania zastanawiałem się nad tym czy to faktycznie nie jest to samo wyjaśnienie dla tych dwóch zagadnień. Ciężko jednak jest znaleźć wyjaśnienie dla klucza null, ponieważ wszędzie podają praktycznie to samo uzasadnienie co do wartości. Za chwilę podzielę się prawdopodobnie najlepszym uzupełnieniem, które znalazłem na StackOverflow w tej sprawie. Jeśli dalej będzie nie do końca odpowiednie to proszę daj mi o tym znać. 🙂

      • Napisałeś:

        Czyli do końca nie wiedzielibyśmy czy otrzymaliśmy wartość null, bo dla klucza null była taka wartość czy może ktoś usunął nasz klucz null i mapa teraz zwraca z tego powodu też null. Mam nadzieję, że to wyjaśnienie jasno tłumaczy dlaczego nie możemy wstawić null jako klucz w ConcurrentHashMap, czyli mapach wielowątkowych.

        Moim zdaniem ten przykład niczego nie wyjaśnia, bynajmniej nie w jasny sposób.

        Przecież chwilę wcześniej napisałeś, że nie wolno trzymać wartości null w tej mapie a następnie ochoczo wrzucasz tą wartość dla nullowego klucza, żeby wspomóc się w wytłumaczeniu na siłę, że nulli nie można trzymać jako klucza w mapie.

        Idąc tym tokiem rozumowania to jak mamy zakaz dzielenia przez zero w matematyce to:
        1:0 jest niedozwolone bo nie można dzielić przez zero //OK
        0:0 jest niedozwolone bo nie można dzielić przez zero //OK
        0:1 jest niedozwolone bo gdybyśmy dzielili przez zero zamiast przez jeden to by się nie dało // ???

        Jak w przykładzie zmienisz wartość na coś innego od null to tłumaczenie dla klucza=null dalej jest sensowne?

        • Cześć Piotrek!

          Wiesz co, zgadzam się. Za bardzo zapętliłem się w tym opisie, bo nigdzie nie mogłem znaleźć sensownego wytłumaczenia dlaczego null nie można wrzucać jako klucz. Bardziej przykłady wyjaśniały dlaczego nie można wrzucić null jako wartości i popisywane były jako wyjaśnienie dla klucza. Dopiero po komentarzu Michaldo przejrzałem na oczy i wiem, że popełniłem tutaj gafę do czego się przyznaję. 🙂 Dzięki serdecznie, że przeczytałeś ten artykuł ze zrozumieniem i podzieliłeś się swoją opinią! Już poprawiam ten wpis, aby nikogo nie wprowadzać w błąd.

          Pozdrawiam serdecznie!

Możliwość komentowania została wyłączona.