HashMap para devolver el valor predeterminado para las claves no encontradas?

151

¿Es posible HashMapdevolver un valor predeterminado para todas las claves que no se encuentran en el conjunto?

Larry
fuente
Puede verificar la existencia de claves y devolver el valor predeterminado. O extienda la clase y modifique el comportamiento. o incluso puede usar nulo, y poner un poco de verificación donde quiera usarlo.
SudhirJ
2
Esto está relacionado / duplicado de stackoverflow.com/questions/4833336/... algunas otras opciones se discuten allí.
Mark Butler
3
Echa un vistazo a la solución Java 8 para el getOrDefault() enlace
Trey Jonn

Respuestas:

136

[Actualizar]

Como lo señalaron otras respuestas y comentaristas, a partir de Java 8 simplemente puede llamar Map#getOrDefault(...).

[Original]

No hay una implementación de Map que haga esto exactamente, pero sería trivial implementar la suya propia extendiendo 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;
  }
}
maerics
fuente
20
Para ser precisos, es posible que desee ajustar la condición de (v == null)a (v == null && !this.containsKey(k))en caso de que agreguen un nullvalor a propósito . Lo sé, este es solo un caso de esquina, pero el autor puede encontrarse con él.
Adam Paynter
@maerics: me di cuenta de que lo usabas !this.containsValue(null). Esto es sutilmente diferente de !this.containsKey(k). La containsValuesolución fallará si a alguna otra clave se le ha asignado explícitamente un valor de null. Por ejemplo: map = new HashMap(); map.put(k1, null); V v = map.get(k2);en este caso, vseguirá siendo null, ¿correcto?
Adam Paynter
21
En general , creo que esta es una mala idea: empujaría el comportamiento predeterminado al cliente, o a un delegado que no pretende ser un Mapa. En particular, la falta de keySet () o entrySet () válidos causará problemas con cualquier cosa que espere que se respete el contrato de Map. Y el conjunto infinito de claves válidas que contieneKey () implica que es probable que cause un mal rendimiento que es difícil de diagnosticar. Sin embargo, no quiere decir que no sirva para un propósito particular.
Ed Staub
Un problema con este enfoque es si el valor es un objeto complicado. Map <String, List> #put no funcionará como se esperaba.
Eyal
No funciona en ConcurrentHashMap. Allí, debe verificar el resultado de get para nulo.
dveim
162

En Java 8, use Map.getOrDefault . Se necesita la clave y el valor para devolver si no se encuentra una clave coincidente.

Spycho
fuente
14
getOrDefaultes muy agradable, pero requiere la definición predeterminada cada vez que se accede al mapa. Definir un valor predeterminado una vez también tendría beneficios de legibilidad al crear un mapa estático de valores.
ach
3
Esto es trivial para implementarse. private static String get(Map map, String s) { return map.getOrDefault(s, "Error"); }
Jack Satriano
@JackSatriano Sí, pero tendrías que codificar el valor predeterminado o tener una variable estática para ello.
Blrp
1
Vea a continuación la respuesta usando computeIfAbsent, mejor cuando el valor predeterminado es costoso o debería ser diferente cada vez.
hectorpal
Aunque es peor para la memoria, y solo ahorrará tiempo de cálculo si el valor predeterminado es costoso de construir / calcular. Si es barato, probablemente encontrará que funciona peor, ya que tiene que insertarse en el mapa, en lugar de simplemente devolver un valor predeterminado. Sin duda, otra opción sin embargo.
Spycho
73

Use el Mapa predeterminado de Commons si no tiene ganas de reinventar la rueda, por ejemplo,

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

También puede pasar un mapa existente si no está a cargo de crear el mapa en primer lugar.

Dave Newton
fuente
26
+1 aunque a veces es más fácil reinventar la rueda que introducir grandes dependencias para pequeñas porciones de funcionalidad simple.
Maerics
3
y lo curioso es que muchos proyectos con los que trabajo ya tienen algo como esto en classpath (ya sea Apache Commons o Google Guava)
bartosz.r
@ bartosz.r, definitivamente no en el móvil
Pacerier
44

Java 8 introdujo un buen método predeterminado computeIfAbsent para la Mapinterfaz que almacena el valor calculado de manera diferida y no rompe el contrato del mapa:

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

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

