HashMap per restituire il valore predefinito per le chiavi non trovate?


151

È possibile avere un HashMapvalore predefinito di ritorno per tutte le chiavi che non si trovano nel set?


È possibile verificare l'esistenza della chiave e restituire l'impostazione predefinita. Oppure estendi la classe e modifica il comportamento. o anche tu puoi usare null - e metti un po 'di controllo ovunque tu voglia usarlo.
SudhirJ,

2
Questo è correlato / duplicato di stackoverflow.com/questions/4833336/… alcune altre opzioni sono discusse qui.
Mark Butler,

3
Scopri la soluzione Java 8 per il getOrDefault() collegamento
Trey Jonn,

Risposte:


136

[Aggiornare]

Come notato da altre risposte e commentatori, a partire da Java 8 puoi semplicemente chiamare Map#getOrDefault(...).

[Originale]

Non esiste un'implementazione di Map che lo fa esattamente, ma sarebbe banale implementare il tuo estendendo HashMap:

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
Per essere precisi, potresti voler regolare la condizione da (v == null)a (v == null && !this.containsKey(k))nel caso in cui abbiano intenzionalmente aggiunto un nullvalore. Lo so, questo è solo un caso angolare, ma l'autore potrebbe imbattersi in esso.
Adam Paynter,

@maerics: ho notato che hai usato !this.containsValue(null). Questo è leggermente diverso da !this.containsKey(k). La containsValuesoluzione fallirà se ad un'altra chiave è stato assegnato esplicitamente un valore di null. Ad esempio: map = new HashMap(); map.put(k1, null); V v = map.get(k2);in questo caso, vsarà ancora nullcorretto?
Adam Paynter,

21
In generale , penso che questa sia una cattiva idea: inserirò il comportamento predefinito nel client o in un delegato che non pretende di essere una mappa. In particolare, la mancanza di keySet () o entrySet () validi causerà problemi con qualsiasi cosa che preveda il rispetto del contratto Map. E la serie infinita di chiavi valide che contiene Key () implica che probabilmente causerà cattive prestazioni difficili da diagnosticare. Per non dire, tuttavia, che potrebbe non servire a uno scopo particolare.
Ed Staub,

Un problema con questo approccio è se il valore è un oggetto complicato. Map <String, List> #put non funzionerà come previsto.
Eyal,

Non funziona su ConcurrentHashMap. Lì, dovresti controllare il risultato di get per null.
dveim

162

In Java 8, usa Map.getOrDefault . Prende la chiave e il valore da restituire se non viene trovata alcuna chiave corrispondente.


14
getOrDefaultè molto bello, ma richiede la definizione predefinita ogni volta che si accede alla mappa. La definizione di una volta un valore predefinito avrebbe anche vantaggi di leggibilità quando si crea una mappa statica di valori.
ach

3
Questo è banale da implementare. private static String get(Map map, String s) { return map.getOrDefault(s, "Error"); }
Jack Satriano

@JackSatriano Sì, ma dovresti codificare il valore predefinito o avere una variabile statica per esso.
Blrp,

1
Vedi sotto la risposta usando computeIfAbsent, meglio quando il valore predefinito è costoso o dovrebbe essere diverso ogni volta.
hectorpal,

Anche se è peggio per la memoria, e farà risparmiare tempo di calcolo solo se il valore predefinito è costoso da costruire / calcolare. Se è economico, probabilmente troverai prestazioni peggiori, poiché deve essere inserito nella mappa, piuttosto che restituire un valore predefinito. Certamente un'altra opzione però.
Spycho,

73

Usa la DefaultedMap di Commons se non hai voglia di reinventare la ruota, ad es.

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

Puoi anche passare a una mappa esistente se non sei incaricato di creare la mappa in primo luogo.


26
+1 anche se a volte è più facile reinventare la ruota piuttosto che introdurre grandi dipendenze per piccole porzioni di semplice funzionalità.
Maerics,

3
e la cosa divertente è che molti progetti con cui lavoro hanno già qualcosa del genere in classpath (o Apache Commons o Google Guava)
bartosz.r

@ bartosz.r, sicuramente non su cellulare
Pacerier

44

Java 8 ha introdotto un bel metodo predefinito di computeIfAbsent per l' Mapinterfaccia che memorizza il valore calcolato in modo pigro e quindi non rompe il contratto della mappa:

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

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

