Javascript实现Java的HashMap(链表散列)

简介: 前言如果你研究过Java中HashMap的源码,你就会知道HashMap底层的存储结构。Java中的HashMap是以链表散列的形式存储的,也就是数组+链表:HashMap中有一个Entry数组,默认的数组长度是16。

前言

如果你研究过Java中HashMap的源码,你就会知道HashMap底层的存储结构。Java中的HashMap是以链表散列的形式存储的,也就是数组+链表:HashMap中有一个Entry数组,默认的数组长度是16。这个值必须是2的整数次幂,以保证在通过key的hash值来计算entry应该放置的数组下标时可以尽量做到平均分配。而Entry数组中的每一个非空Entry都是一个Entry链表的头结点。这样做的好处就是HashMap结合了数组在寻址(查找)上的优势和链表在放置和删除上的优势。当一个链表的长度过长,查询所耗费的时间也在增加,当达到一定阈值的时候,Entry数组就会自动扩容至原来的两倍。这个时候就需要把原数组的所有元素都拷贝至新数组中,这也是HashMap操作过程中一个消耗相对大一些的操作。HashMap的存储结构如下图:


这里写图片描述

不过在JavaScript中,并没有为我们提供一个类似于HashMap的结构用于存储数据。JavaScript中的数组其实可以被用来当做一个映射集,比如如下语句

var a = new Array(1,2,3);
    a[-10]="a[-10]";
    a["sss"]="sss";

都是可以正常运行的,不过有一点需要注意的是,当[]里不是非负整数的时候,该属性并没有被存放到数组中,而是作为这个数组对象的一个属性被存储进来。所以这样的赋值显然也不会改变数组的长度。当上面的语句被执行之后,如果你输出这个数组的长度,则依然会得到3。这显然不是我们想要的结果,所以我们可以参考Java中的HashMap源码来写一个在JavaScript中适用的HashMap。

项目下载地址

正文

在Javascript中,我们仍然以”数组+链表“的链表散列形式来存储HashMap中的数据。这里我们规定key的值必须为字符串,可以为空字符串,但是不能为null或undefined。这样做的原因一是大部分的时间我们都会以字符串作为key,而我们的HashMap计算hash值的方法就是通过计算key值字符串每个字符的ASCII码并拼接获得的;还有一个原因就是Javascript中并不会像Java语言中会为每个不同的对象生成不同的hash值,所以Javascript中对象之间的比较也非常复杂。如果实现了这一功能,在put操作比较key的时候就会非常耗时,违背了HashMap设计的初衷。而对于value的类型并没有限制,可以是任何类型的变量。还有一点值得一提的是,由于Javascript数组本身就不是定容的,即可以通过赋值语句动态的增减数组的长度,所以相对于Java中的HashMap来说,我们将要实现的HashMap在扩容的时候还省去了把原数组中的entry拷贝至新数组的步骤。
下面上代码:

