< Сильный > Вопрос

Как метод putIfAbsent в HashMap может выполнять условную операцию put быстрее, чем предыдущий вызов containsKey (x)?

Например, если вы не использовали putIfAbsent, вы можете использовать:

 if(!map.containsKey(x)){ 
   map.put(x,someValue); 
}

Раньше я думал, что putIfAbsent - это удобный метод для вызова containsKey с последующим помещением в HashMap. Но после запуска теста putIfAbsent значительно быстрее, чем использование containsKey, за которым следует Put. Я посмотрел исходный код java.util, чтобы попытаться понять, как это возможно, но мне это слишком загадочно, чтобы понять. Кто-нибудь знает внутренне, как putIfAbsent, кажется, работает в лучшей временной сложности? Это мое предположение, основанное на выполнении нескольких тестов кода, в которых мой код работал на 50% быстрее при использовании putIfAbsent. Кажется, можно избежать вызова get (), но как?

< Сильный > Пример

if(!map.containsKey(x)){
     map.put(x,someValue);
}

< Сильный > VS

map.putIfAbsent(x,somevalue)

Исходный код Java для Hashmap.putIfAbsent

@Override
public V putIfAbsent(K key, V value) {
    return putVal(hash(key), key, value, true, true);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}
9
Usman Mutawakil 26 Сен 2018 в 09:08

2 ответа

Лучший ответ

Реализация HashMap putIfAbsent выполняет поиск ключа только один раз, и, если ключ не находит, помещает значение в соответствующую ячейку (которая уже была обнаружена). Вот что делает putVal.

С другой стороны, использование map.containsKey(x), за которым следует map.put(x,someValue), выполняет два поиска ключа в Map, что занимает больше времени.

Обратите внимание, что put также вызывает putVal (put вызывает putVal(hash(key), key, value, false, true), а putIfAbsent вызывает putVal(hash(key), key, value, true, true)), поэтому putIfAbsent имеет ту же производительность как вызов только put, что быстрее, чем вызов как containsKey, так и put.

11
Eran 26 Сен 2018 в 06:17

См. Ответ Эрана ... Я также хотел бы ответить на него более кратко. put и putIfAbsent используют один и тот же вспомогательный метод putVal. Но клиенты, использующие put, не могут воспользоваться его многочисленными параметрами, которые допускают поведение "положено, если присутствует". Открытый метод putIfAbsent раскрывает это. Таким образом, использование putIfAbsent имеет ту же основную временную сложность, что и put, которые вы уже собираетесь использовать вместе с containsKey. Тогда использование containsKey становится пустой тратой.

Таким образом, суть в том, что закрытая функция putVal используется как put, так и putIfAbsent.

1
Usman Mutawakil 26 Сен 2018 в 06:39