Descargo de responsabilidad: esta respuesta no coincide exactamente con lo que OP solicitó, pero puede ser útil en algunos casos coincidir con el título de la pregunta cuando el número de claves es limitado y el almacenamiento en caché de diferentes valores sería rentable. No debe usarse en caso contrario con muchas teclas y el mismo valor predeterminado, ya que esto innecesariamente desperdiciaría memoria.

Vadzim
fuente
No es lo que OP preguntó: no quiere ningún efecto secundario en el mapa. Además, almacenar el valor predeterminado para cada clave ausente es una pérdida inútil de espacio de memoria.
numéro6
@ numéro6, sí, esto no coincide exactamente con lo que OP pidió, pero algunas personas en Google todavía encuentran útil esta respuesta secundaria. Como se mencionó en otras respuestas, es imposible lograr exactamente lo que OP pidió sin romper el contrato del mapa. Otra solución alternativa no mencionada aquí es usar otra abstracción en lugar de Map .
Vadzim
Es posible lograr exactamente lo que OP pidió sin romper el contrato del mapa. No se necesita una solución alternativa, solo usar getOrDefault es la forma correcta (más actualizada), computeIfAbsent es la forma incorrecta: perderá tiempo llamando a la función mapping y la memoria almacenando el resultado (ambos para cada tecla faltante). No veo ninguna buena razón para hacerlo en lugar de getOrDefault. Lo que estoy describiendo es la razón exacta por la que hay dos métodos distintos en el contrato de Map: hay dos casos de uso distintos que no deben confundirse (tuve que solucionar algunos en el trabajo). Esta respuesta extendió la confusión.
numéro6
14

¿No puedes crear un método estático que haga exactamente esto?

private static <K, V> V getOrDefault(Map<K,V> map, K key, V defaultValue) {
    return map.containsKey(key) ? map.get(key) : defaultValue;
}
Shervin Asgari
fuente
¿Dónde almacenar la estática?
Pacerier
10

Simplemente puede crear una nueva clase que herede HashMap y agregar el método getDefault. Aquí hay un código de muestra:

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

        return defaultValue;
    }
}

Creo que no debe anular el método get (clave K) en su implementación, debido a las razones especificadas por Ed Staub en su comentario y porque romperá el contrato de la interfaz de Map (esto puede conducir a algunos problemas difíciles de encontrar loco).

Ivan Mushketyk
fuente
44
Tienes un punto en no anular el getmétodo. Por otro lado, su solución no permite usar la clase a través de la interfaz, que a menudo puede ser el caso.
Krzysztof Jabłoński
5

Utilizar:

myHashMap.getOrDefault(key, defaultValue);
Diego Alejandro
fuente
3

Hace esto por defecto. Vuelve null.

mrkhrts
fuente
@ Larry, me parece un poco tonto subclasificar HashMapsolo por esta funcionalidad cuando nullestá perfectamente bien.
mrkhrts
15
Sin NullObjectembargo, no está bien si está utilizando un patrón o si no desea dispersar las verificaciones nulas en todo el código, un deseo que entiendo completamente.
Dave Newton el
3

En java 8+

Map.getOrDefault(Object key,V defaultValue)
Eduardo
fuente
1

El LazyMap me pareció bastante útil.

Cuando se llama al método get (Object) con una clave que no existe en el mapa, la fábrica se usa para crear el objeto. El objeto creado se agregará al mapa utilizando la clave solicitada.

Esto le permite hacer algo como esto:

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

La llamada a getcrea un valor predeterminado para la clave dada. Usted especifica cómo crear el valor predeterminado con el argumento de fábrica para LazyMap.lazyMap(map, factory). En el ejemplo anterior, el mapa se inicializa a un nuevo AtomicIntegercon valor 0.

Benedikt Köppel
fuente
Esto tiene una ventaja sobre la respuesta aceptada en que el valor predeterminado es creado por una fábrica. ¿Qué sucede si mi valor predeterminado es List<String>? Al usar la respuesta aceptada, me arriesgaría a usar la misma lista para cada nueva clave, en lugar de (por ejemplo) una new ArrayList<String>()de fábrica.
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);
}

Ejemplo de uso:

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));
KomodoDave
fuente
0

Necesitaba leer los resultados devueltos por un servidor en JSON donde no podía garantizar que los campos estuvieran presentes. Estoy usando la clase org.json.simple.JSONObject que se deriva de HashMap. Aquí hay algunas funciones auxiliares que empleé:

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