Disclamer: questa risposta non corrisponde esattamente a ciò che OP ha chiesto, ma può essere utile in alcuni casi abbinare il titolo della domanda quando il numero di chiavi è limitato e la memorizzazione nella cache di valori diversi sarebbe redditizia. Non dovrebbe essere usato nel caso opposto con molte chiavi e lo stesso valore predefinito in quanto ciò sprecherebbe inutilmente memoria.


Non è quello che OP ha chiesto: non vuole effetti collaterali sulla mappa. Inoltre, la memorizzazione del valore predefinito per ogni chiave assente è una perdita inutile di spazio di memoria.
numéro6,

@ numéro6, sì, questo non corrisponde esattamente a ciò che OP ha chiesto, ma alcune persone che cercano su Google trovano ancora utile questa risposta. Come menzionato da altre risposte, è impossibile ottenere esattamente ciò che l'OP ha chiesto senza rompere il contratto di mappatura. Un'altra soluzione alternativa non menzionata qui è utilizzare un'altra astrazione invece di Mappa .
Vadzim,

È possibile ottenere esattamente ciò che l'OP ha chiesto senza rompere il contratto cartografico. Non è necessaria alcuna soluzione alternativa, usare semplicemente getOrDefault è il modo giusto (più aggiornato), computeIfAbsent è il modo sbagliato: perderai tempo a chiamare mappingFunction e memoria memorizzando il risultato (entrambi per ogni chiave mancante). Non riesco a vedere alcuna buona ragione per farlo invece di getOrDefault. Quello che sto descrivendo è il motivo esatto per cui ci sono due metodi distinti nel contratto Map: ci sono due casi d'uso distinti che non dovrebbero essere confusi (ho dovuto ripararne alcuni sul lavoro). Questa risposta ha diffuso la confusione.
numéro6,

14

Non puoi semplicemente creare un metodo statico che fa esattamente questo?

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

dove conservare la statica?
Pacerier

10

Puoi semplicemente creare una nuova classe che eredita HashMap e aggiungere il metodo getDefault. Ecco un codice di esempio:

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

        return defaultValue;
    }
}

Penso che non dovresti ignorare il metodo get (tasto K) nella tua implementazione, a causa dei motivi specificati da Ed Staub nel suo commento e perché infrangerai il contratto dell'interfaccia di Map (questo può potenzialmente portare a qualche difficile da trovare insetti).


4
Hai ragione a non ignorare il getmetodo. D'altra parte, la tua soluzione non consente di utilizzare la classe tramite l'interfaccia, il che potrebbe essere il caso.
Krzysztof Jabłoński,


3

Lo fa per impostazione predefinita. Ritorna null.


@Larry, mi sembra un po 'sciocco fare la sottoclasse HashMapsolo per questa funzionalità quando nullva perfettamente bene.
mrkhrts,

15
Non va bene se stai usando un NullObjectmodello, o se non vuoi spargere i controlli null nel tuo codice - un desiderio che comprendo completamente.
Dave Newton,

3

Su java 8+

Map.getOrDefault(Object key,V defaultValue)

1

Ho trovato LazyMap abbastanza utile.

Quando viene chiamato il metodo get (Object) con una chiave che non esiste nella mappa, viene utilizzato il factory per creare l'oggetto. L'oggetto creato verrà aggiunto alla mappa usando la chiave richiesta.

Questo ti permette di fare qualcosa del genere:

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

La chiamata a getcrea un valore predefinito per la chiave specificata. Si specifica come creare il valore predefinito con l'argomento factory su LazyMap.lazyMap(map, factory). Nell'esempio sopra, la mappa viene inizializzata su una nuova AtomicIntegercon valore 0.


Ciò ha un vantaggio rispetto alla risposta accettata in quanto il valore predefinito viene creato da una factory. E se il mio valore predefinito fosse List<String>: usando la risposta accettata rischierei di usare lo stesso elenco per ogni nuova chiave, piuttosto che (diciamo) un new ArrayList<String>()da una fabbrica.


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);
}

Esempio di utilizzo:

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

Avevo bisogno di leggere i risultati restituiti da un server in JSON dove non potevo garantire che i campi fossero presenti. Sto usando la classe org.json.simple.JSONObject che deriva da HashMap. Ecco alcune funzioni di supporto che ho impiegato:

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; }
}   

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.