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.