Map的底层原理(简要)

本文详细介绍了Java中的Map家族,包括HashMap、TreeMap、LinkedHashMap和Hashtable。HashMap基于哈希表实现,无序且不允许重复;TreeMap基于红黑树,保证有序性;LinkedHashMap保持插入顺序或访问顺序;Hashtable线程安全但效率较低。文章探讨了HashMap的底层原理,包括哈希冲突解决、扩容机制,并解释了为何加载因子通常设置为0.75和扩容时为何翻倍。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Map分为Hashmap,Treemap,Hashmap下又有HashTable

1.Hashmap

特点:hashmap在存入数据时,无序且不能重复

           按照K进行排序,底层key遵循哈希表的结构(数组加链表)

相关方法:

增加:put(k,v) 

删除:clear(),remove()

查看:entryset():查看map全部(k,v),KeySet(), size(),values()

判断:containKey(),ContainValue(),equals(),isEmpty()

下面是对entryset()方法的使用以及之后分别遍历k,v的方法

Set<Map.Entry<String,Integer>> t4 = t1.entrySet();
for (Map.Entry<String,Integer>e:t4){
    System.out.println(e.getKey()+"--"+e.getValue());
}

2.其中不能重复的原因且排序的原因

不重复需要重写那个类的equal()和hashcode()方法

@Override
public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Student student = (Student) o;
    return age == student.age &&
            Objects.equals(name, student.name);
}

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

3.LinkedHashmap

特点:有序,按照输入的顺序输出

4.TreeMap

1)特点:唯一,有序(按照升序或者降序)

   原理:二叉树,key按照二叉树的特点放入集合的key的数据类型对应。的数据内部一定要重写毕比较器(外部比较器或者内部比较器),下面是快速生成比较器的方法

Map<Student,Integer> map = new TreeMap<>(new Comparator<Student>() {
    @Override
    public int compare(Student o1, Student o2) {
        return ((Double)o1.getHigh()).compareTo((Double)o2.getHigh());
    }

5.Hashtable

      1)Hashmap:JDK1.2,效率高,线程不安全,key可以存放null值

            Hashtable:JDK1.0,效率低,线程安全,key不能为null

6.HashMap的底层原理

1)代码(摘要)

package Map.map.Hashmap;

