HashMap para retornar o valor padrão para chaves não encontradas?


151

É possível HashMapretornar um valor padrão para todas as chaves que não foram encontradas no conjunto?


Você pode verificar a existência da chave e retornar o padrão. Ou estenda a classe e modifique o comportamento. ou você pode usar nulo - e marque onde quiser.
precisa saber é o seguinte

2
Isso está relacionado / duplicado de stackoverflow.com/questions/4833336/… algumas outras opções são discutidas lá.
Mark Butler

3
Confira o getOrDefault() link da
Trey Jonn

Respostas:


136

[Atualizar]

Conforme observado por outras respostas e comentadores, no Java 8 você pode simplesmente ligar Map#getOrDefault(...).

[Original]

Não há implementação de mapa que faça isso exatamente, mas seria trivial implementar o seu próprio, estendendo o 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
Apenas para ser preciso, convém ajustar a condição de (v == null)para (v == null && !this.containsKey(k)), caso eles tenham adicionado um nullvalor propositalmente . Eu sei, este é apenas um caso de esquina, mas o autor pode se deparar com isso.
Adam Paynter 22/09

@ maerics: notei que você usou !this.containsValue(null). Isso é sutilmente diferente de !this.containsKey(k). A containsValuesolução falhará se alguma outra chave tiver sido atribuída explicitamente ao valor null. Por exemplo: map = new HashMap(); map.put(k1, null); V v = map.get(k2);Nesse caso, vainda estará nullcorreto?
Adam Paynter

21
Em geral , acho que é uma péssima idéia - inseria o comportamento padrão no cliente ou em um delegado que não afirma ser um mapa. Em particular, a falta de keySet () ou entrySet () válido causará problemas com qualquer coisa que espere que o contrato do Mapa seja respeitado. E o conjunto infinito de chaves válidas que contém Key () implica provavelmente causará um desempenho ruim e difícil de diagnosticar. Para não dizer, porém, que isso pode não servir a um propósito específico.
Ed Staub 22/09

Um problema com essa abordagem é se o valor é um objeto complicado. O mapa <String, Lista> #put não funcionará conforme o esperado.
Eyal

Não funciona no ConcurrentHashMap. Lá, você deve verificar o resultado de get como nulo.
Dveim

162

No Java 8, use Map.getOrDefault . Ele pega a chave e o valor a retornar se nenhuma chave correspondente for encontrada.


14
getOrDefaulté muito bom, mas requer a definição padrão sempre que o mapa é acessado. Definir um valor padrão uma vez também traria benefícios de legibilidade ao criar um mapa estático de valores.
ach

3
Isso é trivial para se implementar. private static String get(Map map, String s) { return map.getOrDefault(s, "Error"); }
Jack Satriano

@JackSatriano Sim, mas você teria que codificar o valor padrão ou ter uma variável estática para ele.
Blrp

1
Veja abaixo a resposta usando computeIfAbsent, melhor quando o valor padrão é caro ou deve ser diferente a cada vez.
hectorpal

Embora seja pior para a memória e economize tempo de computação apenas se o valor padrão for caro para construir / calcular. Se for barato, você provavelmente encontrará um desempenho pior, pois precisa inserir no mapa, em vez de apenas retornar um valor padrão. Certamente outra opção embora.
Spycho

73

Use o DefaultedMap do Commons se não quiser reinventar a roda, por exemplo,

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

Você também pode passar um mapa existente se não estiver encarregado de criar o mapa em primeiro lugar.


26
+1, embora às vezes seja mais fácil reinventar a roda do que introduzir grandes dependências para pequenas fatias de funcionalidade simples.
maerics

3
e engraçado é que muitos projetos que eu trabalho com a já ter algo parecido com isso no classpath (tanto do Apache Commons ou Google Guava)
bartosz.r

@ bartosz.r, definitivamente não no celular
Pacerier

44

O Java 8 introduziu um bom método padrão computeIfAbsent para fazer a Mapinterface, que armazena valor lento e, portanto, não quebra o contrato do mapa:

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

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

