初步深入了解HashSet,HashMap,HashTable

HashSet

我们要弄明白hashSet就需要去看它的代码,所以我新建了一个hashSet然后ctrl点了进去,看到了这个

 public HashSet() {
        map = new HashMap<>();
    }

我们看到hashSet无参的构造函数竟然是个map,但是看到这里,我又有了一个疑问,我们都知道map结构是个键值对即(key,value)格式,而我们在用set的时候只需要输入一个value就可以了,那么问题来了,既然HashSet底层是HashMap,那么HashMap的key哪里去了呢?
带着这个疑问我又往下去了解,结果发现了这个

public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

我们发现添加的时候,会将我们传入的值放在key的位置上,而那个放value的地方会放一个PRESENT,而后面那个全是大写的东西,我看了一下,是一个常量(因为被final修饰了,值也不能变,相当于是个null)

 // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

原来我们set存放的数据的地方竟然是map的key的位置,这样也就说得通了,我们为什么set不能重复,而且在方数据的时候只能放一个值,原来是放在了map的key上
至于为什么map的key为什么不能重复,就让我们来继续探讨HashMap吧

HashMap

参考我们的HashSet的探究方式,我们来探讨HashMap,首先是HashMap的构造方法,先来看看无参构造函数

public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
    }

嗯?这个loadFactor又是个什么东西?去看看

 final float loadFactor;
 static final float DEFAULT_LOAD_FACTOR = 0.75f;

哦,原来又是一个常量,还是个float类型,**解释一下DEFAULT_LOAD_FACTOR,这东西好像叫做负载因子,默认是0.75,我的个人理解大概就是空间利用率之类的东西,举个例子,就是有一个100的容器,但是只有其中75的空间可用,如果我想放一个80大小的东西,我就需要给我100的容器扩容到 80 / 0.75 = 106.67左右,就能刚好放下80。**了解是个常量,但是我们并没有看到什么有用的东西,像map是怎么实现的,那我们就继续往下看

public HashMap(Map<? extends K, ? extends V> m) {
        this.loadFactor = DEFAULT_LOAD_FACTOR;
        putMapEntries(m, false);
    }

这个构造方法,我想大家都懂,不懂得我简单介绍一下,差不多相当于一个克隆复制,将你原来的map在创建一个新的对象,并且将原来的map值传给他。
但是关键并不是这个构造器,关键在putMapEntries上,这个方法是怎把我们原来的<key, value>放进去的呢

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
        int s = m.size();
        if (s > 0) {
            if (table == null) { // pre-size
                float ft = ((float)s / loadFactor) + 1.0F;
                int t = ((ft < (float)MAXIMUM_CAPACITY) ?
                         (int)ft : MAXIMUM_CAPACITY);
                if (t > threshold)
                    threshold = tableSizeFor(t);
            }
            else if (s > threshold)
                resize();
            for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
                K key = e.getKey();
                V value = e.getValue();
                putVal(hash(key), key, value, false, evict);
            }
        }
    }

代码有点长,有点全部看不懂,但是这不重要,能够看懂最后那个foreach循环就行了,**我们看到我们明明是个map结构,里面是<key, value>的格式,但是<key, value>的格式又是靠什么存储的呢?**这个foreach循环很好的告诉了我们答案。它是靠Entry进行一个存储的,Entry又是个什么呢?

 interface Entry<K,V> {
        
        K getKey();

        
        V getValue();

        boolean equals(Object o);

        
        int hashCode();

       
        public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getKey().compareTo(c2.getKey());
        }

        
        public static <K, V extends Comparable<? super V>> Comparator<Map.Entry<K,V>> comparingByValue() {
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> c1.getValue().compareTo(c2.getValue());
        }

        
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByKey(Comparator<? super K> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getKey(), c2.getKey());
        }

        
        public static <K, V> Comparator<Map.Entry<K, V>> comparingByValue(Comparator<? super V> cmp) {
            Objects.requireNonNull(cmp);
            return (Comparator<Map.Entry<K, V>> & Serializable)
                (c1, c2) -> cmp.compare(c1.getValue(), c2.getValue());
        }
    }

方法注释有点长,就给大家删除了,有兴趣的可以自己回去看,我想表达的重点是,你看这个Entry里面,有key,有value,还有hashCode,甚至还有equals,是不是很眼熟,这不就和我们建树是的节点差不多嘛,一个值,一个指针,这不就是个节点的接口吗?而且我还看到了一个有趣的东西,我们都知道Map不能使用迭代器,但是Set可以使用迭代器,但是Set的底层又是map实现的,那我们不就可以将map转换成set在进行一个迭代,差不多就是下面那个样子。

Map.Entry<String, String> entry = map.entrySet().iterator().next();

我们了解了map是用Entry<key, value>存储的,下一个就是HashTable了

HashTable

还是老样子,构造方法安排。。。等等,我发现了什么

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable

大家看,这个HashTable实现了Map接口,说明HashTable应该和HashMap相似,那么为什么HashTable还有存在的必要呢?来看一下构造方法

public Hashtable() {
        this(11, 0.75f);
    }

前面那个11,就是容量大小,而后面那个0.75f就是我们的老朋友,负载因子了。但这并不能看出什么,就继续往下走。

 public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry<?,?>[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }

用指定初始容量和指定加载因子构造一个新的空哈希表。其中initHashSeedAsNeeded方法用于初始化hashSeed参数,其中hashSeed用于计算key的hash值,它与key的hashCode进行按位异或运算。这个hashSeed是一个与实例相关的随机值,主要用于解决hash冲突,hash冲突又是什么,如何产生,解释一下,在存储一个数据的时候,我们需要对其进行一个hash值的求取使用hashCode(),然后放到哈希值映射的地址上,(hash值和地址没有必然联系,只是通过一个哈希函数进行映射),但是,冲突就是两个对象的哈希值一样了,产生了冲突。
面对hash冲突我们一般采用链地址的方法进行一个关联:
将所有产生冲突的关键字所对应的数据全部存储在同一个线性链表中。
例如有一组关键字为{19,14,23,01,68,20,84,27,55,11,10,79},其哈希函数为:H(key)=key MOD 13,使用链地址法所构建的哈希表如图 3 所示:
请添加图片描述
又是后冲突太多,导致后面的链表过长,所以在jdk8之后,又出现了红黑树进行,当超过一定数量之后,就将链表变成红黑树进行一个存储,来提高一个效率。请添加图片描述

HashMap和HashTable区别

1.HashMap可以允许存在一个为null的key任意个为null的value,但是HashTable中的key和value都不允许为null。如下:

HashMap遇到为null的key时,它会调用putForNullKey方法来进行处理。对于value没有进行任何处理,只要是对象都可以。

if (key == null)
return putForNullKey(value);

而当HashTable遇到null时,他会直接抛出NullPointerException异常信息。

if (value == null) {
throw new NullPointerException();
}

2.Hashtable的方法是同步的,而HashMap的方法不是。所以有人一般都建议如果是涉及到多线程同步时采用HashTable,没有涉及就采用HashMap。

3.HashTable有一个contains(Object value),功能和containsValue(Object value)功能一样。

4.两个遍历方式的内部实现上不同。

Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

5.哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。代码是这样的:

int hash = key.hashCode(); 
int index = (hash & 0x7FFFFFFF) % tab.length;

而HashMap重新计算hash值,而且用与代替求模:

int hash = hash(k); 
int i = indexFor(hash, table.length); 

6.Hashtable和HashMap它们两个内部实现方式的数组的初始大小和扩容的方式。HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。(为什么HashMap要是2的倍数扩容,原因是减小冲突)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值