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


151

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


Ви можете перевірити наявність ключа та повернути за замовчуванням. Або розширити клас та змінити поведінку. або навіть ви можете скористатись null - і поставте перевірку де завгодно.
SudhirJ

2
Це пов’язано / дублікат stackoverflow.com/questions/4833336/…, деякі інші варіанти обговорюються там.
Марк Батлер

3
Перегляньте рішення Java 8 для getOrDefault() посилання
Трей Джонн

Відповіді:


136

[Оновлення]

Як зазначають інші відповіді та коментатори, щодо Java 8 ви можете просто зателефонувати Map#getOrDefault(...).

[Оригінал]

Немає втілення карт, яке б це робило саме так, але було б тривіально реалізувати власну, розширивши 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
Щоб бути точним, ви, можливо, захочете скорегувати умову (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 () спричинить проблеми з тим, що очікує дотримання договору з картою. І нескінченний набір дійсних ключів, який міститьKey (), може призвести до поганої продуктивності, яку важко діагностувати. Хоча не сказати, що це може не служити певній меті.
Ед Штауб

Одна з проблем такого підходу полягає в тому, якщо значення є складним об'єктом. Карта <Рядок, список> #put не працюватиме, як очікувалося.
Eyal

Не працює на ConcurrentHashMap. Там ви повинні перевірити результат отримання null.
двеім

162

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


14
getOrDefaultдуже приємно, але вимагає визначення за замовчуванням кожного разу, коли доступ до карти. Визначення значення за замовчуванням один раз також матиме переваги читабельності при створенні статичної карти значень.
ач

3
Це банально реалізовувати себе. private static String get(Map map, String s) { return map.getOrDefault(s, "Error"); }
Джек Сатріано

@JackSatriano Так, але вам доведеться жорстко кодувати значення за замовчуванням або мати статичну змінну для нього.
Блпр

1
Дивіться нижче відповідь, використовуючи computeIfAbsent, краще, коли значення за замовчуванням дороге або має бути різним кожен раз.
hectorpal

Хоча це і гірше для пам'яті, і заощадить час на обчислення, лише якщо значення за замовчуванням дорого побудувати / обчислити. Якщо він дешевий, ви, ймовірно, виявите, що він працює гірше, оскільки його потрібно вставити на карту, а не просто повертати значення за замовчуванням. Звичайно ж, інший варіант.
Шпигун

73

Використовуйте Commons ' DefaultedMap, якщо ви не хочете винаходити колесо, наприклад,

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/

Відмова від відповідальності: Ця відповідь не відповідає саме тому, що запитувала ОП, але може бути корисною в деяких випадках, узгоджуючи заголовок питання, коли число ключів обмежене і кешування різних значень було б вигідним. Він не повинен використовуватися в протилежному випадку з великою кількістю клавіш і таким же значенням за замовчуванням, оскільки це зайве марно пам'ять.


Не те, що запитував ОП: він не хоче побічних ефектів на карті. Також зберігання значення за замовчуванням для кожного відсутнього ключа є марною втратою місця в пам'яті.
numéro6

@ numéro6, так, це не відповідає саме тому, що запитував ОП, але деякі люди, що гуглить, як і раніше вважають цю сторону корисною. Як було сказано в інших відповідях, неможливо досягти саме того, що запитувала ОП, не порушуючи договір на карту. Іншим способом вирішення, про яке не згадується, є використання іншої абстракції замість Map .
Вадим

Можна досягти саме того, що запитувала ОП, не порушуючи договір на карту. Не потрібно вирішувати, просто використання getOrDefault - це правильний (найновіший) спосіб, а computeIfAbsent - це неправильний спосіб: ви втратите час на виклик mappingFunction та пам'яті, зберігаючи результат (обидва для кожної відсутньої клавіші). Я не бачу жодної вагомої причини зробити це замість getOrDefault. Те, що я описую, - це точна причина, чому в договорі з картою є два чіткі методи: є два чіткі випадки використання, які не слід плутати (мені довелося виправити деякі на роботі). Ця відповідь поширила розгубленість.
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;
}

де зберігати статику?
Pacerier

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 (ключ) у вашій реалізації через причини, зазначені Едом Стаубом у своєму коментарі, і тому, що ви порушите договір інтерфейсу Map (це може призвести до важкого пошуку клопи).


4
У вас є сенс не перекривати getметод. З іншого боку - ваше рішення не дозволяє використовувати клас через інтерфейс, що часто буває так.
Krzysztof Jabłoński


3

Це робиться за замовчуванням. Це повертається null.


@Larry, мені здається трохи нерозумним підклас HashMapсаме за цю функціональність, коли nullце прекрасно.
mrkhrts

15
Це не добре, якщо ви використовуєте NullObjectшаблон, або не хочете розповсюджувати нульові перевірки по всьому коду - бажання, яке я повністю розумію.
Дейв Ньютон


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

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.