Использует избыточность java Map.containsKey () при использовании map.get ()

93

В течение некоторого времени я задавался вопросом, допустимо ли в рамках передовой практики воздерживаться от использования containsKey()метода java.util.Mapи вместо этого выполнять нулевую проверку результата из get().

Мое объяснение состоит в том, что кажется излишним выполнять поиск значения дважды - сначала для, containsKey()а затем еще раз для get().

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

Буду очень признателен за ваши комментарии.

Эрик Мэдсен
источник

Ответы:

112

Некоторым реализациям Map разрешено иметь нулевые значения, например HashMap, в этом случае, если он get(key)возвращается, nullэто не гарантирует, что на карте нет записи, связанной с этим ключом.

Итак, если вы хотите знать, есть ли на карте ключевой use Map.containsKey. Если вам просто нужно значение, сопоставленное с ключом, используйте Map.get(key). Если эта карта допускает значения NULL, то возвращаемое значение NULL не обязательно означает, что карта не содержит сопоставления для ключа; В таком случае Map.containsKeyбесполезен и повлияет на производительность. Более того, в случае одновременного доступа к карте (например ConcurrentHashMap) после того, как вы протестировали, Map.containsKey(key)есть вероятность, что запись будет удалена другим потоком перед вызовом Map.get(key).

Евгений Дорофеев
источник
8
Даже если установлено значение null, хотите ли вы относиться к нему иначе, чем к неустановленному ключу / значению? Если вам специально не нужно относиться к нему по-другому, вы можете просто использоватьget()
Питер Лоури
1
Если Mapэто так private, ваш класс может гарантировать, что a nullникогда не будет вставлен на карту. В этом случае вы можете использовать с get()последующей проверкой на нуль вместо containsKey(). В некоторых случаях это может быть более понятным и, возможно, немного более эффективным.
Raedwald 01
44

Я думаю, что довольно стандартно написать:

Object value = map.get(key);
if (value != null) {
    //do something with value
}

вместо того

if (map.containsKey(key)) {
    Object value = map.get(key);
    //do something with value
}

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

ассилий
источник
8

Как указал ассилий, это семантический вопрос. Как правило, Map.get (x) == null - это то, что вам нужно, но есть случаи, когда важно использовать containsKey.

Один из таких случаев - тайник. Однажды я работал над проблемой производительности в веб-приложении, которое часто запрашивало свою базу данных в поисках несуществующих сущностей. Когда я изучил код кеширования для этого компонента, я понял, что он запрашивает базу данных, если cache.get (key) == null. Если база данных вернула значение null (объект не найден), мы бы кэшировали этот ключ -> сопоставление null.

Переход на containsKey решил проблему, потому что сопоставление с нулевым значением действительно что-то значило. Отображение ключа на null имеет другое семантическое значение, чем несуществующий ключ.

Брэндон
источник
Интересно. Почему вы просто не добавили нулевую проверку перед кешированием значений?
Сакет
Это ничего не изменит. Дело в том, что привязка ключа к null означает, что «мы уже сделали это. Оно кэшировано. Значение равно null». Вместо того, чтобы вообще не содержать данный ключ, что означает: «Не знаю, не в кеше, нам может потребоваться проверить БД».
Брэндон
5
  • containsKeyс последующим a getявляется избыточным, только если мы знаем априори, что нулевые значения никогда не будут разрешены. Если нулевые значения недействительны, вызов containsKeyимеет нетривиальное снижение производительности и просто накладные расходы, как показано в тесте ниже.

  • OptionalИдиомы Java 8 - Optional.ofNullable(map.get(key)).ifPresentили Optional.ofNullable(map.get(key)).ifPresent- несут нетривиальные накладные расходы по сравнению с обычными нулевыми проверками.

  • A HashMapиспользует O(1)постоянный поиск в таблице, тогда как a TreeMapиспользует O(log(n))поиск. containsKeyСледует getидиомы гораздо медленнее при вызове на TreeMap.

Контрольные точки

См. Https://github.com/vkarun/enum-reverse-lookup-table-jmh

// t1
static Type lookupTreeMapNotContainsKeyThrowGet(int t) {
  if (!lookupT.containsKey(t))
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return lookupT.get(t);
}
// t2
static Type lookupTreeMapGetThrowIfNull(int t) {
  Type type = lookupT.get(t);
  if (type == null)
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return type;
}
// t3
static Type lookupTreeMapGetOptionalOrElseThrow(int t) {
  return Optional.ofNullable(lookupT.get(t)).orElseThrow(() -> new 
      IllegalStateException("Unknown Multihash type: " + t));
}
// h1
static Type lookupHashMapNotContainsKeyThrowGet(int t) {
  if (!lookupH.containsKey(t))
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return lookupH.get(t);
}
// h2
static Type lookupHashMapGetThrowIfNull(int t) {
  Type type = lookupH.get(t);
  if (type == null)
    throw new IllegalStateException("Unknown Multihash type: " + t);
  return type;
}
// h3
static Type lookupHashMapGetOptionalOrElseThrow(int t) {
  return Optional.ofNullable(lookupH.get(t)).orElseThrow(() -> new 
    IllegalStateException("Unknown Multihash type: " + t));
}
Тест (итерации) (lookupApproach) Режим Cnt Score Единицы ошибки

MultihashTypeLookupBenchmark.testLookup 1000 t1 avgt 9 33,438 ± 4,514 мкс / оп
MultihashTypeLookupBenchmark.testLookup 1000 t2 avgt 9 26,986 ± 0,405 us / op
MultihashTypeLookupBenchmark.testLookup 1000 t3 avgt 9 39,259 ± 1,306 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h1 avgt 9 18,954 ± 0,414 мкс / оп
MultihashTypeLookupBenchmark.testLookup 1000 h2 avgt 9 15,486 ± 0,395 мкс / оп
MultihashTypeLookupBenchmark.testLookup 1000 h3 avgt 9 16,780 ± 0,719 us / op

Ссылка на источник TreeMap

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java

Ссылка на исходный код HashMap

https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/HashMap.java

Венкат Карун Венугопалан
источник
3

Мы можем сделать ответ @assylias более читабельным с помощью Java8 Optional,

Optional.ofNullable(map.get(key)).ifPresent(value -> {
     //do something with value
};)
Раджа
источник
2

В Java, если вы проверите реализацию

public boolean containsKey(Object key) {
    return getNode(hash(key), key) != null;
}

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

оба используют getNode для получения соответствия, где и выполняется основная работа.

избыточность является контекстной, например, если у вас есть словарь, хранящийся в хеш-карте. Когда вы хотите узнать значение слова

делаю ...

if(dictionary.containsKey(word)) {
   return dictionary.get(word);
}

избыточно.

но если вы хотите проверить, действительно ли слово, на основе словаря. делаю ...

 return dictionary.get(word) != null;

над...

 return dictionary.containsKey(word);

избыточно.

Если вы проверяете реализацию HashSet , которая использует HashMap внутри, используйте метод containsKey в методе contains.

    public boolean contains(Object o) {
        return map.containsKey(o);
    }
asela38
источник