Використовує java Map.containsKey () зайвим при використанні map.get ()


93

Я деякий час гадав, чи допустимо в рамках найкращої практики утримуватися від використання containsKey()методу java.util.Mapі замість цього робити нульову перевірку результату з get().

Моє обґрунтування полягає в тому, що здається зайвим робити пошук значень двічі - спочатку для, containsKey()а потім знову для get().

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

Буду дуже вдячний за ваші коментарі.

Відповіді:


112

Деякі реалізації Карти можуть мати нульові значення, наприклад HashMap, у цьому випадку, якщо він get(key)повертається, nullце не гарантує, що на карті немає жодного запису, пов'язаного з цим ключем.

Тож якщо ви хочете знати, чи містить карта ключове використання Map.containsKey. Якщо вам просто потрібне значення, зіставлене з використанням ключа Map.get(key). Якщо ця карта дозволяє нульові значення, то повернене значення null не обов'язково вказує на те, що карта не містить відображення ключа; У такому випадку Map.containsKeyмарно і вплине на продуктивність. Більше того, у разі одночасного доступу до карти (наприклад ConcurrentHashMap), після тестування Map.containsKey(key)існує ймовірність того, що запис буде видалено іншим потоком перед тим, як зателефонувати Map.get(key).


8
Навіть якщо для значення встановлено значення null, чи хочете ви по-різному ставитися до ключа / значення, який не встановлено? Якщо вам не потрібно спеціально ставитися до цього по-іншому, ви можете просто використовуватиget()
Пітер Лорі

1
Якщо ваше Mapє private, ваш клас може бути в змозі гарантувати , nullніколи не вставляються в карті. У такому випадку ви можете використовувати, get()а потім перевірку на наявність null замість containsKey(). Це може бути зрозумілішим і, можливо, трохи ефективнішим, в деяких випадках.
Raedwald,

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 (ключ) == null. Якби база даних повернула null (сутність не знайдена), ми б кешували цей ключ -> null mapping.

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


Цікаво. Чому ви просто не додали нульову перевірку перед кешуванням значень?
Сакет

Це нічого не змінило б. Справа в тому , що ключ відображення до засобів нульових «ми вже зробили це. Це кешуються. Значення є нульовий». Натомість, що взагалі не містить заданий ключ, що означає "Не знаю, не в кеші нам може знадобитися перевірити БД".
Брендон,

5
  • containsKeyа потім a getє зайвим, лише якщо ми знаємо apriori, що нульові значення ніколи не будуть дозволені. Якщо нульові значення не є дійсними, виклик 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 Оцінка одиниць помилок

MultihashTypeLookupBenchmark.testLookup 1000 t1 avgt 9 33,438 ± 4,514 us / op
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 год1 сер. 9 18,954 ± 0,414 us / op
MultihashTypeLookupBenchmark.testLookup 1000 h2 avgt 9 15.486 ± 0.395 us / op
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);
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.