Общее·количество·просмотров·страницы

Java Dev Notes - разработка на Java (а также на JavaScript/Python/Flex и др), факты, события из АйТи

Показаны сообщения с ярлыком HashMap. Показать все сообщения
Показаны сообщения с ярлыком HashMap. Показать все сообщения

пятница, 20 мая 2011 г.

Устройство java.util.HashMap. Часть 4.

Рассмотрим, как объекты кладутся в хеш-таблицу и получаются из нее. Для начала рассмотрим функцию хеширования:
static int hash(int h) {
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}

Эта функция применяется к хеш-кодам ключей - она защищает от плохого хеширования.

Теперь рассмотрим функцию, которая получает индекс в массиве бакетов:

static int indexFor(int h, int length) {
return h & (length-1);
}

Т.к. длина массива бакетов - это степень двойки, то побитовое умножение позволяет нам отсечь старшие биты - таким образом мы избегаем операции получения остатка от деления.

Теперь рассмотрим функцию получения объекта по ключу:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}


Здесь все просто: для получения значения по null-ключу мы используем специальный метод: getForNullKey. Затем получаем индекс в массиве бакетов и проходимся по списку Entry в данной ячейке списка.

Рассмотрим метод getForNullKey:
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) return e.value;
}
return null;
}


Как видно из кода, мы просто проходимся по списку Entry в первой ячейке массива бакетов, до тех пор, пока не встретили Entry с нулевым ключом.

Теперь рассмотрим метод для записи объекта в хеш-таблицу:
public V put(K key, V value) {
if (key == null) return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
 
modCount++;
addEntry(hash, key, value, i);
return null;
}

Здесь для добавления объекта в null-ключом вызывается специальный метод - putForNullKey. Далее, просматривается список в массиве бакетов по данному хешу. Если в этом списке нет такого ключа, то для добавления объекта в этот список вызывается метод addEntry, заодно увеличивается счетчик modCount - который подсчитывает количество структурных модификаций.

Рассмотрим метод addEntry:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}

Этот метод добавляет новый объект в начало списка, и также увеличивает размер массива бакетов, если достигнуто пороговое значение.

Мы рассмотрели основные аспекты java.util.HashMap - все остальное можно легко посмотреть по исходникам.

Устройство java.util.HashMap. Часть 1.
Устройство java.util.HashMap. Часть 2.
Устройство java.util.HashMap. Часть 4.

Устройство java.util.HashMap. Часть 3.

Здесь мы рассмотрим варианты создания хеш-таблицы: ее конструкторы, включая и неявные конструкторы (т.е. clone).

public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
 
// Find a power of 2 >= initialCapacity
int capacity = 1;
while (capacity < initialCapacity)
capacity <<= 1;
 
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}


Здесь все довольно просто. Сначала мы проверяем аргументы на валидность в этом блоке кода:
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);


Затем находим ближайшую ступень двойки и записываем ее в capacity. Устанавливаем loadFactor и threshold = (int)(capacity * loadFactor), а также массив бакетов:
table = new Entry[capacity];
Затем вызываем метод init(), который является хуком для классов, наследующих от HashMap. В данной реализации это просто пустой метод.

Теперь рассмотрим конструктор без аргументов:

public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}


Здесь все то же самое, за исключением проверки аргументов. Это и логично, т.к. аргументов в конструкторе нет. Они могли бы вызвать конструктор с аргументами вот так:
public HashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
}


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

Теперь рассмотрим конструктор, который создает хеш-таюлицу по другой хеш-таблице:

public HashMap(Map<? extends K, ? extends V> m) {
this(Math.max((int) (m.size() / DEFAULT_LOAD_FACTOR) + 1,
DEFAULT_INITIAL_CAPACITY), DEFAULT_LOAD_FACTOR);
putAllForCreate(m);
}

Здесь также все просто - выбираются значения initialCapacity и loadFactor, после этого все элементы копируются из одной таблицы в другую, используя метод putAllForCreate. Этот же метод используется в методе clone.

А вот и метод clone:

public Object clone() {
HashMap<K,V> result = null;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// assert false;
}
result.table = new Entry[table.length];
result.entrySet = null;
result.modCount = 0;
result.size = 0;
result.init();
result.putAllForCreate(this);
 
return result;
}


В следующем посте мы рассмотрим как объекты получаются из и кладутся в таблицу, а также хеширование, изменение емкости и т.п.

Устройство java.util.HashMap. Часть 1.
Устройство java.util.HashMap. Часть 2.
Устройство java.util.HashMap. Часть 4.

Устройство java.util.HashMap. Часть 2.

