HashMap, aby zwrócić domyślną wartość dla nieodnalezionych kluczy?


151

Czy jest możliwe HashMapzwrócenie wartości domyślnej dla wszystkich kluczy, których nie ma w zestawie?


Możesz sprawdzić istnienie klucza i zwrócić wartość domyślną. Lub rozszerz klasę i zmodyfikuj zachowanie. lub nawet możesz użyć null - i umieścić jakiś czek, gdziekolwiek chcesz go użyć.
— SudhirJ,

2
Jest to powiązane / duplikat stackoverflow.com/questions/4833336/ ... omówiono tam kilka innych opcji.
— Mark Butler,

3
Sprawdź rozwiązanie Java 8 dla getOrDefault() łącza
— Trey Jonn

Odpowiedzi:


136

[Aktualizacja]

Jak zauważyli inni odpowiedzi i komentatorzy, od wersji Java 8 możesz po prostu zadzwonić Map#getOrDefault(...).

[Oryginalny]

Nie ma implementacji Map, która robi to dokładnie, ale zaimplementowanie własnej przez rozszerzenie HashMap byłoby trywialne:

public class DefaultHashMap<K,V> extends HashMap<K,V> {
  protected V defaultValue;
  public DefaultHashMap(V defaultValue) {
    this.defaultValue = defaultValue;
  }
  @Override
  public V get(Object k) {
    return containsKey(k) ? super.get(k) : defaultValue;
  }
}

20
Aby być precyzyjnym, możesz dostosować warunek z (v == null)do (v == null && !this.containsKey(k))w przypadku, gdy celowo dodali nullwartość. Wiem, to tylko przypadek narożny, ale autor może się na to natknąć.
— Adam Paynter,

@maerics: Zauważyłem, że korzystałeś !this.containsValue(null). To jest nieco inne niż !this.containsKey(k). containsValueRozwiązanie zawiedzie, jeśli jakiś inny klucz został jednoznacznie przypisana wartość null. Na przykład: w map = new HashMap(); map.put(k1, null); V v = map.get(k2);tym przypadku vnadal będzie null, prawda?
— Adam Paynter

21
Ogólnie uważam, że to zły pomysł - wpychałbym domyślne zachowanie do klienta lub delegata, który nie twierdzi, że jest mapą. W szczególności brak prawidłowego zestawu keySet () lub entrySet () spowoduje problemy ze wszystkim, co wymaga przestrzegania kontraktu Map. A nieskończony zestaw prawidłowych kluczy, który zawiera metodę includeKey (), prawdopodobnie spowoduje złe działanie, które jest trudne do zdiagnozowania. Nie znaczy to jednak, że może nie służyć jakiemuś konkretnemu celowi.
— Ed Staub,

Problem z tym podejściem polega na tym, że wartość jest skomplikowanym obiektem. Map <String, List> #put nie będzie działać zgodnie z oczekiwaniami.
— Eyal

Nie działa na ConcurrentHashMap. Tam powinieneś sprawdzić, czy wynik get jest pusty.
— dveim

162

W Javie 8 użyj Map.getOrDefault . Pobiera klucz i wartość do zwrócenia, jeśli nie zostanie znaleziony pasujący klucz.


14
getOrDefaultjest bardzo ładny, ale wymaga domyślnej definicji przy każdym dostępie do mapy. Jednokrotne zdefiniowanie wartości domyślnej przyniosłoby również korzyści związane z czytelnością podczas tworzenia statycznej mapy wartości.
— ach

3
Samodzielne wdrożenie jest trywialne. private static String get(Map map, String s) { return map.getOrDefault(s, "Error"); }
— Jack Satriano

@JackSatriano Tak, ale musiałbyś na stałe zakodować wartość domyślną lub mieć dla niej zmienną statyczną.
— Blrp

1
Zobacz poniżej odpowiedź za pomocą computeIfAbsent, lepiej, gdy wartość domyślna jest droga lub powinna być za każdym razem inna.
— hectorpal

Chociaż jest to gorsze dla pamięci i pozwoli zaoszczędzić czas obliczeń tylko wtedy, gdy wartość domyślna jest kosztowna do skonstruowania / obliczenia. Jeśli jest tani, prawdopodobnie okaże się, że działa gorzej, ponieważ musi zostać wstawiony do mapy, a nie tylko zwracać domyślną wartość. Z pewnością inna opcja.
— Spycho

73

Użyj DefaultedMap Commons, jeśli nie masz ochoty wymyślać koła na nowo, np.