Disclamer: Essa resposta não corresponde exatamente ao que o OP pediu, mas pode ser útil em alguns casos, corresponder ao título da pergunta quando o número de chaves for limitado e o armazenamento em cache de valores diferentes for rentável. Não deve ser usado no caso oposto, com muitas chaves e o mesmo valor padrão, pois isso desperdiçaria desnecessariamente memória.


Não é o que o OP pediu: ele não quer nenhum efeito colateral no mapa. Além disso, armazenar o valor padrão para cada chave ausente é uma perda inútil de espaço na memória.
numéro6

@ numéro6, sim, isso não corresponde exatamente ao que o OP pediu, mas algumas pessoas pesquisando no Google ainda acham essa resposta útil. Como outras respostas mencionadas, é impossível obter exatamente o que o OP pediu sem quebrar o contrato do mapa. Outra solução alternativa não mencionada aqui é usar outra abstração em vez de Map .
Vadzim

É possível obter exatamente o que o OP pediu sem quebrar o contrato do mapa. Nenhuma solução alternativa é necessária, apenas usando getOrDefault é o caminho certo (mais atualizado), computeIfAbsent é o caminho errado: você perderá tempo chamando o mappingFunction e a memória armazenando o resultado (ambos para cada chave ausente). Não vejo nenhum bom motivo para fazer isso em vez de getOrDefault. O que estou descrevendo é a razão exata pela qual existem dois métodos distintos no contrato Map: existem dois casos de uso distintos que não devem ser confundidos (tive que corrigir alguns no trabalho). Essa resposta espalhou a confusão.
numéro6

14

Você não pode simplesmente criar um método estático que faça exatamente isso?

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

onde armazenar a estática?
Pacerier

10

Você pode simplesmente criar uma nova classe que herda HashMap e adicione o método getDefault. Aqui está um código de exemplo:

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

        return defaultValue;
    }
}

Eu acho que você não deve substituir o método get (chave K) em sua implementação, por causa dos motivos especificados por Ed Staub em seu comentário e porque você quebrará o contrato da interface do Mapa (isso pode potencialmente levar a alguns difíceis de encontrar) insetos).


4
Você tem razão em não substituir o getmétodo. Por outro lado - sua solução não permite o uso da classe via interface, o que geralmente pode ser o caso.
Krzysztof Jabłoński


3

Faz isso por padrão. Retorna null.


@ Larry, parece um pouco tolo para mim subclasse HashMapapenas para esta funcionalidade quando nullestá perfeitamente bem.
mrkhrts 22/09

15
Não é bom se você estiver usando um NullObjectpadrão, ou não quiser espalhar verificações nulas por todo o código - um desejo que eu entendo completamente.
Dave Newton

3

Em java 8+

Map.getOrDefault(Object key,V defaultValue)

1

Achei o LazyMap bastante útil.

Quando o método get (Object) é chamado com uma chave que não existe no mapa, a fábrica é usada para criar o objeto. O objeto criado será adicionado ao mapa usando a chave solicitada.

Isso permite que você faça algo assim:

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

A chamada para getcria um valor padrão para a chave fornecida. Você especifica como criar o valor padrão com o argumento de fábrica para LazyMap.lazyMap(map, factory). No exemplo acima, o mapa é inicializado para um novo AtomicIntegercom o valor 0.


Isso tem uma vantagem sobre a resposta aceita, pois o valor padrão é criado por uma fábrica. E se o meu valor padrão for List<String>- usando a resposta aceita, eu correria o risco de usar a mesma lista para cada nova chave, em vez de (digamos) a new ArrayList<String>()de uma 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);
}

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

0

Eu precisava ler os resultados retornados de um servidor em JSON onde não podia garantir que os campos estivessem presentes. Estou usando a classe org.json.simple.JSONObject, que é derivada do HashMap. Aqui estão algumas funções auxiliares que eu empreguei:

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

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.