Введение в класс Java.util.Hashtable

1. Обзор

  • Hashtable - самая старая реализация структуры данных хеш-таблицы в Java. ** HashMap - вторая реализация, который был введен в JDK 1.2.

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

2. Когда использовать Hashtable

Допустим, у нас есть словарь, где каждое слово имеет свое определение.

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

Следовательно, Hashtable (или HashMap ) имеет смысл. Слова будут ключами в Hashtable , поскольку они должны быть уникальными. Определения, с другой стороны, будут значениями.

3. Пример использования

Давайте продолжим с примером словаря. Мы будем моделировать Word в качестве ключа:

public class Word {
    private String name;

    public Word(String name) {
        this.name = name;
    }

   //...
}

Допустим, значения Strings . Теперь мы можем создать Hashtable :

Hashtable<Word, String> table = new Hashtable<>();

Во-первых, давайте добавим запись:

Word word = new Word("cat");
table.put(word, "an animal");

Также для получения записи:

String definition = table.get(word);

Наконец, давайте удалим запись:

definition = table.remove(word);

В классе есть еще много методов, и некоторые из них мы опишем позже.

Но сначала давайте поговорим о некоторых требованиях к ключевому объекту.

4. Важность hashCode ()

  • Для использования в качестве ключа в Hashtable объект не должен нарушать ссылку:/java-hashcode[ hashCode () contract.]** Короче говоря, равные объекты должны возвращать один и тот же код. Чтобы понять, почему давайте посмотрим, как организована хеш-таблица.

Hashtable использует массив. Каждая позиция в массиве является «корзиной», которая может быть либо нулевой, либо содержать одну или несколько пар ключ-значение. Индекс каждой пары рассчитывается.

Но почему бы не хранить элементы последовательно, добавляя новые элементы в конец массива?

Дело в том, что поиск элемента по индексу происходит намного быстрее, чем последовательная итерация по элементам со сравнением. Следовательно, нам нужна функция, которая отображает ключи на индексы.

4.1. Таблица прямых адресов

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

index(k)=k,
where k is a key

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

Но у нас есть две проблемы:

  • Во-первых, наши ключи не целые, а Word объекты

Во-вторых, если бы они были целыми числами, никто бы не гарантировал, что они были маленькими.

Представьте себе, что ключи - это 1, 2 и 1000000. У нас будет большой массив размером 1000000, содержащий всего три элемента, а остальное будет потрачено впустую.

Метод hashCode () решает первую проблему.

Логика для манипулирования данными в Hashtable решает вторую проблему.

Давайте обсудим это подробно.

4.2. hashCode () Метод

Любой объект Java наследует метод hashCode () , который возвращает значение int . Это значение вычисляется из адреса внутренней памяти объекта. По умолчанию hashCode () возвращает разные целые числа для разных объектов.

Таким образом любой ключевой объект может быть преобразован в целое число с помощью hashCode () .

Но это целое число может быть большим.

4.3. Уменьшение диапазона

Методы get () , put () и remove () содержат код, который решает вторую проблему - уменьшение диапазона возможных целых чисел.

Формула вычисляет индекс для ключа:

int index = (hash & 0x7FFFFFFF) % tab.length;

Где tab.length - размер массива, а hash - число, возвращаемое методом hashCode () ключа.

Как мы видим, index является напоминанием о делении hash на размер массива . Обратите внимание, что одинаковые хеш-коды дают одинаковый индекс.

4.4. Коллизии

Кроме того, даже разные хеш-коды могут давать один и тот же индекс . Мы называем это столкновением. Для разрешения коллизий Hashtable хранит LinkedList пар ключ-значение.

Такая структура данных называется хеш-таблицей с цепочкой.

4.5. Коэффициент нагрузки

Нетрудно догадаться, что столкновения замедляют работу с элементами.

Чтобы получить запись, недостаточно знать ее индекс, но нам нужно просмотреть список и провести сравнение с каждым элементом.

Поэтому важно уменьшить количество столкновений. Чем больше массив, тем меньше вероятность столкновения. Коэффициент загрузки определяет баланс между размером массива и производительностью. По умолчанию он равен 0,75, что означает, что размер массива удваивается, когда 75% сегментов становятся не пустыми. Эта операция выполняется методом rehash () .

Но вернемся к ключам.

4.6. Переопределение equals () и hashCode ()

Когда мы помещаем запись в Hashtable и извлекаем ее из нее, мы ожидаем, что значение можно получить не только с тем же экземпляром ключа, но и с равным ключом:

