内容简介:上一篇文章笔者解读了 HashMap 的源码,正好趁热打铁,今天笔者抽了些时间通过 TDD 实现了一个精简版的 HashMap,经笔者测试,正常情况下效率略微逊于 HashMap。其中最难的应属红黑树,真的是极其复杂,笔者用了一个小时还没能理解其中要领,索性使用链表替代了,等有时间再静下心来把未完成的任务消灭掉。理解问题,Tasking,TDD(包含重构),这是笔者最近一直在遵守的规则,希望可以给您给来一点感悟。
上一篇文章笔者解读了 HashMap 的源码,正好趁热打铁,今天笔者抽了些时间通过 TDD 实现了一个精简版的 HashMap,经笔者测试,正常情况下效率略微逊于 HashMap。
预设计
public class SimpleHashMap<K, V> {
public V put(K key, V value);
public V get(K key);
public V remove(K key);
public boolean containsKey(K key);
public int size();
public Iterator<V> values();
public void forEach(Consumer<? super K> action);
}
复制代码
Tasking
- 无参构建 SimpleHashMap
- 构造函数初始化 initial capacity
- 构造函数初始化 initial capacity 和 load factor
- initial capacity 默认使用 16
- load factor 默认使用 0.75f
- 初始化的 resize 门槛为 initial capacity * load factor
- 再次 resize 门槛为 threshold = threshold << 1
- 增加 put 接口
- 计算 hash 值
- 增加 hash table 用于保存数据节点.
- 如果 hash table 的容量为 0 或者 hash table 的容量超过门槛,则设置新的 resize 门槛,并扩容和 rehash。
- hash table 的下标为 hash & (capacity -1)
- 扩容时需要把旧的 hash table 的数据转移到新的 hash table
- 转移数据到新的 hash table 之前需要 rehash,rehash = entry.hash & (new_capacity -1)
- 如果 hash 冲突,使用链表存储
- 如果同一个 hash 冲突超过 8 次,使用红黑树存储
- 增加 size 接口
- 增加全局的 size 成员变量.
- put 接口调用成功,则 size += 1.
- remove 接口调用成功,则 size -= 1.
- 考虑链表
- 考虑红黑树
- 增加 containsKey 接口
- 通过 key 计算 hash
- 通过 hash 计算 index
- 通过 index 检索 key,检索到return true,否则 return false,
- 考虑 hash table 为 null.
- 考虑链表
- 考虑红黑树
- 增加 get 接口
- 通过 key 计算 hash
- 通过 hash 计算 index
- 通过 index 检索 bucket
- 如果 bucket 存在多个数据节点,则需要判断 key 的值和引用是否相等.
- 如果相等返回对应的 value,否则返回 null.
- 考虑链表
- 考虑红黑树
- 增加 remove 接口
- 通过 key 计算 hash
- 通过 hash 计算 index
- 通过 index 检索 bucket
- 如果相等则将对应的 bucket 置 null,并返回对应的 value,否则返回 null,
- 考虑链表
- 考虑红黑树
- 增加 values 接口
- 每次 put 成功时保存 list 中到
- 每次 put 替换成功时,需要替换 list 中对应的 value
- 每次 remove 成功时从 list 中到删除
- 考虑链表
- 考虑红黑树
- 增加 forEach 接口
- 遍历 hash table
- 如果存在 bucket,则通过 action.apply(key)
- 考虑链表
- 考虑红黑树
- 增加 fail-fast
- 增加 modCount 成员变量用于统计变更次数
- 迭代前后需要验证 modCount 前后是否一致
- 如果 modCount 前后是否一致需要抛出 ConcurrentModificationException.
- 增加 rb tree 保存 hash 冲突超过 8 次的数据节点.
测试覆盖率
测试代码
**
* @author lyning
*/
public class SimpleHashMapTest {
private SimpleHashMap<Integer, Integer> map;
@BeforeEach
public void setUp() throws Exception {
// given
this.map = new SimpleHashMap<>();
}
/************ size test start **********/
@Test
@DisplayName("given empty entries" +
"when call size() " +
"then return 0")
public void size1() {
// when
int size = map.size();
// then
assertThat(size).isZero();
}
@Test
@DisplayName("given multiple entries(contains duplicate key) " +
"when call size() " +
"then return correct size")
public void size2() {
// given
SimpleHashMap<Integer, Integer> map = new SimpleHashMap<>();
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(3, 4);
map.put(3, 5);
map.put(4, 4);
map.put(5, 5);
map.remove(1);
map.remove(2);
// when
int size = map.size();
// then
assertThat(size).isEqualTo(3);
}
@Test
@DisplayName("given multiple entries(hash conflict) " +
"when call size() " +
"then return correct size")
public void size3() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
map.remove(new HashConflict(5));
map.remove(new HashConflict(3));
// when
int size = map.size();
// then
assertThat(size).isEqualTo(3);
}
/************ size test end **********/
/************ put test start **********/
@Test
@DisplayName("given empty entries " +
"when put one entry " +
"then return size 1")
public void put1() {
// when
map.put(1, 1);
// then
assertThat(map.size()).isOne();
}
@Test
@DisplayName("given empty entries " +
"when put two entries(duplicate key) " +
"then return size 1")
public void put2() {
// when
map.put(1, 1);
map.put(1, 2);
// then
assertThat(map.size()).isEqualTo(1);
}
@Test
@DisplayName("given empty entries " +
"when put three entries " +
"then return size 3")
public void put3() {
// when
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
// then
assertThat(map.size()).isEqualTo(3);
}
@Test
@DisplayName("should return value " +
"when call put")
public void put4() {
// when
Integer value = map.put(1, 1);
// then
assertThat(value).isEqualTo(1);
}
@Test
@DisplayName("given empty entries " +
"when put multiples entries(hash conflict) " +
"then")
public void put5() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
// when
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(3), 4);
map.put(new HashConflict(3), 5);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
// then
assertThat(Lists.newArrayList(map.values())).isEqualTo(Lists.list(1, 2, 5, 4, 5));
}
@Test
@DisplayName("should auto grow " +
"when capacity exceed threshold")
public void put6() {
// given default threshold = 8
// when
for (int i = 1; i <= 20; i++) {
map.put(i, i);
}
// then
assertThat(map.size()).isEqualTo(20);
assertThat(map.get(20)).isEqualTo(20);
}
/************ put test end **********/
/************ get test start **********/
@Test
@DisplayName("given empty entries" +
"when get by null key" +
"then return null")
public void get1() {
// when
Integer value = map.get(null);
// then
assertThat(value).isNull();
}
@Test
@DisplayName("given empty entries" +
"when get value by not exist key" +
"then return null")
public void get2() {
// when
Integer value = map.get(2);
// then
assertThat(value).isNull();
}
@Test
@DisplayName("given entry" +
"when get value by not exist key" +
"then return null")
public void get3() {
// given
map.put(1, 1);
// when
Integer value = map.get(2);
// then
assertThat(value).isNull();
}
@Test
@DisplayName("given entry" +
"when get value" +
"then return value")
public void get4() {
// given
map.put(1, 1);
// when
Integer value = map.get(1);
// then
assertThat(value).isEqualTo(1);
}
@Test
@DisplayName("given multiple entries(hash conflict)" +
"when get value by hash conflict key" +
"then return value")
public void get5() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(3), 4);
map.put(new HashConflict(3), 5);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
// when
Integer value = map.get(new HashConflict(3));
// then
assertThat(value).isEqualTo(5);
}
@Test
@DisplayName("given multiple entries(hash conflict)" +
"when get value by not exist hash conflict key" +
"then return null")
public void get6() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
// when
Integer value = map.get(new HashConflict(6));
// then
assertThat(value).isNull();
}
/************ get test end **********/
/************ remove test start **********/
@Test
@DisplayName("given empty entries" +
"when remove by null key" +
"then return null")
public void remove1() {
// when
Integer value = map.remove(null);
// then
assertThat(value).isNull();
}
@Test
@DisplayName("given entry" +
"when remove by null key" +
"then return null")
public void remove2() {
// given
map.put(1, 1);
// when
Integer value = map.remove(null);
// then
assertThat(value).isNull();
}
@Test
@DisplayName("given entry" +
"when remove by key" +
"then return value")
public void remove3() {
// given
map.put(1, 1);
// when
int value = map.remove(1);
// then
assertThat(value).isEqualTo(1);
}
@Test
@DisplayName("given entry" +
"when remove by not exist key" +
"then return null")
public void remove4() {
// given
map.put(1, 1);
// when
Integer value = map.remove(2);
// then
assertThat(value).isNull();
}
@Test
@DisplayName("given multiple entries(hash conflict)" +
"when remove by hash conflict key" +
"then return value")
public void remove5() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
// when
Integer value = map.remove(new HashConflict(3));
// then
assertThat(value).isEqualTo(3);
assertThat(Lists.newArrayList(map.values())).isEqualTo(Lists.list(1, 2, 4, 5));
}
/************ remove test end **********/
/************ values test start **********/
@Test
@DisplayName("given empty entries" +
"when call values" +
"then return empty values")
public void values1() {
// when
Iterable<Integer> values = map.values();
// then
assertThat(values).isEmpty();
}
@Test
@DisplayName("given multiple entries" +
"when call values" +
"then return all values")
public void values2() {
// given
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(3, 4);
map.put(4, 4);
map.remove(4);
// when
Iterable<Integer> values = map.values();
// then
assertThat(values.spliterator().estimateSize()).isEqualTo(3);
assertThat(Lists.newArrayList(values)).isEqualTo(Lists.list(1, 2, 4));
}
/************ values test end **********/
/************ containsKey test start **********/
@Test
@DisplayName("given entry" +
"when key exist" +
"then return true")
public void contains_key1() {
// given
map.put(1, 1);
// when
boolean result = map.containsKey(1);
// then
assertThat(result).isTrue();
}
@Test
@DisplayName("given entry" +
"when key not exist" +
"then return false")
public void containsKey2() {
// given
map.put(1, 1);
// when
boolean result = map.containsKey(2);
// then
assertThat(result).isFalse();
}
@Test
@DisplayName("given multiple entries(hash conflict)" +
"when call containsKey" +
"then return correct result")
public void containsKey3() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
// then
assertThat(map.containsKey(new HashConflict(3))).isTrue();
assertThat(map.containsKey(new HashConflict(5))).isTrue();
assertThat(map.containsKey(new HashConflict(6))).isFalse();
}
/************ containsKey test end **********/
/************ forEach test start **********/
@Test
@DisplayName("given multiple entries" +
"when call forEach" +
"then pass")
public void forEach1() {
// given
map.put(1, 1);
map.put(2, 2);
map.put(3, 3);
map.put(4, 4);
// when
List<Integer> results = new ArrayList<>();
map.forEach((key) -> results.add(map.get(key)));
// then
assertThat(results).isEqualTo(Lists.list(1, 2, 3, 4));
}
@Test
@DisplayName("given multiple entries(hash conflict)" +
"when call forEach" +
"then pass")
public void forEach2() {
// given
SimpleHashMap<HashConflict, Integer> map = new SimpleHashMap<>();
map.put(new HashConflict(1), 1);
map.put(new HashConflict(2), 2);
map.put(new HashConflict(3), 3);
map.put(new HashConflict(4), 4);
map.put(new HashConflict(5), 5);
// when
List<Integer> results = new ArrayList<>();
map.forEach((key) -> results.add(map.get(key)));
// then
assertThat(results).isEqualTo(Lists.list(1, 2, 3, 4, 5));
}
/************ forEach test end **********/
class HashConflict {
private int field;
HashConflict(int field) {
this.field = field;
}
@Override
public int hashCode() {
return this.field <= 8 ? 1 : this.field;
}
@Override
public boolean equals(Object obj) {
return ((HashConflict) obj).field == this.field;
}
}
}
复制代码
SimpleHashMap 源码
/**
* @author lyning
*/
public class SimpleHashMap<K, V> {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
private static final float DEFAULT_LOAD_FACTOR = 0.75f;
private int size;
private Bucket<K, V>[] table;
private int threshold;
public boolean containsKey(K key) {
int hash = this.hash(key);
int index = this.index(hash);
Bucket<K, V> bucket = this.table[index];
return bucket != null
&& bucket.lookup(key) != null;
}
public void forEach(Consumer<K> action) {
for (Bucket<K, V> bucket : this.table) {
while (bucket != null) {
action.accept(bucket.key);
bucket = bucket.next;
}
}
}
public V get(K key) {
if (this.tableEmpty()) {
return null;
}
int hash = this.hash(key);
int index = this.index(hash);
return this.getVal(index, key);
}
public V put(K key, V value) {
if (this.tableEmpty() || this.nearByThreshold()) {
this.resize();
}
int hash = this.hash(key);
return this.putVal(key, value, hash);
}
public V remove(K key) {
if (this.tableEmpty()) {
return null;
}
int hash = this.hash(key);
int index = this.index(hash);
return this.removeVal(index, key);
}
public int size() {
return this.size;
}
public Iterable<V> values() {
if (this.tableEmpty()) {
return new ArrayList<>();
}
List<V> collections = new ArrayList<>();
this.collectValues(collections);
return collections;
}
private void collectValues(List<V> collections) {
for (Bucket<K, V> bucket : this.table) {
while (bucket != null) {
collections.add(bucket.value);
bucket = bucket.next;
}
}
}
private Bucket<K, V> findBucket(int index) {
return this.table[index];
}
private V getVal(int index, K key) {
Bucket<K, V> bucket = this.findBucket(index);
if (Objects.isNull(bucket) || Objects.isNull(bucket = bucket.lookup(key))) {
return null;
}
return bucket.value;
}
private void grow(int newCap) {
if (this.tableEmpty()) {
this.initTable(newCap);
return;
}
this.table = this.rebuildTable(newCap);
}
private int hash(K key) {
int hashcode;
return key == null
? 0
: (hashcode = key.hashCode()) ^ (hashcode >>> 16);
}
private int index(int hash) {
return hash & (this.table.length - 1);
}
private void initTable(int newCap) {
this.table = new Bucket[newCap];
}
private boolean nearByThreshold() {
return this.size + 1 >= this.threshold;
}
private V putVal(K key, V value, int hash) {
int index = this.index(hash);
Bucket<K, V> bucket = this.table[index];
if (Objects.isNull(bucket)) {
this.table[index] = new Bucket<>(hash, key, value);
} else {
Bucket<K, V> indexBucket = bucket.lookup(key);
if (indexBucket != null) {
indexBucket.value = value;
return value;
}
bucket.putLast(new Bucket<>(hash, key, value));
}
this.size += 1;
return value;
}
private Bucket<K, V>[] rebuildTable(int newCap) {
Bucket<K, V>[] oldTable = this.table;
Bucket<K, V>[] newTable = new Bucket[newCap];
for (Bucket<K, V> bucket : oldTable) {
if (bucket != null) {
int index = this.index(bucket.hash);
newTable[index] = bucket;
}
}
return newTable;
}
private V removeVal(int index, K key) {
Bucket<K, V> bucket = this.findBucket(index);
Bucket<K, V> prev = null;
while (bucket != null) {
if (bucket.matchKey(key)) {
if (Objects.isNull(prev)) {
this.table[index] = null;
} else {
prev.next = bucket.next;
}
this.size -= 1;
return bucket.value;
}
prev = bucket;
bucket = bucket.next;
}
return null;
}
private void resize() {
int oldCap = this.tableCapacity();
int newCap = 0;
if (oldCap == 0) {
oldCap = DEFAULT_INITIAL_CAPACITY;
this.threshold = (int) (DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
} else {
newCap = oldCap << 1;
this.threshold = this.threshold << 1;
}
if (newCap == 0) {
newCap = oldCap;
}
this.grow(newCap);
}
private int tableCapacity() {
return Objects.isNull(this.table) ? 0 : this.table.length;
}
private boolean tableEmpty() {
return Objects.isNull(this.table);
}
static class Bucket<K, V> {
Bucket<K, V> next;
int hash;
K key;
V value;
public Bucket(int hash, K key, V value) {
this.hash = hash;
this.key = key;
this.value = value;
}
public Bucket<K, V> lookup(K key) {
Bucket<K, V> bucket = this;
while (bucket != null) {
if (bucket.matchKey(key)) {
return bucket;
}
bucket = bucket.next;
}
return null;
}
public boolean matchKey(K key) {
return this.key == key || this.key.equals(key);
}
public void putLast(Bucket<K, V> bucket) {
this.last().next = bucket;
}
private Bucket last() {
Bucket<K, V> bucket = this;
while (true) {
if (Objects.isNull(bucket.next)) {
return bucket;
}
bucket = bucket.next;
}
}
}
}
复制代码
总结
其中最难的应属红黑树,真的是极其复杂,笔者用了一个小时还没能理解其中要领,索性使用链表替代了,等有时间再静下心来把未完成的任务消灭掉。
理解问题,Tasking,TDD(包含重构),这是笔者最近一直在遵守的规则,希望可以给您给来一点感悟。
源码
- READMD: github.com/lynings/lea…
- Test Code: github.com/lynings/lea…
- Resource: github.com/lynings/lea…
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 开发可伸缩系统必须遵守这 11 个软件设计原则
- RESTful API 中的 Status code 是否要遵守规范
- Effective Java Item56 - 遵守普遍接受的命名慣例
- 每个人都必须遵守的9大Kubernates最佳安全实践
- 电动汽车制造商特斯拉已经采取行动遵守 GPL 许可
- Linux 基金会推出 ACT 项目,帮助开发者遵守开源许可证
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Introduction to Linear Optimization
Dimitris Bertsimas、John N. Tsitsiklis / Athena Scientific / 1997-02-01 / USD 89.00
"The true merit of this book, however, lies in its pedagogical qualities which are so impressive..." "Throughout the book, the authors make serious efforts to give geometric and intuitive explanations......一起来看看 《Introduction to Linear Optimization》 这本书的介绍吧!