最近面试,面试官给留了作业,手写一个Map,需求如下
- 手写一个支持泛型的Map,使用链地址法实现,工程使用Maven
- 增加junit单元测试
- 用JMH对手写Map和HashMap做对比测试
手写Map
方法定义
首先定义Map的几个方法,存、取、扩容
public interface MyMap<K, V> {
/**
* 插入元素
* @param key
* @param value
*/
void put(K key, V value);
/**
* 获取元素
* @param key
* @return
*/
Object get(K key);
/**
* 扩容
* 扩容为两倍
*/
void resize();
}
方法实现
定义元素
仅用数组实现Map,所以数组每个元素只需要包含key、value两个属性
public class Node<K, V> {
K key;
V value;
Node(K key, V value) {
this.key = key;
this.value = value;
}
}
公共配置
参考HashMap,定义初始容量、扩容阈值、数组,同时记录已经存入的元素个数(扩容边界条件)
// 数组
private Node<K, V>[] nodes;
// map大小
private int size;
// 初始容量
static final int DEFAULT_SIZE = 16;
// 扩容阈值
double threshold = 0.75;
// 已经插入的元素个数
int usedSize = 0;
插入数据
使用链地址法,插入数据时,遇到hash冲突,直接向后(到n-1的话再从0开始)找一个空位插入对应的key,因为扩容策略0.75的阈值,所以正常应该都会有空位放入数据 or 扩容后有空位放入数据
@Override
public void put(K key, V value) {
if (null == key) {
throw new RuntimeException("Key is null");
}
if (null == nodes) {
nodes = new Node[DEFAULT_SIZE];
size = DEFAULT_SIZE;
}
// 查找插入位置
int inputIndex = key.hashCode() & (size - 1);
Node node = nodes[inputIndex];
if (null == node) {
// 不存在key,插入
nodes[inputIndex] = new Node(key, value);
usedSize++;
return;
}
if (node.key.equals(key)) {
// 已经存在相同的key, 替换
nodes[inputIndex].value = value;
return;
}
// 查找下一个空位
for (int i = inputIndex; i < size; i = getNextIndex(i, size)) {
if (null == nodes[i]) {
nodes[i] = new Node<>(key, value);
usedSize++;
break;
}
}
// 扩容边界条件
if (usedSize >= size * threshold) {
resize();
}
}
private int getNextIndex(int index, int len) {
return index < len - 1 ? index + 1 : 0;
}
获取数据
从hash值对应的index初开始遍历,挨个做值的比较,时间复杂度O(n),相比HashMap用链表or红黑树解决hash冲突,本方案平均时间复杂度较高。
@Override
public Object get(K key) {
if (null == key) {
throw new RuntimeException("Key is null");
}
int getIndex = key.hashCode() & (size - 1);
Node node = nodes[getIndex];
if (null != node && node.key.equals(key)) {
return node.value;
} else {
while (node != null) {
if (node.key.equals(key)) {
return node.value;
} else {
getIndex = getNextIndex(getIndex, size);
node = nodes[getIndex];
}
}
}
return null;
}
扩容
扩容的基本思想,是新建更大的数组,再将之前数组的元素重新计算hash值,再重新进行插入,完成后,再将数组指针指向新创建的数组
@Override
public void resize() {
if (usedSize < size * threshold) {
return;
}
int oldSize = size;
// 2倍扩容
int newSize = oldSize << 1;
Node[] newNodes = new Node[newSize];
int sum = 0;
for (int i = 0; i < oldSize; i++) {
Node node = nodes[i];
if (null != node) {
// 计算新数组的插入坐标
int index = node.key.hashCode() & (newSize - 1);
while (null != newNodes[index]) {
index = getNextIndex(index, newSize);
}
newNodes[index] = node;
sum++;
}
}
size = newSize;
usedSize = sum;
nodes = newNodes;
}
单元测试
引入junit依赖
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
构建Map测试用例,随机生成1000个UUID,做Map的插入、获取操作
public class MapTest {
@Test
public void mapTest() {
Map map = new Map();
List<String> list = new ArrayList<>(1000);
for (int i = 0; i < 1000; i++) {
String item = UUID.randomUUID().toString();
list.add(item);
map.put(item, item);
}
for (int i = 0; i < 1000; i++) {
Assert.assertEquals(map.get(list.get(i)), list.get(i));
}
}
}
JMH测试
JMH可以用来做性能测试,之前只用过Jmeter,趁此机会也对JMH做了了解
引入jmh依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
<scope>provided</scope>
</dependency>
模拟一个线程针对手写的Map以及HashMap,做插入和获取操作,输出性能报告,查看读写性能差异
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 2, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Thread)
public class MyBenchmark {
static Map map = new Map();
static HashMap hashMap = new HashMap();
static List<String> list = new ArrayList<>(1000);
static Map getMap = new Map();
static HashMap getHashMap = new HashMap();
static {
for (int i = 0; i < 1000; i++) {
String item = UUID.randomUUID().toString();
list.add(item);
getMap.put(item, item);
getHashMap.put(item, item);
}
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.output("output.log")
.build();
new Runner(opt).run();
}
@Benchmark
public void testMyMapPut() {
for (int i = 0; i < 1000; i++) {
String item = list.get(i);
map.put(item, item);
}
}
@Benchmark
public void testHashMapPut() {
for (int i = 0; i < 1000; i++) {
String item = list.get(i);
hashMap.put(item, item);
}
}
@Benchmark
public void testMyMapGet() {
for (int i = 0; i < 1000; i++) {
String item = list.get(i);
getMap.get(item);
}
}
@Benchmark
public void testHashMapGet() {
for (int i = 0; i < 1000; i++) {
String item = list.get(i);
getHashMap.get(item);
}
}
}
通过报告指标,手写map的写入效率,远远低于HashMap,使用链地址法,通过占用其他index的方式解决hash冲突,存储效率低
HashMapGet和MyMapGet效率近似,因为查找元素,都是通过遍历查找的方式,时间复杂度都差不多是O(n)(可能是因为数据量不大,HashMap的红黑树查询提升不够明显)
Benchmark Mode Cnt Score Error Units
MyBenchmark.testHashMapGet avgt 5 0.004 ± 0.001 ms/op
MyBenchmark.testHashMapPut avgt 5 0.006 ± 0.001 ms/op
MyBenchmark.testMyMapGet avgt 5 0.005 ± 0.001 ms/op
MyBenchmark.testMyMapPut avgt 5 2.408 ± 5.783 ms/op