function HashMap(){

    //初始大小
    var size = 0;

    //数组
    var table = [];

    //初始数组长度为16
    var length = 2 << 3;

    //数组扩容临界值为12
    var threshold = 0.75 * length;

    //hash值计算
    this.hash = function(h) {
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    //返回HashMap的size
    this.size = function(){
        return size;
    }

    //是否包含某个key
    this.containsKey = function(key) {
        if(key == null || key == undefined)
            return false;
        else {
            var hashCode = this.hashCode(key);
            var hash = this.hash(hashCode);
            var index = this.indexFor(hash, length)
            for(var e = table[index]; e != null && e != undefined; e = e.next){
                if(e.key === key){
                    return true;
                }
            }
            return false;
        }
    }

    //是否包含某个value
    this.containsValue = function(value) {
        for(var index = 0; index < table.length; index++) {
            for (var e = table[index]; e != null && e != undefined; e = e.next) {
                if (JSON.stringify(e.value) === JSON.stringify(value)) {
                    return true;
                }
            }
        }
        return false;
    }

    //HashMap是否为空
    this.isEmpty = function(){
        return size === 0;
    }

    //计算HashCode值,不同的key有不同的HashCode,这里使用字符串转ASCII码并拼接的方式
    this.hashCode = function(key){
        var hashcode = '';
        for(var i=0 ;i< key.length; i++){
            hashcode += key.charCodeAt(i);
        }
        return hashcode;
    }

    //向HashMap中存放值
    this.put = function(key, value){
        if(key == null || key == undefined)
            return
        var hashCode = this.hashCode(key);
        var hash = this.hash(hashCode);
        var index = this.indexFor(hash, length)
        for(var e = table[index]; e != null && e != undefined; e = e.next){
            if(e.key === key){
                var oldValue = e.value;
                e.value = value;
                return oldValue;
            }
        }
        this.addEntry(key, value, index)
    }

    //从HashMap中获取值
    this.get = function(key){
        if(key == null || key == undefined)
            return undefined
        var hashCode = this.hashCode(key);
        var hash = this.hash(hashCode);
        var index = this.indexFor(hash, length)
        for(var e = table[index]; e != null && e != undefined; e = e.next){
            if(e.key === key){
                return e.value;
            }
        }
        return undefined;
    }

    //从HashMap中删除值
    this.remove = function(key){
        if(key == null || key == undefined)
            return undefined
        var hashCode = this.hashCode(key);
        var hash = this.hash(hashCode);
        var index = this.indexFor(hash, length)
        var prev = table[index];
        var e = prev;
        while(e != null && e!= undefined){
            var next = e.next;
            if(e.key === key){
                size--;
                if(prev == e){
                    table[index] = next;
                }
                else{
                    prev.next = next;
                }
                return e;
            }
            prev = e;
            e = next;
        }
        return e == null||e == undefined? undefined: e.value;
    }

    //清空HashMap
    this.clear = function() {
        table = [];
        // 设置size为0
        size = 0;
        length = 2 << 3;
        threshold = 0.75 * length;
    }

    //根据hash值获取数据应该存放到数组的哪个桶(下标)中
    this.indexFor = function(h, length) {
        return h & (length-1);
    }

    //添加一个新的桶来保存key和value
    this.addEntry = function(key, value, bucketIndex) {
        // 保存对应table的值
        var e = table[bucketIndex];
        // 然后用新的桶套住旧的桶,链表
        table[bucketIndex] = { key: key, value: value, next: e}
        // 如果当前size大于等于阈值
        if (size++ >= threshold)
        // 调整容量
        {
            length = length << 1;
            threshold = 0.75 * length;
        }
    }

    //获取HashMap中所有的键值对
    this.getEntries = function(){
        var entries = [];
        for(var index = 0; index < table.length; index++) {
            for (var e = table[index]; e != null && e != undefined; e = e.next) {
                entries.push({key: e.key, value: e.value})
            }
        }
        return entries;
    }

}

其中hashcode这个函数,就是把key值的每个字符转换成ASCII码并且拼接的过程。这样可以保证当key值不同的时候,生成的字符串肯定也不相同。而hash这个函数则与Java中的HashMap源码中的hash方法完全相同。通过得到的这个hash值与数组的当前长度进行与运算,获取到我们要put的键值对应该放置在数组的哪个下标之中。
我们可以观察containsValue这个函数,这个函数的作用是判断HashMap中是否有某个value。在这个函数的实现里,有一个对象之间的比较:

if (JSON.stringify(e.value) === JSON.stringify(value)) {
    return true;
}

这里使用了JSON对象转字符串并进行比较,对嵌套的对象结构依然有效,不过比较的效率相对来说很低。不过相对于key的比较,value的比较并不会经常被调用,这也解释了之前所说的key只能为字符串,而value可以使任何形式的对象的说法。getEntries函数可以获得HashMap中所有的键值对,这个函数返回一个包含所有键值对的数组,如果想对HashMap进行遍历,那么可以调用这个函数,并且遍历返回的数组就可以了。

测试

测试语句如下:

 var hs = new HashMap();
    hs.put('asd', 123)
    hs.put('bqwe', 456)
    hs.put('czx', 789)
    hs.put('dcv', {a: 1, b: 2})
    hs.put('edf', 123)
    hs.put('fsdf', 456)
    hs.put('gdf', 789)
    hs.put('hds', {a: 1, b: 2})
    hs.put('idc', 123)
    hs.put('jdf', 456)
    hs.put('ker', 789)
    hs.put('lsd', [1, 2, 3])
    hs.put('mvr', {a: 1, b: 2, c: {d: 2}})
    hs.put("vvv", null)
    hs.put('', 'asdasd')
    console.log(hs.get(''))
    console.log(hs.get("qqq"))

    console.log(hs.containsKey('asd'))
    console.log(hs.get('asd'))

    hs.remove('asd')
    console.log(hs.containsKey('asd'))

    console.log(hs.containsValue({a: 1, b: 2, c: {d: 2}}))
    console.log(hs.containsValue(123))

    console.log(hs.get('asd'))
    console.log(hs.size())
    var en = hs.getEntries();

    console.log(en)

    en.forEach((item) => {
        console.log(item.key)
        console.log(item.value)
    })

    hs.clear();
    console.log(hs.isEmpty())

输出结果:

asdasd
undefined
true
123
false
true
true
undefined
14
[Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object, Object]

asdasd
hds
Object {a: 1, b: 2}
gdf
789
jdf
456
mvr
Object {a: 1, b: 2, c: Object}
fsdf
456
dcv
Object {a: 1, b: 2}
idc
123
bqwe
456
lsd
[1, 2, 3]
czx
789
ker
789
edf
123
vvv
null
true
相关文章
|
3月前
|
存储 安全 Java
Java 集合框架中的老炮与新秀:HashTable 和 HashMap 谁更胜一筹?
嗨,大家好,我是技术伙伴小米。今天通过讲故事的方式,详细介绍 Java 中 HashMap 和 HashTable 的区别。从版本、线程安全、null 值支持、性能及迭代器行为等方面对比,帮助你轻松应对面试中的经典问题。HashMap 更高效灵活,适合单线程或需手动处理线程安全的场景;HashTable 较古老,线程安全但性能不佳。现代项目推荐使用 ConcurrentHashMap。关注我的公众号“软件求生”,获取更多技术干货!
61 3
|
4月前
|
Java
Java之HashMap详解
本文介绍了Java中HashMap的源码实现(基于JDK 1.8)。HashMap是基于哈希表的Map接口实现,允许空值和空键,不同步且线程不安全。文章详细解析了HashMap的数据结构、主要方法(如初始化、put、get、resize等)的实现,以及树化和反树化的机制。此外,还对比了JDK 7和JDK 8中HashMap的主要差异,并提供了使用HashMap时的一些注意事项。
165 2
Java之HashMap详解
|
24天前
|
存储 缓存 安全
Java HashMap详解及实现原理
Java HashMap是Java集合框架中常用的Map接口实现,基于哈希表结构,允许null键和值,提供高效的存取操作。它通过哈希函数将键映射到数组索引,并使用链表或红黑树解决哈希冲突。HashMap非线程安全,多线程环境下需注意并发问题,常用解决方案包括ConcurrentHashMap和Collections.synchronizedMap()。此外,合理设置初始化容量和加载因子、重写hashCode()和equals()方法有助于提高性能和避免哈希冲突。
52 17
Java HashMap详解及实现原理
|
5月前
|
存储 Java
Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。
【10月更文挑战第19天】本文详细介绍了Java中的HashMap和TreeMap,通过具体示例展示了它们在处理复杂数据结构问题时的应用。HashMap以其高效的插入、查找和删除操作著称,而TreeMap则擅长于保持元素的自然排序或自定义排序,两者各具优势,适用于不同的开发场景。
68 1
|
5月前
|
存储 安全 Java
Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
【10月更文挑战第17天】Java Map新玩法:探索HashMap和TreeMap的高级特性,让你的代码更强大!
116 2
|
5月前
|
存储 Java 开发者
Java Map实战:用HashMap和TreeMap轻松解决复杂数据结构问题!
【10月更文挑战第17天】本文深入探讨了Java中HashMap和TreeMap两种Map类型的特性和应用场景。HashMap基于哈希表实现,支持高效的数据操作且允许键值为null;TreeMap基于红黑树实现,支持自然排序或自定义排序,确保元素有序。文章通过具体示例展示了两者的实战应用,帮助开发者根据实际需求选择合适的数据结构,提高开发效率。
130 2
|
5月前
|
存储 缓存 安全
HashMap VS TreeMap:谁才是Java Map界的王者?
HashMap VS TreeMap:谁才是Java Map界的王者?
200 2
|
5月前
|
Java
用java搞定时任务,将hashmap里面的值存到文件里面去
本文介绍了如何使用Java的`Timer`和`TimerTask`类创建一个定时任务,将HashMap中的键值对写入到文本文件中,并提供了完整的示例代码。
59 1
用java搞定时任务,将hashmap里面的值存到文件里面去
|
5月前
|
存储 Java 程序员
Java面试加分点!一文读懂HashMap底层实现与扩容机制
本文详细解析了Java中经典的HashMap数据结构,包括其底层实现、扩容机制、put和查找过程、哈希函数以及JDK 1.7与1.8的差异。通过数组、链表和红黑树的组合,HashMap实现了高效的键值对存储与检索。文章还介绍了HashMap在不同版本中的优化,帮助读者更好地理解和应用这一重要工具。
152 5
|
5月前
|
存储 Java API
详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
【10月更文挑战第19天】深入剖析Java Map:不仅是高效存储键值对的数据结构,更是展现设计艺术的典范。本文从基本概念、设计艺术和使用技巧三个方面,详细解析HashMap、TreeMap、LinkedHashMap等实现类,帮助您更好地理解和应用Java Map。
109 3

热门文章

最新文章