import java.io.Serializable;
import java.util.AbstractMap;
import java.util.Map;

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>,Cloneable, Serializable {
//重要属性
    static final int DeFAULT_INITIAL_CAPACITY = 16 ; //定义了一个16,一会要赋给数组的
    static final int MAXMUM_CAPACITY =1; //定义了一个很大的数
    static final float DEFAULT_LOAD_FACTOR = 0.75f;//负载因子,加载因子,用来判断自动扩容
    transient Entry<K,V>[] table;//底层主数组
    transient int size; //添加的元素的数量
    int shthreshould; //定义变量,默认为0,用来判断扩容的边界值,门槛值
    final float loadFactor; //这个变量用来接收;装载因子,负载因子,加载因子

//构造器
    //1).空构造器,一般调用这个
    public HashMap(){this(DeFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);}
    //2).带参构造器
    public HashMap(int initialCapacity,float loadFactor){//传入自定义数组长度,负载因子
        if (initialCapacity > MAXMUM_CAPACITY)
            initialCapacity = MAXMUM_CAPACITY;
        if (loadFactor<=0 || Float.isNaN(loadFactor))
            throw new IllegalAccessException("illegqal load factor:" + loadFactor);
        int capacity = 1;
        while (capacity<initialCapacity)
            capacity<<=1;  //如果小于输入的数组长度,就自增一倍
        this.loadFactor = loadFactor;
        threshold=(int)Math.min(capacity*loadFactor,MAXMUM_CAPACITY+1);//取自定义的负载因子乘当前数组的最大二次幂算出扩容边界值
        table = new Entry[capacity];   //将底层数组放入,生成一个Entry对象,内部由4个属性(key,value,哈希值,地址)
    }

//存储数据的方法
    public V put(K key,V value){//K:Integer V:String
        //对key进行判断是否为空,允许为空
        if(key == null)
            return putforNullKey(Value);
        
        //获取key的哈希码
        int hash = hash(key);
        //得到元素在数组中的位置
        int i = indexFor(hash,table.length);
        //如果放入的数组的位置上没有元素的话,那么直接添加就行了,不用走下面的for循环
        
        //如果满足e!= null的话,说明位置上已经有东西了
        for(Entry<K,V> e = table[i];e! = null;e = e.next){
            Object k;
            //发生哈希碰撞的时候,会先比较哈希值
            //比较key是否是一个对象,如果key是一个对象的话,equals就不比较了
            //如果不是同一个对象,会比较equals方法
            //如果hash值一样,equals值也一样,就会走这个
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))){
                V oldValue = e.value;
                e.Value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        
        addEntry(hash,key,value,i); //哈希码,key,value,以及数组位置
        return null;
    }
    
    final int hash(Object k){
        int h = 0;
        if(useAltHashing){
            if (k instanceof String){
                return sun.misc.Hashing,stringHash32((String) k);
            }
            h = hashCode();
        }
        //二次散列,没有直接用hashCode的值,解决哈希冲突
        h ^= k.hashCode();

        //扰动函数,核心思想,增加值的不确定性
        h ^= (h>>>20) ^ (h>>>1);
        return h ^(h>>>7) ^ (h>>>1);
    }
    static int indexFor(int h,int length){
        return h&(length-1);//计算位置对应的公式  等效于:h&length求余 因为效率低,所以用 &运算
    }
    
    //添加元素的方法:
    void addEntry(int hash,K key,V value,int bucketIndex){
        //如果size>=threshold这个变量不满足,这个if不走
        if ((size>=threshold) && (null != table[bucketIndex])){
            resize(2*table.length);
            hash = (null != key) ?hash(key) : 0 ;
            bucketIndex = indexFor(hash,table.length);
        }
        //走这里创建Entry对象:
        createEntry(hash,key,value,bucketIndex);
    }
    
    void createEntry(int hash,K key,V value,int bucketIndex){
        //将下标位置上的元素给e
        Entry<K,V> e = table[bucketIndex];
        //封装对象,将这个对象给table[bucketIndex]
        table[bucketIndex] = new Entry<>(hash,key,value,e);
        size++;
    }

}

2)从代码中可以看出每一个存入Map的元素其实可以看做一个封装的值,包括(hash,k,y,index)分别对应哈希值,key,value,数组中所在的位置。

哈希值:对应hash()方法,主要的参数为底层数组中的key值,经过hashCode()方法求出哈希值,得到值后通过二次散列和扰动函数方法解决可能出现的哈希冲突,简单了来说就是通过方法得出更多的不同的哈希值,防止哈希冲突

哈希冲突:元素key值相同或者key值求出来的哈希值相等

final int hash(Object k){
    int h = 0;
    if(useAltHashing){
        if (k instanceof String){
            return sun.misc.Hashing,stringHash32((String) k);
        }
        h = hashCode();
    }
    //二次散列,没有直接用hashCode的值,解决哈希冲突
    h ^= k.hashCode();

    //扰动函数,核心思想,增加值的不确定性
    h ^= (h>>>20) ^ (h>>>1);
    return h ^(h>>>7) ^ (h>>>1);
}

key:存入数据中的key值,其中涉及到值的存储,以及发生哈希冲突,哈希碰撞的解决方法

方法:存入数据时,会先判断key是否为空(允许为空),获取哈希码,通过indefor方法获取在数组中的位置,判断位置上是否有元素,如果没有就直接放入,如果有,就比较哈希值,哈希值相同接着判断是否是同一个对象,不是则继续比较值,如果值和hash值都一样,就将后插入的值替换掉老值。

存储元素的方法:

