java集合之HashMap相关原理 方法

java集合之HashMap

  • Map接口的基于哈希表的实现。 此实现提供所有可选的映射操作,并允许空null值和空null键。(除了非同步和允许使用 null 之外,HashMap 类与 Hashtable 大致相同)该类不保证映射的顺序; 特别是它不保证该顺序恒久不变。
  • 此实现假定哈希函数将元素适当地分布在各桶之间,可为基本操作(getput)提供稳定的性能。迭代collection 视图需要的时间与 HashMap 实例的“容量”(桶的数量)加上它的大小(键-值映射的数量)成比例。 因此,如果迭代性能很重要,则不要将初始容量设置得太高(或负载因子太低),这一点非常重要。
  • HashMap 的实例有两个影响其性能的参数:初始容量和负载因子(initial capacity and load factor)。 容量是哈希表中的桶数,初始容量就是哈希表创建时的容量。 负载因子是衡量哈希表在其容量自动增加之前允许达到多满的指标。 当哈希表中的条目数超过负载因子和当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。
  • 通常,默认负载因子 (.75) 在时间和空间成本之间提供了很好的权衡。加载因子过高虽然减少了空间开销,但同时也增加了查询成本(在大多数 HashMap 类的操作中,包括 getput 操作,都反映了这一点)。在设置其初始容量时,应考虑映射中的预期条目数及其负载因子,以尽量减少 rehash 操作的次数。 如果初始容量大于最大条目数除以负载因子,则不会发生 rehash 操作。
  • 如果很多映射关系要存储在 HashMap 实例中,则相对于按需执行自动的 rehash 操作以增大表的容量来说,使用足够大的初始容量创建它将使得映射关系能更有效地存储。 请注意,使用具有相同 hashCode() 的多个键是降低任何哈希表性能的可靠方法。 为了改善影响,当键是 Comparable 时,此类可以使用键之间的比较顺序来帮助打破联系。
  • 请注意,此实现不是同步的。 如果多个线程并发访问一个散列映射,并且至少有一个线程在结构上修改了映射,则必须在外部进行同步。 (结构修改是添加或删除一个或多个映射的任何操作;仅更改与实例已包含的键关联的值不是结构修改。)这通常是通过同步一些自然封装映射的对象来完成的 . 如果不存在这样的对象,则应使用 Collections.synchronizedMap 方法“包装”映射。 这最好在创建时完成,以防止对映射的意外不同步访问:Map m = Collections.synchronizedMap(new HashMap(...));
  • 此类的所有“集合视图方法”返回的迭代器都是快速失败的:如果在迭代器创建后的任何时间对映射进行结构修改,除了通过迭代器自己的 remove 方法外,迭代器将抛出 ConcurrentModificationException . 因此,面对并发修改,迭代器快速而干净地失败,而不是在未来不确定的时间冒着任意、非确定性行为的风险。
  • 请注意,无法保证迭代器的快速失败行为,因为一般而言,在存在非同步并发修改的情况下不可能做出任何硬保证。 快速失败的迭代器会尽最大努力抛出 ConcurrentModificationException。 因此,编写一个依赖此异常来确保其正确性的程序是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
  • 关于主数组的长度为2的倍数的原因:
    • 扩容主数组的长度为2的倍数,因为这个length的长度,会影响 key的位置。
    • key的位置的计算: h & (length - 1);实际上这个算法就是:h%length ,但是取模效率太低,所以用位运算效率代替,而此算法等效的前提就是 length必须是2的整数倍。
    • 如果不是2的整数倍,则哈希碰撞的概率较高
  • 关于装填因子 0.75:
    • 如果装填因子大一点如1, 即数组满了再扩容,可以做到最大的空间利用率,但这是一个理想状态,元素不可能完全的均匀分布,很可能就哈西碰撞产生链表了。产生链表查询时间就长了。
      —>空间好,时间不好
    • 如果装填因子小一点如0.5,就浪费空间。可以做到逢0.5就扩容 ,哈希碰撞较少,不产生链表,查询效率较高 —>时间好,空间不好
    • 在空间和时间中,取中间值,平衡两个因素就取值为 0.75