Word word = new Word("cat");
table.put(word, "an animal");
String extracted = table.get(new Word("cat"));
  • Чтобы установить правила равенства, мы переопределяем метод equals () ключа: **

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Word))
        return false;

    Word word = (Word) o;
    return word.getName().equals(this.name);
}

Но если мы не переопределим hashCode () при переопределении equals () , тогда два одинаковых ключа могут оказаться в разных сегментах, потому что Hashtable вычисляет индекс ключа, используя его хеш-код.

Давайте внимательно посмотрим на приведенный выше пример. Что произойдет, если мы не переопределим hashCode () ?

  • Здесь задействованы два экземпляра Word - первый для

запись и вторая для получения записи. Хотя эти экземпляры равны, их метод hashCode () возвращает разные числа ** Индекс для каждого ключа рассчитывается по формуле из раздела 4.3.

Согласно этой формуле разные хеш-коды могут давать разные индексы ** Это означает, что мы помещаем запись в одно ведро, а затем пытаемся получить

это из другого ведра. Такая логика ломает Hashtable

  • Равные ключи должны возвращать одинаковые хеш-коды, поэтому мы переопределяем метод hashCode () : **

public int hashCode() {
    return name.hashCode();
}

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

Также обратите внимание, что нам не нужны ключи типа String , Integer , Long или другого типа оболочки. Методы equal () и hashCode () уже переопределены в классах-оболочках.

5. Итерация Hashtables

Есть несколько способов перебора __Hashtables. __В этом разделе хорошо поговорите о них и объясните некоторые последствия.

5.1. Fail Fast: Iteration

Отказоустойчивая итерация означает, что если Hashtable изменен после того, как его _Iterator создан, то ConcurrentModificationException_ будет выброшено. Давайте продемонстрируем это.

Сначала мы создадим Hashtable и добавим в него записи:

Hashtable<Word, String> table = new Hashtable<Word, String>();
table.put(new Word("cat"), "an animal");
table.put(new Word("dog"), "another animal");

Во-вторых, мы создадим Iterator :

Iterator<Word> it = table.keySet().iterator();

И в-третьих, мы изменим таблицу:

table.remove(new Word("dog"));

Теперь, если мы попробуем перебрать таблицу, мы получим ConcurrentModificationException :

while (it.hasNext()) {
    Word key = it.next();
}
java.util.ConcurrentModificationException
    at java.util.Hashtable$Enumerator.next(Hashtable.java:1378)

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

5.2. Не сбой быстро: Enumeration

Enumeration в Hashtable не сбой быстро. Давайте посмотрим на пример.

Сначала давайте создадим Hashtable и добавим в него записи:

Hashtable<Word, String> table = new Hashtable<Word, String>();
table.put(new Word("1"), "one");
table.put(new Word("2"), "two");

Во-вторых, давайте создадим Enumeration :

Enumeration<Word> enumKey = table.keys();

В-третьих, давайте изменим таблицу:

table.remove(new Word("1"));

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

while (enumKey.hasMoreElements()) {
    Word key = enumKey.nextElement();
}

5.3. Непредсказуемый порядок итераций

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

Это понятно, так как он рассчитывает каждый индекс, используя хэш-код ключа. Более того, время от времени происходит перефразировка, изменяя порядок структуры данных.

Поэтому давайте добавим несколько записей и проверим вывод:

Hashtable<Word, String> table = new Hashtable<Word, String>();
    table.put(new Word("1"), "one");
    table.put(new Word("2"), "two");
   //...
    table.put(new Word("8"), "eight");

    Iterator<Map.Entry<Word, String>> it = table.entrySet().iterator();
    while (it.hasNext()) {
        Map.Entry<Word, String> entry = it.next();
       //...
    }
}
five
four
three
two
one
eight
seven

6. Hashtable vs. HashMap

Hashtable и HashMap предоставляют очень похожую функциональность.

Оба они обеспечивают:

  • Быстрая итерация

  • Непредсказуемый порядок итераций

Но есть и некоторые отличия:

  • HashMap не предоставляет Enumeration, в то время как Hashtable предоставляет

не быстрое Enumeration ** Hashtable не разрешает null ключи и null значения, в то время как

HashMap разрешить один null ключ и любое количество null значений ** Методы Hashtable are синхронизируются, а методы HashMaps

не

7. Hashtable API в Java 8

Java 8 представила новые методы, которые помогают сделать наш код чище. В частности, мы можем избавиться от некоторых if блоков. Давайте продемонстрируем это.

7.1. getOrDefault ()

