HashMap, чтобы вернуть значение по умолчанию для не найденных ключей?

151

Можно ли HashMapвернуть значение по умолчанию для всех ключей, которые не найдены в наборе?

Ларри
источник
Вы можете проверить наличие ключа и вернуть значение по умолчанию. Или расширить класс и изменить поведение. или даже вы можете использовать нуль - и поставить некоторые проверки, где вы хотите его использовать.
SudhirJ
2
Это связано / дублирует stackoverflow.com/questions/4833336/… некоторые другие варианты обсуждаются там.
Марк Батлер
3
Посмотрите решение Java 8 для getOrDefault() ссылки
Трей Джонн

Ответы:

136

[Обновить]

Как отмечают другие ответы и комментаторы, начиная с Java 8 вы можете просто позвонить Map#getOrDefault(...).

[Оригинал]

Нет реализации Map, которая делает это точно, но было бы тривиально реализовать свою собственную, расширяя 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
источник
20
Просто чтобы быть точным, вы можете изменить условие с (v == null)на, (v == null && !this.containsKey(k))если они намеренно добавили nullзначение. Я знаю, это всего лишь угловой случай, но автор может столкнуться с этим.
Адам Пейнтер
@maerics: я заметил, что вы использовали !this.containsValue(null). Это немного отличается от !this.containsKey(k). containsValueРешение потерпит неудачу , если некоторые другие ключ был явно присвоено значение null. Например: map = new HashMap(); map.put(k1, null); V v = map.get(k2);в этом случае vвсе равно будет null, правильно?
Адам Пейнтер
21
В целом , я думаю, что это плохая идея - я бы запихнул поведение по умолчанию в клиента или делегата, который не претендует на то, чтобы быть картой. В частности, отсутствие действительного keySet () или entrySet () вызовет проблемы со всем, что ожидает соблюдения контракта Map. А бесконечный набор допустимых ключей, который включает в себяKey (), может привести к снижению производительности, которую сложно диагностировать. Не сказать, однако, что это может не служить какой-то конкретной цели.
Эд Стауб
Одна из проблем этого подхода заключается в том, что значение является сложным объектом. Карта <String, List> #put не будет работать должным образом.
Eyal
Не работает на ConcurrentHashMap. Там вы должны проверить результат get для нуля.
Двим
162

В Java 8 используйте Map.getOrDefault . Он принимает ключ и значение, которое возвращается, если не найдено ни одного соответствующего ключа.

Spycho
источник
14
getOrDefaultэто очень хорошо, но требует определения по умолчанию каждый раз, когда к карте обращаются. Однократное определение значения по умолчанию также будет иметь преимущества для удобства чтения при создании статической карты значений.
ACH
3
Это тривиально, чтобы реализовать себя. private static String get(Map map, String s) { return map.getOrDefault(s, "Error"); }
Джек Сатриано,
@JackSatriano Да, но вам придется жестко закодировать значение по умолчанию или иметь статическую переменную для него.
Blrp
1
Ниже приведен ответ с использованием computeIfAbsent, лучше, когда значение по умолчанию дорого или каждый раз должно отличаться.
Hectorpal
Хотя это хуже для памяти и сэкономит время вычислений, только если значение по умолчанию дорого построить / вычислить. Если это дешево, вы, вероятно, обнаружите, что оно работает хуже, поскольку его нужно вставить в карту, а не просто вернуть значение по умолчанию. Конечно, еще один вариант, хотя.
Spycho
73

Используйте DefonstedMap Commons, если вы не хотите изобретать велосипед, например,

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

Вы также можете передать существующую карту, если вы не отвечаете за ее создание.

Дэйв Ньютон
источник
26
+1, хотя иногда проще заново изобрести колесо, чем вводить большие зависимости для крошечных кусочков простой функциональности.
maerics
3
и забавно то, что многие проекты, с которыми я работаю, уже имеют что-то подобное в classpath (Apache Commons или Google Guava)
bartosz.r
@ bartosz.r, определенно не на мобильном телефоне
Pacerier
44

Java 8 представила хороший метод по умолчанию computeIfAbsent для Mapинтерфейса, который хранит ленивые значения и не нарушает контракт карты:

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

Происхождение: http://blog.javabien.net/2014/02/20/loadingcache-in-java-8-without-guava/