相关源码

public abstract class HashMap<K, V>
        extends AbstractMap<K, V> //继承的AbstractMap中,已经实现了Map接口,又实现了这个接口
        implements Map<K, V>, Cloneable, Serializable {
    
    
    //重要属性:
    static final int DEFAULT_INITIAL_CAPACITY = 16;//哈希表主数组的默认长度
    /*定义了一个float类型的变量,以后作为:默认的装填因子,加载因子是表示Hsah表中元素的填满的程度,太大容易引起哈西冲突,太小容易浪费  0.75是经过大量运算后得到的最好值,这个值其实可以自己改,但是不建议改*/
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    transient Entry<K, V>[] table;//主数组,每个元素为Entry类型
    transient int size;
    int threshold;//数组扩容的界限值,门槛值   16*0.75=12
    final float loadFactor;//用来接收装填因子的变量

    public HashMap() {
    
     //空构造器:内部相当于:this(16,0.75f);调用了当前类中的带参构造器
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR);
    }

    //带参数构造器:对一些数值进行初始化
    public HashMap(int initialCapacity, float loadFactor) {
    
    
        //对capacity赋值,capacity的值一定是 大于传进来的initialCapacity的 2^n 中最小的数
        int capacity = 1;
        while (capacity < initialCapacity)
            capacity <<= 1;
        //对loadFactor赋值,将装填因子0.75赋值给loadFactor
        this.loadFactor = loadFactor;
        //数组扩容的界限值,门槛值
        threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
        //对table数组赋值,初始化数组长度为16
        table = new Entry[capacity];
    }
    public V put(K key, V value) {
    
    //put方法:
        if (key == null)//空值的判断
            return putForNullKey(value);
        
        int hash = hash(key);//调用hash方法,获取哈希码
        int i = indexFor(hash, table.length);//得到key对应在数组中的位置
        //如果你放入的元素,在主数组那个位置上没有值,e==null  那么不走这个循环;当在同一个位置上放入元素时,进行链表操作
        for (Entry<K, V> e = table[i]; e != null; e = e.next) {
    
    
            Object k;
            //哈希值一样  并且  equals相比一样或者是同一个对象
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
    
    
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);//addEntry添加这个节点
        return null;
    }

    //hash方法返回这个key对应的哈希值,内部进行二次散列,为了尽量保证不同的key得到不同的哈希码!
    final int hash(Object k) {
    
    
        int h = 0;
        if (useAltHashing) {
    
    
            if (k instanceof String) {
    
    
                return sun.misc.Hashing.stringHash32((String) k);
            }
            h = hashSeed;
        }
        //k.hashCode()函数调用的是key键值类型自带的哈希函数,
        //由于不同的对象其hashCode()有可能相同,所以需对hashCode()再次哈希,以降低相同率。
        h ^= k.hashCode();
/*此函数可确保在每个位位置仅相差常数倍的 hashCode 具有有限数量的冲突(在默认加载因子下约为 8)。接下来的一串与运算和异或运算,称之为“扰动函数”,扰动的核心思想在于使计算出来的值在保留原有相关特性的基础上,增加其值的不确定性,从而降低冲突的概率。往右移动的目的,就是为了将h的高位利用起来,减少哈希冲突*/
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    }

    static int indexFor(int h, int length) {
    
    //返回int类型数组的坐标
        //算法等效为为取模运算:h%length,取模效率不如位运算
        return h & (length - 1);
    }

    void addEntry(int hash, K key, V value, int bucketIndex) {
    
    
        //size的大小  大于等于 16*0.75=12的时候
        if ((size >= threshold) && (null != table[bucketIndex])) {
    
    
            resize(2 * table.length);//主数组扩容为2倍
            hash = (null != key) ? hash(key) : 0;//重新调整当前元素的hash码
            bucketIndex = indexFor(hash, table.length);//重新计算元素位置
        }
        //将hash,key,value,bucketIndex 封装为一个Entry对象:
        createEntry(hash, key, value, bucketIndex);
    }

    void createEntry(int hash, K key, V value, int bucketIndex) {
    
    
        Entry<K, V> e = table[bucketIndex];//获取bucketIndex位置上的元素给e
        //将hash, key, value封装为一个对象,然后将下一个元素的指向为e (链表的头插法)并将新的Entry放在table[bucketIndex]的位置上
        table[bucketIndex] = new Entry<>(hash, key, value, e);
        size++;//集合中加入一个元素 size+1
    }
    void resize(int newCapacity) {
    
     //数组扩容
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
    
    
            threshold = Integer.MAX_VALUE;
            return;
        }
        //创建长度为newCapacity的主数组
        Entry[] newTable = new Entry[newCapacity];
        boolean oldAltHashing = useAltHashing;
        useAltHashing |= sun.misc.VM.isBooted() &&
                (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
        boolean rehash = oldAltHashing ^ useAltHashing;
        //转让方法:将老数组中的东西都重新放入新数组中
        transfer(newTable, rehash);
        //老数组替换为新数组
        table = newTable;
        //重新计算
        threshold = (int) Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }

    void transfer(Entry[] newTable, boolean rehash) {
    
    
        int newCapacity = newTable.length;
        for (Entry<K, V> e : table) {
    
    
            while (null != e) {
    
    
                Entry<K, V> next = e.next;
                if (rehash) {
    
    
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                //将哈希值,和新的数组容量传进去,重新计算key在新数组中的位置
                int i = indexFor(e.hash, newCapacity);
                //头插法
                e.next = newTable[i];//获取链表上元素给e.next
                newTable[i] = e;//然后将e放在i位置
                e = next;//e再指向下一个节点继续遍历
            }
        }
    }
}