Допустим, нам нужно получить определение слова «собака __ » и присвоить его переменной, если оно находится в таблице. В противном случае присвойте переменной «not found».

До Java 8:

Word key = new Word("dog");
String definition;

if (table.containsKey(key)) {
     definition = table.get(key);
} else {
     definition = "not found";
}

После Java 8:

definition = table.getOrDefault(key, "not found");

7.2. putIfAbsent ()

Допустим, нам нужно добавить слово «кот _ только в том случае, если его еще нет в словаре.

До Java 8:

if (!table.containsKey(new Word("cat"))) {
    table.put(new Word("cat"), definition);
}

После Java 8:

table.putIfAbsent(new Word("cat"), definition);

7.3. boolean remove ()

Допустим, нам нужно убрать слово «кошка», но только если оно определено как «животное».

До Java 8:

if (table.get(new Word("cat")).equals("an animal")) {
    table.remove(new Word("cat"));
}

После Java 8:

boolean result = table.remove(new Word("cat"), "an animal");

Наконец, в то время как старый метод remove () возвращает значение, новый метод возвращает boolean .

7.4. replace ()

Допустим, нам нужно заменить определение «кошка», но только если его старое определение - «маленькое домашнее хищное млекопитающее».

До Java 8:

if (table.containsKey(new Word("cat"))
    && table.get(new Word("cat")).equals("a small domesticated carnivorous mammal")) {
    table.put(new Word("cat"), definition);
}

После Java 8:

table.replace(new Word("cat"), "a small domesticated carnivorous mammal", definition);

7.5. computeIfAbsent ()

Этот метод похож на putIfabsent () . Но putIfabsent () принимает значение напрямую, а computeIfAbsent () принимает функцию отображения. Он вычисляет значение только после проверки ключа, и это более эффективно, особенно если значение трудно получить.

table.computeIfAbsent(new Word("cat"), key -> "an animal");

Следовательно, приведенная выше строка эквивалентна:

if (!table.containsKey(cat)) {
    String definition = "an animal";//note that calculations take place inside if block
    table.put(new Word("cat"), definition);
}

7.6. computeIfPresent ()

Этот метод похож на метод replace () . Но, опять же, replace () принимает значение напрямую, а computeIfPresent () принимает функцию отображения. Он вычисляет значение внутри блока if , поэтому он более эффективен.

Допустим, нам нужно изменить определение:

table.computeIfPresent(cat, (key, value) -> key.getName() + " - " + value);

Следовательно, приведенная выше строка эквивалентна:

if (table.containsKey(cat)) {
    String concatination=cat.getName() + " - " + table.get(cat);
    table.put(cat, concatination);
}

7.7. compute ()

Теперь мы решим еще одну задачу. Допустим, у нас есть массив String , где элементы не являются уникальными. Также давайте посчитаем, сколько вхождений String мы можем получить в массиве. Вот массив:

String[]animals = { "cat", "dog", "dog", "cat", "bird", "mouse", "mouse" };

Также мы хотим создать Hashtable , который содержит животное в качестве ключа и количество его вхождений в качестве значения.

Вот решение:

Hashtable<String, Integer> table = new Hashtable<String, Integer>();

for (String animal : animals) {
    table.compute(animal,
        (key, value) -> (value == null ? 1 : value + 1));
}

Наконец, давайте удостоверимся, что таблица содержит двух кошек, двух собак, одну птицу и двух мышей:

assertThat(table.values(), hasItems(2, 2, 2, 1));

7.8. merge ()

Есть еще один способ решения вышеуказанной задачи:

for (String animal : animals) {
    table.merge(animal, 1, (oldValue, value) -> (oldValue + value));
}

Второй аргумент, 1 , является значением, которое отображается на ключ, если ключ еще не находится в таблице. Если ключ уже находится в таблице, мы рассчитываем его как oldValue 1 .

7.9. для каждого()

Это новый способ перебора записей. Давайте распечатать все записи:

table.forEach((k, v) -> System.out.println(k.getName() + " - " + v)

7.10. заменить все()

Кроме того, мы можем заменить все значения без итерации:

table.replaceAll((k, v) -> k.getName() + " - " + v);

8. Заключение

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

Кроме того, мы рассмотрели, что такое коллизии и каков коэффициент загрузки, в Hashtable. Также мы узнали, почему следует переопределять equals () и hashCode () для ключевых объектов.

Наконец, мы поговорили о свойствах Hashtable и API, специфичных для Java 8.

Как обычно, полный исходный код доступен on Github .