Отказ от ответственности: этот ответ не соответствует точно заданному OP, но может быть полезен в некоторых случаях, когда заголовок вопроса совпадает, когда число ключей ограничено и кэширование различных значений будет выгодным. Он не должен использоваться в противоположном случае с большим количеством ключей и тем же значением по умолчанию, так как это приведет к ненужной трате памяти.

Vadzim
источник
Не то, что спросил ОП: он не хочет никаких побочных эффектов на карте. Кроме того, сохранение значения по умолчанию для каждого отсутствующего ключа является бесполезной потерей памяти.
numéro6
@ numéro6, да, это не совсем соответствует тому, что спросил OP, но некоторые люди, прибегающие к поиску, все еще находят этот ответ полезным. Как уже упоминалось в других ответах, невозможно добиться именно того, что попросил ОП, не нарушая контракт карты. Другой обходной путь, не упомянутый здесь, состоит в том, чтобы использовать другую абстракцию вместо Map .
Вадим
Можно добиться именно того, что попросил ОП, не нарушая контракт карты. Никакого обходного пути не требуется, просто использование getOrDefault является правильным (наиболее обновленным) способом, computeIfAbsent - неправильным путем: вы потеряете время на вызов функции mappingFunction и памяти, сохраняя результат (оба для каждого отсутствующего ключа). Я не вижу веской причины сделать это вместо getOrDefault. То, что я описываю, является точной причиной, почему в контракте Map есть два различных метода: есть два различных варианта использования, которые не следует путать (некоторые из них мне пришлось исправить на работе). Этот ответ распространяет путаницу.
numéro6
14

Разве вы не можете просто создать статический метод, который делает именно это?

private static <K, V> V getOrDefault(Map<K,V> map, K key, V defaultValue) {
    return map.containsKey(key) ? map.get(key) : defaultValue;
}
Шервин Аскари
источник
где хранить статику?
Пейсер
10

Вы можете просто создать новый класс, который наследует HashMap, и добавить метод getDefault. Вот пример кода:

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

        return defaultValue;
    }
}

Я думаю, что вам не следует переопределять метод get (K key) в вашей реализации из-за причин, указанных Эдом Стаубом в его комментарии, и из-за того, что вы нарушите контракт интерфейса Map (это может привести к некоторому трудному для поиска ошибок).

Иван Мушкетик
источник
4
У вас есть смысл не переопределять getметод. С другой стороны - ваше решение не позволяет использовать класс через интерфейс, что часто может иметь место.
Кшиштоф Яблоньский
5

Использование:

myHashMap.getOrDefault(key, defaultValue);
Диего Алехандро
источник
3

Это делает это по умолчанию. Это возвращается null.

mrkhrts
источник
@Larry, мне кажется немного глупым выделять подкласс HashMapтолько для этой функциональности, когда nullвсе в порядке.
mrkhrts
15
Это не хорошо, если вы используете NullObjectшаблон или не хотите разбрасывать нулевые проверки по всему коду - желание, которое я полностью понимаю.
Дэйв Ньютон
3

На яве 8+

Map.getOrDefault(Object key,V defaultValue)
Эдуардо
источник
1

Я нашел LazyMap весьма полезным.

Когда метод get (Object) вызывается с ключом, который не существует на карте, фабрика используется для создания объекта. Созданный объект будет добавлен на карту с использованием запрошенного ключа.

Это позволяет вам сделать что-то вроде этого:

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

Вызов getсоздает значение по умолчанию для данного ключа. Вы указываете, как создать значение по умолчанию с заводским аргументом в LazyMap.lazyMap(map, factory). В приведенном выше примере карта инициализируется как новая AtomicIntegerсо значением 0.

Бенедикт Коппель
источник
Это имеет преимущество перед принятым ответом в том, что значение по умолчанию создается фабрикой. Что если мое значение по умолчанию List<String>- использование принятого ответа, я рискну использовать один и тот же список для каждого нового ключа, а не, скажем, a new ArrayList<String>()с завода.
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);
}

Пример использования:

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
источник
0

Мне нужно было прочитать результаты, полученные с сервера в JSON, где я не мог гарантировать наличие полей. Я использую класс org.json.simple.JSONObject, который является производным от HashMap. Вот некоторые вспомогательные функции, которые я использовал:

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
источник