构造器

  • HashMap() 构造一个具有默认初始容量 (16) 和默认加载因子 (0.75) 的空 HashMap。
  • HashMap (int initialCapacity) 构造一个具有指定初始容量和默认负载因子 (0.75) 的空 HashMap。
  • HashMap (int initialCapacity, float loadFactor) 构造一个具有指定初始容量和负载因子的空 HashMap。
  • HashMap (Map<? extends K, ? extends V> m) 构造一个与指定 Map 具有相同映射关系的新 HashMap。

方法

Modifier and Type Method Description
void clear() 从此映射中删除所有映射。
Object clone() 返回这个 HashMap 实例的浅拷贝:键和值本身没有被克隆。
boolean containsKey (Object key) 如果此映射包含指定键的映射,则返回 true。
boolean containsValue (Object value) 如果此映射将一个或多个键映射到指定值,则返回 true。
Set<Map.Entry<K, V>> entrySet() 返回此映射中包含的映射的 Set 视图。
V get (Object key) 返回指定键映射到的值,如果此映射不包含该键的映射,则返回 null。
boolean isEmpty() 如果此映射不包含键值映射,则返回 true。
Set keySet() 返回此映射中包含的键的 Set 视图。
V put (K key, V value) 将指定值与此映射中的指定键关联。
void putAll (Map<? extends K, ? extends V> m) 将所有映射从指定映射复制到此映射。
V remove (Object key) 从此映射中删除指定键的映射(如果存在)。
int size() 返回此映射中键值映射的数量。
Collection values() 返回此映射中包含的值的集合视图。
  • V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
    • 尝试计算指定键及其当前映射值的映射(如果没有当前映射,则为 null)。
  • V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
    • 如果指定的键尚未与值关联(或映射为 null),则尝试使用给定的映射函数计算其值,并且 除非为空,否则将其输入此地图。
  • V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
    • 如果指定键的值存在且非空,则尝试计算给定键及其的新映射 当前映射值。
  • V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)
    • 如果指定的键尚未与值关联或与空值关联,则将其与给定的值关联 非空值。

