diff --git a/chapter_005/src/main/java/ru/job4j/list/UnidirectionalLinkedList.java b/chapter_005/src/main/java/ru/job4j/list/UnidirectionalLinkedList.java index 3037330..c8c4bcb 100644 --- a/chapter_005/src/main/java/ru/job4j/list/UnidirectionalLinkedList.java +++ b/chapter_005/src/main/java/ru/job4j/list/UnidirectionalLinkedList.java @@ -1,6 +1,8 @@ package ru.job4j.list; /** + * Реализация односвязного списка. + * * @author John Ivanov (johnivo@mail.ru) * @since 09.04.2019 */ diff --git a/chapter_005/src/main/java/ru/job4j/map/SimpleHashMap.java b/chapter_005/src/main/java/ru/job4j/map/SimpleHashMap.java new file mode 100644 index 0000000..c0d3634 --- /dev/null +++ b/chapter_005/src/main/java/ru/job4j/map/SimpleHashMap.java @@ -0,0 +1,259 @@ +package ru.job4j.map; + +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Objects; + +/** + * Ассоциативный массив на базе хэш-таблицы. + * + * @author John Ivanov (johnivo@mail.ru) + * @since 20.04.2019 + */ +public class SimpleHashMap implements Iterable { + + /** + * Начальная вместимость по-умолчанию (1 << 4 = 16). + */ + private static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; + + /** + * Коэффициент загрузки, при его достижении размер массива удваивается. + */ + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + + /** + * Массив для хранения пар ключ-значение. + */ + private Node[] table; + + /** + * Количество добавленных элементов в массив. + */ + private int size; + + /** + * Счётчик структурных изменений (для реализации fail-fast поведения итератора). + */ + private int modCount; + + /** + * Конструктор формирует пустую карту с начальной вместимостью по-умолчанию. + */ + public SimpleHashMap() { + this(DEFAULT_INITIAL_CAPACITY); + } + + /** + * Конструктор формирует пустую карту с заданной начальной вместимостью. + * + * @param initialCapacity начальная вместимость. + * @throws IllegalArgumentException при попытке задать некорректную вместимость. + */ + public SimpleHashMap(int initialCapacity) { + if (initialCapacity < 0 && initialCapacity % 2 != 0) { + throw new IllegalArgumentException("Illegal Capacity: " + initialCapacity); + } + this.resize(initialCapacity); + } + + /** + * Хеш-функция на основе хеш-кода ключа. + * Сдвигаем старшие разряды числа(начального хеш-кода ключа) вправо на 16 позиций (>>> 16) + * и выполняем операцию XOR (^ побитовое логическое или). + * Этим страхуемся от неудачной функции hashcode(). + * + * @param key ключ. + * @return hash значение хеш-функции. + */ + private int hash(K key) { + int hash = 0; + if (key != null) { + hash = key.hashCode(); + hash ^= hash >>> 16; + } + return hash; + } + + /** + * Вычисляет корзину/индекс ячейки/бакет в массиве, в которой будет храниться новый элемент. + * + * @param hash результат хеш-фукнции для нового элемента. + * @param length количество ячеек/размер массива. + * @return индекс ячейки. + */ + private int index(int hash, int length) { + return (length - 1) & hash; + } + + /** + * Добавляет в карту новый объект на основе заданной пары ключ-значение. + * + * @param key ключ. + * @param value значение. + * @return true. + */ + public boolean insert(K key, V value) { + boolean result = false; + int i = this.index(this.hash(key), this.table.length); + if (this.table[i] == null) { + this.table[i] = new Node<>(key, value); + this.size++; + this.modCount++; + result = true; + if (this.size >= DEFAULT_LOAD_FACTOR * this.table.length) { + resize(this.table.length << 1); + } + } else { + if (key != null && key.equals(this.table[i].key)) { + this.table[i] = new Node<>(key, value); + this.modCount++; + result = true; + } + } + return result; + } + + /** + * Создает новое хранилище заданной вместимости. + * Перемещает в него элементы из текущего, если они не null. + * + * @param newSize заданная вместимость. + * @return карта новой вместимости. + */ + private void resize(int newSize) { + Node[] newTable = (Node[]) new Node[newSize]; + if (this.table != null) { + for (Node node : this.table) { + if (node != null) { + int i = this.index(this.hash(node.key), newSize); + newTable[i] = node; + } + } + } + this.table = newTable; + } + + /** + * Возвращает значение по заданному по ключу. + * + * @param key ключ. + * @return value значение. + */ + public V get(K key) { + int i = this.index(this.hash(key), this.table.length); + Node node = this.table[i]; + return node != null && Objects.equals(key, node.key) ? node.value : null; + } + + /** + * Удаляет из карты объект по заданному по ключу. + * + * @param key ключ. + * @return true. + */ + public boolean delete(K key) { + boolean result = false; + int i = this.index(this.hash(key), this.table.length); + Node node = this.table[i]; + if (node != null && key.equals(node.key)) { + this.table[i] = null; + this.modCount++; + result = true; + } + return result; + } + + /** + * Вспомогательный метод, получает полный размер карты (с учетом пустых ячеек). + * + * @return размер карты (с учетом пустых ячеек). + */ + public int size() { + return this.table.length; + } + + /** + * Вспомогательный метод, получает размер заполненной части карты. + * + * @return размер карты. + */ + public int getSize() { + return this.size; + } + + /** + * Вспомогательный метод, получает множество пар ключ-значение. + * + * @return множество пар ключ-значение, в котором могут быть значения null. + */ + public Node[] entrySet() { + return this.table; + } + + /** + * Возвращает итератор для последовательного прохода по элементам карты. + * + * @return переопределенный итератор. + */ + @Override + public Iterator iterator() { + return new Iterator() { + + int expectedModCount = SimpleHashMap.this.modCount; + private int pointer; + private int counter; + + @Override + public boolean hasNext() { + for (int i = pointer; i < table.length; i++) { + if (table[i] != null) { + pointer = i; + break; + } + } + return counter < SimpleHashMap.this.size; + } + + @Override + public Node next() { + if (this.expectedModCount != SimpleHashMap.this.modCount) { + throw new ConcurrentModificationException(); + } + if (!this.hasNext()) { + throw new NoSuchElementException(); + } + counter++; + return table[pointer++]; + } + }; + } + + /** + * Внутренний класс для хранения данных в виде пар ключ-значение. + */ + private static class Node { + + private K key; + private V value; + + public Node(K key, V value) { + this.key = key; + this.value = value; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + @Override + public String toString() { + return "Node{" + "key=" + key + ", value=" + value + '}'; + } + } +} diff --git a/chapter_005/src/test/java/ru/job4j/map/SimpleHashMapTest.java b/chapter_005/src/test/java/ru/job4j/map/SimpleHashMapTest.java new file mode 100644 index 0000000..a23c163 --- /dev/null +++ b/chapter_005/src/test/java/ru/job4j/map/SimpleHashMapTest.java @@ -0,0 +1,139 @@ +package ru.job4j.map; + +import org.junit.Test; + +import java.util.*; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.*; + +/** + * @author John Ivanov (johnivo@mail.ru) + * @since 20.04.2019 + */ +public class SimpleHashMapTest { + + @Test + public void whenInsertedThenGetReturnsCorrectUser() { + SimpleHashMap map = new SimpleHashMap<>(); + Calendar currentTime = Calendar.getInstance(); + map.insert(0, new User("Bob", 2, currentTime)); + map.insert(1, new User("Ann", 3, currentTime)); + map.insert(2, new User("Nik", 1, currentTime)); + assertThat(map.get(0).getName(), is("Bob")); + assertThat(map.get(1).getChildren(), is(3)); + assertEquals(map.get(2).getBirthday().getTimeInMillis(), currentTime.getTimeInMillis()); + } + + @Test + public void whenInsertingByDuplicateKeyShouldReplacesValueAndReturnsTrue() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(0, "Bob"); + map.insert(1, "Ann"); + assertThat(map.insert(0, "Clark"), is(true)); + assertThat(map.insert(0, "Ju"), is(true)); + assertThat(map.get(0), is("Ju")); + } + + @Test + public void whenInvocationNullKeyThenResultsAreCorrect() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(0, "Bob"); + map.insert(1, "Ann"); + assertNull(map.get(null)); + map = new SimpleHashMap<>(); + assertTrue(map.insert(null, "Clark")); + assertThat(map.getSize(), is(1)); + assertEquals(map.get(null), "Clark"); + } + + @Test + public void whenDeleteElementThenGetReturnsNull() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(0, "Bob"); + map.insert(1, "Ann"); + map.insert(2, "Nik"); + assertTrue(map.delete(2)); + assertNull(map.get(2)); + assertTrue(map.insert(4, "Ups")); + assertTrue(map.delete(4)); + assertNull(map.get(4)); + } + + @Test + public void whenLoadDefaultIsReachedThenCapacityDoubles() { + SimpleHashMap newMap = new SimpleHashMap<>(4); + Calendar birthday = new GregorianCalendar(2018, 11, 28); + assertThat(newMap.size(), is(4)); + assertThat(newMap.getSize(), is(0)); + newMap.insert(3, new User("Nik2", 1, birthday)); + newMap.insert(4, new User("Nik3", 1, birthday)); + newMap.insert(5, new User("Nik4", 1, birthday)); + assertThat(newMap.size(), is(8)); + assertThat(newMap.getSize(), is(3)); + } + + @Test + public void iteratorHasNextBeforeAndAfterInvocation() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(5, "Bob"); + map.insert(10, "Ann"); + map.insert(2, "Nik"); + Iterator iterator = map.iterator(); + assertThat(iterator.hasNext(), is(true)); + assertThat(iterator.hasNext(), is(true)); + System.out.println(map.entrySet()[0]); + assertThat(iterator.next(), is(map.entrySet()[2])); + assertThat(iterator.next(), is(map.entrySet()[5])); + iterator.hasNext(); + assertThat(iterator.next(), is(map.entrySet()[10])); + assertThat(iterator.hasNext(), is(false)); + } + + @Test(expected = NoSuchElementException.class) + public void invocationNextWhenHasNotNextThrowsNSEE() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(0, "Bob"); + map.insert(1, "Ann"); + map.insert(2, "Nik"); + Iterator iterator = map.iterator(); + iterator.next(); + iterator.next(); + iterator.next(); + iterator.next(); + } + + @Test(expected = ConcurrentModificationException.class) + public void invocationNextAfterChangingInnerStateThrowsCME() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(0, "Bob"); + map.insert(1, "Ann"); + map.insert(2, "Nik"); + Iterator it = map.iterator(); + map.delete(0); + it.next(); + } + + @Test(expected = ConcurrentModificationException.class) + public void invocationNextAfterInsertingByDuplicateKeyShouldThrowsCME() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(0, "Bob"); + map.insert(1, "Ann"); + map.insert(2, "Nik"); + Iterator it = map.iterator(); + map.insert(1, "Ju"); + it.next(); + } + + @Test + public void whenInsertingUnconsecutiveKeysThenSecondCallNextShouldReturnExpectedValue() { + SimpleHashMap map = new SimpleHashMap<>(); + map.insert(1, "Bob"); + map.insert(10, "Ann"); + Iterator iterator = map.iterator(); + assertThat(iterator.next(), is(map.entrySet()[1])); + assertThat(iterator.next(), is(map.entrySet()[10])); + assertThat(iterator.hasNext(), is(false)); + } +} \ No newline at end of file