Map<String, String> map = new DefaultedMap<>("[NO ENTRY FOUND]");
String surname = map.get("Surname"); 
// surname == "[NO ENTRY FOUND]"

Możesz również przekazać istniejącą mapę, jeśli nie jesteś odpowiedzialny za jej tworzenie.


26
+1 chociaż czasami łatwiej jest wymyślić koło na nowo niż wprowadzić duże zależności dla małych kawałków prostej funkcjonalności.
— maerics

3
i zabawne jest to, że wiele projektów, nad którymi pracuję, ma już coś takiego w ścieżce klas (albo Apache Commons, albo Google Guava)
— bartosz.r

@ bartosz.r, zdecydowanie nie na komórce
— Pacerier

44

Java 8 wprowadziła ładną domyślną metodę computeIfAbsent do Mapinterfejsu, która przechowuje leniwą wartość obliczoną i dlatego nie przerywa kontraktu mapy:

Map<Key, Graph> map = new HashMap<>();
map.computeIfAbsent(aKey, key -> createExpensiveGraph(key));

Pochodzenie: http://blog.javabien.net/2014/02/20/loadingcache-in-java-8-without-guava/

Disclamer: Ta odpowiedź nie pasuje dokładnie do tego, co zadał OP, ale może być przydatna w niektórych przypadkach dopasowanie tytułu pytania, gdy liczba kluczy jest ograniczona, a buforowanie różnych wartości byłoby opłacalne. Nie należy go używać w przeciwnym przypadku z dużą ilością kluczy i tą samą wartością domyślną, ponieważ niepotrzebnie marnuje to pamięć.


Nie to, o co pytał OP: nie chce żadnych efektów ubocznych na mapie. Ponadto przechowywanie wartości domyślnej dla każdego nieobecnego klucza jest bezużyteczną utratą miejsca w pamięci.
— numéro6

@ numéro6, tak, to nie zgadza się dokładnie z tym, o co pytał OP, ale niektórzy googlujący ludzie nadal uważają tę boczną odpowiedź za przydatną. Jak wspomniały inne odpowiedzi, niemożliwe jest osiągnięcie dokładnie tego, o co prosił OP bez zerwania kontraktu dotyczącego mapy. Innym obejściem, o którym tutaj nie wspomniano, jest użycie innej abstrakcji zamiast Map .
— Vadzim

Możliwe jest osiągnięcie dokładnie tego, o co prosił OP, bez zrywania kontraktu dotyczącego mapy. Żadne obejście nie jest potrzebne, po prostu użycie getOrDefault jest właściwym (najbardziej zaktualizowanym) sposobem, computeIfAbsent jest złym sposobem: stracisz czas na wywoływanie funkcji mappingFunction i pamięci, przechowując wynik (oba dla każdego brakującego klucza). Nie widzę żadnego dobrego powodu, aby to zrobić zamiast getOrDefault. To, co opisuję, to dokładny powód, dla którego w kontrakcie Map istnieją dwie różne metody: istnieją dwa różne przypadki użycia, których nie należy mylić (niektóre musiałem naprawić w pracy). Ta odpowiedź spowodowała zamieszanie.
— numéro6

14

Nie możesz po prostu utworzyć statycznej metody, która robi dokładnie to?

private static <K, V> V getOrDefault(Map<K,V> map, K key, V defaultValue) {
    return map.containsKey(key) ? map.get(key) : defaultValue;
}

gdzie przechowywać statyczne?
— Pacerier

10

Możesz po prostu utworzyć nową klasę, która dziedziczy HashMap i dodać metodę getDefault. Oto przykładowy kod:

public class DefaultHashMap<K,V> extends HashMap<K,V> {
    public V getDefault(K key, V defaultValue) {
        if (containsKey(key)) {
            return get(key);
        }

        return defaultValue;
    }
}

Myślę, że nie powinieneś nadpisywać metody get (klawisz K) w swojej implementacji, z powodów określonych przez Eda Stauba w swoim komentarzu oraz z powodu zerwania kontraktu interfejsu Map (może to potencjalnie prowadzić do trudnych do znalezienia robaki).


4
Masz rację, aby nie zastępować getmetody. Z drugiej strony - Twoje rozwiązanie nie pozwala na używanie klasy przez interfejs, co często może mieć miejsce.
— Krzysztof Jabłoński


3

Robi to domyślnie. Wraca null.


@Larry, wydaje mi się trochę głupie, aby utworzyć podklasę HashMaptylko dla tej funkcji, kiedynull jest w porządku.
— mrkhrts

