面试竟然被留了作业

401 阅读3分钟

最近面试,面试官给留了作业,手写一个Map,需求如下

  1. 手写一个支持泛型的Map,使用链地址法实现,工程使用Maven
  2. 增加junit单元测试
  3. 用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