Здесь мы рассмотрим поля класса java.util.HashMap.

Итак:

static final int DEFAULT_INITIAL_CAPACITY = 16;
Начальная емкость хеш-таблицы

static final int MAXIMUM_CAPACITY = 1 << 30;
Максимальная емкость хеш-таблицы - составляет 2^29

static final float DEFAULT_LOAD_FACTOR = 0.75f;
Фактор загрузки по умолчанию

transient Entry[] table;
Массив бакетов. Его длина всегда должна быть степенью двойки. Этот массив автоматически увеличивается вдвое, если фактор загрузки превышен.

transient int size;
Количество мэппингов, которые содержатся в данной хеш-таблице.

int threshold;
Пороговое значение, следующее значение размера количества мэппингов, по достижении которого размер хеш-таблицы (а точнее, размер массива бакетов) будет увеличен. Рассчитывается как (емкость * фактор_загрузки).

final float loadFactor;
Значение фактора загрузки для данной хеш-таблицы.

transient volatile int modCount;
Количество структурных модификаций, которые произошли над данной хеш-таблицей. Это поле используется итераторами, чтобы отследить, когда нужно будет выкинуть ConcurrentModificationException.

private transient Set<Map.Entry<K,V>> entrySet = null;
Представление хеш-таблицы в виде множества Entry.

Как видим, полей не очень много.

Устройство java.util.HashMap. Часть 1.
Устройство java.util.HashMap. Часть 3.
Устройство java.util.HashMap. Часть 4.

Устройство java.util.HashMap. Часть 1.

Одним из важнейших классом в Java Collections Framework является java.util.HashMap. Этой структура данных и ее реализация заслуживает отдельного разговора. В данном посте я хочу разобрать назначение HashMap и некоторые его особенности без погружения в исходный код. В основном, это будет перевод javadocs к классу java.util.HashMap.

java.util.HashMap - это реализация интерфейса java.util.Map на основе хеш-таюлицы. Эта реализация поддерживает также null-значения в качестве значений, а также и в качестве ключей. Не дается никаких гарантий по поводу внутреннего порядка данных. Особенно, не гарантируется, что порядок членов коллекции будет все время один и тот же.

Данная реализация обеспечивает постоянное время выполнения для базовых операций - get и put, т.е. время порядка O(1). При этом делается предположение, что хеш-функция равномерно распределяет элементы по бакетам (buckets). Бакет - элемент хеш-таблицы, который содержит список данных, положенных в хеш-таблицу. Остаток от деления хеш-кода каждого из этих данных на размер хеш-таблицы (точнее, на количество бакетов) один и тот же.

Итерация по хеш-таблице пропорционально количеству бакетов + их размер. Поэтому, если важна производительность итерации, важно не делать количество бакетов (т.е. емкость хеш-таблицы) очень большим.

Каждый экземпляр класса java.util.HashMap имеет два параметра для настройки производительности - начальная емкость (initial capacity) и фактор загрузки (load factor). Емкость хеш-таблицы - это количество бакетов в ней, а начальная емкость - это то количество бакетов, с которым хеш-таблица создается. Фактор загрузки определяет, насколько должна быть заполнена хеш-таблица перед тем, как автоматически увеличить ее емкость.

По умолчанию, начальная емкость равна 16, а фактор загрузки = 0.75. Начальная емкость должна быть всегда степенью двойки. Если в конструктор передается начальная емкость, которая не является степенью двойки, то устанавливается не эта неправильная емкость, а ближайшая степень двойки, которая больше этого числа. Это все легко посмотреть по исходникам. Степень двойки для емкости хеш-таблица позволяет некоторые оптимизации, которые положительно влияют на производительность хеш-таблицы.

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

Если несколько потоков используют java.util.HashMap одновременно, и хотя бы один из них структурно модифицирует хеш-таблицу, то такое использование должно быть внешне синхронизированно. Структурная модификация - это модификация, которая добавляет или удаляет один или несколько мэппингов из хеш-таблицы. Простое изменение значения, ассоциированного с ключом, не является структурной модификацией.

Если нет внешнего объекта, по которому синхронизируется такое совместное использование хеш-таблицы, то следует "обернуть" ее следующим методом:
Map m = Collections.synchronizedMap(new HashMap(...));

Итераторы, которые возвращаются в таком случае, имеют поведение fail-fast: если хеш-таблица структурно модифицируется иным образом, кроме как через методы remove или add этого итератора, то итератор выкидывает ConcurrentModificationException.

Устройство java.util.HashMap. Часть 2.
Устройство java.util.HashMap. Часть 3.
Устройство java.util.HashMap. Часть 4.

Постоянные читатели