增加

  • V put(K key, V value)将指定的值与此映射中的指定键关联(可选操作)。如果此映射以前包含一个该键的映射关系,则用指定值替换旧值(当且仅当 m.containsKey(k) 返回 true 时,才能说映射 m 包含键 k 的映射关系)。
    • key - 与指定值关联的键;value - 与指定键关联的值
    • 返回:以前与 key 关联的值,如果没有针对 key 的映射关系,则返回 null。(如果该实现支持 null 值,则返回 null 也可能表示此映射以前将 null 与 key 关联)。
HashMap<Integer,String> map = new HashMap<>();
System.out.println(map.put(1,"fyz")); //null
map.put(6,"yjk");
map.put(8,"xhr");
System.out.println(map.put(1,"fyznb")); //fyz
map.put(5,"zsh");
System.out.println(map.size()); //4
System.out.println(map); //{1=fyznb, 5=zsh, 6=yjk, 8=xhr}
示意图

在这里插入图片描述

删除

clear() remove(Object key)

map.remove("zsh"); //移除
System.out.println(map);//{xhr=8, fyz=2, yjk=7}
map.clear(); //清空
System.out.println(map); //{}

查看

entrySet() get(Object key) keySet() size() values()

System.out.println(map.entrySet()); //[1=fyznb, 5=zsh, 6=yjk, 8=xhr]
System.out.println(map.get(5)); //zsh
System.out.println(map.keySet()); //[1, 5, 6, 8]
System.out.println(map.size()); //4
System.out.println(map.values()); //[fyznb, zsh, yjk, xhr]
使用视图遍历
System.out.println("\n---------keySet()---------");
//keySet()对集合中的key进行遍历查看:
        Set<Integer> set = map.keySet();
        for(Integer s:set){
    
    
            System.out.print(s + s.hashCode() + "\t");
        }
/*---------keySet()---------
2	10	12	16	*/

        System.out.println("\n---------values()---------");
//values()对集合中的value进行遍历查看:
        Collection<String> values = map.values();
        for(String i:values){
    
    
            System.out.print(i + "\t");
        }
/*---------values()---------
fyznb	zsh	yjk	xhr	*/

        System.out.println("\n---------get(Object key) keySet()---------");
        Set<Integer> set2 = map.keySet();
        for(Integer s:set2){
    
    
            System.out.print(map.get(s) + "\t");
        }
/*---------get(Object key) keySet()---------
fyznb	zsh	yjk	xhr	*/

        System.out.println("\n---------entrySet()---------");
        Set<Map.Entry<Integer, String>> entries = map.entrySet();
        for(Map.Entry<Integer, String> e:entries){
    
    
            System.out.print(e.getKey()+"----"+e.getValue() + "\t");
        }
/*---------entrySet()---------
1----fyznb	5----zsh	6----yjk	8----xhr	*/

判断

containsKey(Object key) containsValue(Object value) equals(Object o) isEmpty()

System.out.println(map.containsKey(5)); //true
System.out.println(map.containsValue("fyz")); //false
System.out.println("判断是否为空:"+map.isEmpty()); //判断是否为空:false
HashMap<Integer,String> map2 = new HashMap<>();
map2.put(1,"fyz");
map2.put(6,"yjk");
map2.put(8,"xhr");
map2.put(1,"fyznb");
map2.put(5,"zsh");
System.out.println("判断是否相等  ==  :" + (map == map2)); //判断是否相等  ==  :false
System.out.println("判断是否相等equals:"+map.equals(map2));//true,equals进行了重写,比较的是集合中的值是否一致
 map2 = new HashMap<>();
map2.put(1,"fyz");
map2.put(6,"yjk");
map2.put(8,"xhr");
map2.put(1,"fyznb");
map2.put(5,"zsh");
System.out.println("判断是否相等  ==  :" + (map == map2)); //判断是否相等  ==  :false
System.out.println("判断是否相等equals:"+map.equals(map2));//true,equals进行了重写,比较的是集合中的值是否一致

猜你喜欢

转载自blog.csdn.net/m0_46530662/article/details/119801259