Почему внутренняя реализация HashSet создает фиктивные объекты для вставки как значений в HashMap вместо вставки нулей?

HashSet реализуется с использованием HashMap, и когда мы добавляем что-либо, скажем, e1 в HashSet, внутри он добавляет (e1, новый Object()) в HashMap, если e1 не присутствовал в наборе. Мой вопрос заключается в том, почему они вставляют новый Object(), когда они могут вставлены как (e1, null), что является более оптимизированным подходом, так как новые объекты не создаются. Есть ли недостаток в вставке нулей здесь?

Ответ 1

HashSet не добавляет новый Object каждый раз, когда новый ключ put на карту. Он использует Object, но каждый раз использует один и тот же Object. Это значение называется PRESENT в исходном коде HashSet.

Метод add вызывает put(key, PRESENT) на внутренней HashMap. Метод remove вызывает remove(key) на внутренней HashMap, но он должен возвращать boolean указывающее, присутствовал ли ключ. Если значение null было сохранено как значение, тогда HashSet необходимо сначала вызвать containsKey, а затем remove, чтобы определить, присутствует ли ключ - дополнительные служебные данные. Здесь есть только накладные расходы на память одного Object, что является весьма минимальным.

Ответ 2

Я просто посмотрел на исходный код и увидел этот код

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public boolean remove(Object o) {
    return map.remove(o)==PRESENT;
}

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

Ответ 3

Например, если вы передаете объект HashSet конструктору ConcurrentSkipListSet, он не может содержать никаких нулевых значений.