15
Nie jest dobrze, jeśli używasz NullObject wzorca lub nie chcesz rozpraszać kontroli null w całym kodzie - pragnienie, które całkowicie rozumiem.
— Dave Newton


1

znalazłem LazyMap jest całkiem pomocny.

Gdy metoda get (Object) jest wywoływana z kluczem, którego nie ma na mapie, do utworzenia obiektu używana jest fabryka. Utworzony obiekt zostanie dodany do mapy przy użyciu żądanego klucza.

To pozwala zrobić coś takiego:

    Map<String, AtomicInteger> map = LazyMap.lazyMap(new HashMap<>(), ()->new AtomicInteger(0));
    map.get(notExistingKey).incrementAndGet();

Wywołanie do gettworzy wartość domyślną dla danego klucza. Możesz określić, jak utworzyć wartość domyślną z argumentem fabryki na LazyMap.lazyMap(map, factory). W powyższym przykładzie mapa jest inicjalizowana jako nowa AtomicIntegerz wartością 0.


Ma to przewagę nad zaakceptowaną odpowiedzią, ponieważ wartość domyślna jest tworzona przez fabrykę. A co, jeśli moją wartością domyślną jest List<String>- używając zaakceptowanej odpowiedzi zaryzykowałbym użycie tej samej listy dla każdego nowego klucza, zamiast (powiedzmy) a new ArrayList<String>()z fabryki.


0
/**
 * Extension of TreeMap to provide default value getter/creator.
 * 
 * NOTE: This class performs no null key or value checking.
 * 
 * @author N David Brown
 *
 * @param <K>   Key type
 * @param <V>   Value type
 */
public abstract class Hash<K, V> extends TreeMap<K, V> {

    private static final long serialVersionUID = 1905150272531272505L;

    /**
     * Same as {@link #get(Object)} but first stores result of
     * {@link #create(Object)} under given key if key doesn't exist.
     * 
     * @param k
     * @return
     */
    public V getOrCreate(final K k) {
        V v = get(k);
        if (v == null) {
            v = create(k);
            put(k, v);
        }
        return v;
    }

    /**
     * Same as {@link #get(Object)} but returns specified default value
     * if key doesn't exist. Note that default value isn't automatically
     * stored under the given key.
     * 
     * @param k
     * @param _default
     * @return
     */
    public V getDefault(final K k, final V _default) {
        V v = get(k);
        return v == null ? _default : v;
    }

    /**
     * Creates a default value for the specified key.
     * 
     * @param k
     * @return
     */
    abstract protected V create(final K k);
}

Przykładowe zastosowanie:

protected class HashList extends Hash<String, ArrayList<String>> {
    private static final long serialVersionUID = 6658900478219817746L;

    @Override
        public ArrayList<Short> create(Short key) {
            return new ArrayList<Short>();
        }
}

final HashList haystack = new HashList();
final String needle = "hide and";
haystack.getOrCreate(needle).add("seek")
System.out.println(haystack.get(needle).get(0));

0

Musiałem odczytać wyniki zwrócone z serwera w formacie JSON, gdzie nie mogłem zagwarantować, że pola będą obecne. Używam klasy org.json.simple.JSONObject, która jest pochodną HashMap. Oto kilka funkcji pomocniczych, których użyłem:

public static String getString( final JSONObject response, 
                                final String key ) 
{ return getString( response, key, "" ); }  
public static String getString( final JSONObject response, 
                                final String key, final String defVal ) 
{ return response.containsKey( key ) ? (String)response.get( key ) : defVal; }

public static long getLong( final JSONObject response, 
                            final String key ) 
{ return getLong( response, key, 0 ); } 
public static long getLong( final JSONObject response, 
                            final String key, final long defVal ) 
{ return response.containsKey( key ) ? (long)response.get( key ) : defVal; }

public static float getFloat( final JSONObject response, 
                              final String key ) 
{ return getFloat( response, key, 0.0f ); } 
public static float getFloat( final JSONObject response, 
                              final String key, final float defVal ) 
{ return response.containsKey( key ) ? (float)response.get( key ) : defVal; }

public static List<JSONObject> getList( final JSONObject response, 
                                        final String key ) 
{ return getList( response, key, new ArrayList<JSONObject>() ); }   
public static List<JSONObject> getList( final JSONObject response, 
                                        final String key, final List<JSONObject> defVal ) { 
    try { return response.containsKey( key ) ? (List<JSONObject>) response.get( key ) : defVal; }
    catch( ClassCastException e ) { return defVal; }
}   

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.