public V put(K key,V value){//K:Integer V:Stringinde
    //对key进行判断是否为空,允许为空
    if(key == null)
        return putforNullKey(Value);

    //获取key的哈希码
    int hash = hash(key);
    //得到元素在数组中的位置
    int i = indexFor(hash,table.length);
    //如果放入的数组的位置上没有元素的话,那么直接添加就行了,不用走下面的for循环

    //如果满足e!= null的话,说明位置上已经有东西了
    for(Entry<K,V> e = table[i];e! = null;e = e.next){
        Object k;
        //发生哈希碰撞的时候,会先比较哈希值
        //比较key是否是一个对象,如果key是一个对象的话,equals就不比较了
        //如果不是同一个对象,会比较equals方法
        //如果hash值一样,equals值也一样,就会走这个
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))){
            V oldValue = e.value;
            e.Value = value;
            e.recordAccess(this);
            return oldValue;
        }
    }

    addEntry(hash,key,value,i); //哈希码,key,value,以及数组位置
    return null;
}

3)获取在数组中的位置

就是判断hashcod,qiuyu5e对数组长度求雨,得到位置

static int indexFor(int h,int length){
    return h&(length-1);//计算位置对应的公式  等效于:h&length求余 因为效率低,所以用 &运算
}

4)其他方法:

构造器:需要一个构造器,创建底层数组,可以自定义获取使用默认的空构造器

默认构造器:其中加载因子为0.75,数组长度为16

自定义构造器:自己传入加载因子和数组初始长度

扩容边界值:加载因子*数组长度

基本扩容1:直接判断数组长度,如果大于16,则直接长度*2.

基本扩容2:实际数组长度>=加载因子*初始数组长度,就扩展2倍,为了防止哈希冲突,因为当实际数组长度>=加载因子*初始数组长度时,很容易发生哈希碰撞(数组过小,实际数组都占的差不多了,很容易得到hash值相同)。

为什么加载因子不为1或者0.5,因为过小会导致空间浪费,过小就直接扩容了,过大会导致hash冲突,取中解决相对效果较好一些。

为什么扩容必须是原数组的两倍,不是其他倍数呢。

防止hash冲突,位置冲突,h%(length-1)等效于H%length求余的前提是length必须是2的倍数

例子:2的整数倍

2的

legth :8

hash 3   00000011

length-1 00000111

位置       00000011

hash2    00000010

length-1 00000111

位置      00000010

不是2的整数倍

length 9

hash 3   00000011

length-1 00001000

位置       00000000

hash 2   00000010

length-1 00001000

位置       00000000

s//构造器
    //1).空构造器,一般调用这个
    public HashMap(){this(DeFAULT_INITIAL_CAPACITY,DEFAULT_LOAD_FACTOR);}
    //2).带参构造器
    public HashMap(int initialCapacity,float loadFactor){//传入自定义数组长度,负载因子
        if (initialCapacity > MAXMUM_CAPACITY)
            initialCapacity = MAXMUM_CAPACITY;
        if (loadFactor<=0 || Float.isNaN(loadFactor))
            throw new IllegalAccessException("illegqal load factor:" + loadFactor);
        int capacity = 1;
        while (capacity<initialCapacity)
            capacity<<=1;  //如果小于输入的数组长度,就自增一倍
        this.loadFactor = loadFactor;
        threshold=(int)Math.min(capacity*loadFactor,MAXMUM_CAPACITY+1);//取自定义的负载因子乘当前数组的最大二次幂算出扩容边界值
        table = new Entry[capacity];   //将底层数组放入,生成一个Entry对象,内部由4个属性(key,value,哈希值,地址)
    }
void addEntry(int hash,K key,V value,int bucketIndex){
    //如果size>=threshold这个变量不满足,这个if不走
    if ((size>=threshold) && (null != table[bucketIndex])){
        resize(2*table.length);
        hash = (null != key) ?hash(key) : 0 ;
        bucketIndex = indexFor(hash,table.length);
    }
    //走这里创建Entry对象:
    createEntry(hash,key,value,bucketIndex);
}

void createEntry(int hash,K key,V value,int bucketIndex){
    //将下标位置上的元素给e
    Entry<K,V> e = table[bucketIndex];
    //封装对象,将这个对象给table[bucketIndex]
    table[bucketIndex] = new Entry<>(hash,key,value,e);
    size++;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值