集合(二)Map接口+源码分析(HashMap重点)

Map 接口

Map:双列数据,存储key-value对的数据

  • HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value

    • LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历。

      原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。对于频繁的遍历操作,此类执行效率高于HashMap。

  • TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序底层使用红黑树

  • Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value

    • Properties:常用来处理配置文件。key和value都是String类型

Map结构的理解

  • Map与Collection并列存在。用于保存具有 映射关系的数据:key-value

  • Map 中的 key 和 value 都可以是任何引用类型的数据

  • Map 中的 key 用Set来存放, 不允许重复,即同一个 Map 对象所对应的类,须重写hashCode()和equals()方法

  • 常用String类作为Map的“键”

  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到唯一的、确定的 value

  • Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和Properties。其中, HashMap是 Map 接口使用频率最高的实现类

Map中定义的方法

添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合

HashMap

面试题:

  1. HashMap的底层实现原理?

  2. HashMap 和 Hashtable的异同?

HashMap的结构

  • 所有的key构成的集合是Set:无序的、不可重复的 —> key所在的类要重写equals()和hashCode()

  • 所有的value构成的集合是Collection:无序的、可重复的 —>value所在的类要重写equals()

  • 一个键值对:key-value构成了一个Entry对象。

  • Map中的entry:无序的、不可重复的,使用Set存储所有的entry

  • 所有的entry构成的集合是Set:无序的、不可重复的

  • HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。

  • HashMap 判断两个 value 相等的标准是:两个 value 通过 equals() 方法返回 true。

HashMap的底层实现原理

DEFAULT_INITIAL_CAPACITY : HashMap的默认容量,16

DEFAULT_LOAD_FACTOR:HashMap的默认加载因子:0.75

threshold:扩容的临界值,=容量*填充因子:16 * 0.75 => 12

TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树:8

MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量:64

jdk7底层分析:
初始化

jdk7调用构造函数初始化,底层创建了长度是16的一维数组Entry[] table。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8iqCdwz9-1609142920120)(C:\Users\Bryce\Desktop\博文\images\hashmap.png)]

HashMap有四个构造方法,但是最终都会调用第一个HashMap(int,float),下面我们分析源码

public HashMap(int initialCapacity, float loadFactor) {
    //初始化数组大小小于0,抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                         		 initialCapacity);
    //初始化数组大小最大为默认最大值,最大值是1>>30=1073741824    
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    //加载因子要在0到1之间
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);

    //这下面三行代码是容量初始化,返回的是大于输入参数且最接近的2的整数次幂的数,默认为16,
    //如果是自己输入容量,例如15,经计算是2^4=16>15>2^3=8,初始容量还是16
    //如果是17,就是2^5=32>17>2^4=16,初始容量就是32
    // Find a power of 2 >= initialCapacity
    int capacity = 1;
    while (capacity < initialCapacity)
        capacity <<= 1;

    this.loadFactor = loadFactor;
    //临界值,影响扩容
    threshold = (int)Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);
    //创建底层数组
    table = new Entry[capacity];
    useAltHashing = sun.misc.VM.isBooted() &&
        (capacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    init();
}
添加数据
static int indexFor(int h, int length) {
    //位与运算获取数组存放位置,比取模运算效率快
    return h & (length-1);
}
public V put(K key, V value) {
    //存放null值
    if (key == null)
        return putForNullKey(value);
    //计算hash值
    int hash = hash(key);
    //计算数组中存放位置
    int i = indexFor(hash, table.length);
    //当e=null跳出循环,情况1
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {
        Object k;
        //当e.hash != hash,情况2
        //当!key.equals(k),情况3
        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);
    return null;
}

首先,调用key1所在类的hashCode()计算key1哈希值,此哈希值经过某种算法计算以后,得到在Entry数组中的存放位置。

  • 如果此位置上的数据为空,此时的key1-value1添加成功。 ----情况1

  • 如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:

    • 如果key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功。----情况2

    • 如果key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较:调用key1所在类的equals(key2)方法,比较:

      • 如果equals()返回false:此时key1-value1添加成功。----情况3

      • 如果equals()返回true:使用value1替换value2。

补充:关于情况2和情况3:此时key1-value1和原来的数据在同一位置以链表的方式存储。

底层扩容

在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

void addEntry(int hash, K key, V value, int bucketIndex) {
    //判断是否扩容
    if ((size >= threshold) && (null != table[bucketIndex])) {
        //扩容至两倍
        resize(2 * table.length);
        hash = (null != key) ? hash(key) : 0;
        bucketIndex = indexFor(hash, table.length);
    }
	//添加元素
    createEntry(hash, key, value, bucketIndex);
}
插入数据

在数组中某一个位置形成链表时,把旧的元素放到next中,新的元素替换旧元素之前的位置并指向旧的元素,简称头部插入

void createEntry(int hash, K key, V value, int bucketIndex) {
    //取出旧元素
    Entry<K,V> e = table[bucketIndex];
    //新元素替换旧元素
    table[bucketIndex] = new Entry<>(hash, key, value, e);
    size++;
}
//元素的构造方法
Entry(int h, K k, V v, Entry<K,V> n) {
    value = v;
    next = n;
    key = k;
    hash = h;
}
jdk8底层分析
初始化

jdk8初始化,并不创建数组,当有

public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap(int initialCapacity, float loadFactor) {
	// 初始化数组大小小于0,抛出异常
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    // 初始化数组大小最大为默认最大值
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    // 加载因子要在0到1之间
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    // threshold是根据当前的初始化大小和加载因子算出来的边界大小,
    // 当桶中的键值对超过这个大小就进行扩容
    this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}
/**
 * Returns a power of two size for the given target capacity.
 */
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
添加数据和插入数据

put()方法

 public V put(K key, V value) {
     //先计算key的hash值,然后再调用putVal
     return putVal(hash(key), key, value, false, true);
 }

调用putVal()

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //当table为null,进入resize()初始化创建函数
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //计算元素在数组中存放位置,如果位置上为null,则直接添加,情况1
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //如果此位置不为空,新元素和旧元素hash相等,k值相同,直接替换
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 如果是红黑树结点的话,进行红黑树插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //开始循环判断是否符合添加元素条件,从第二个元素开始
            for (int binCount = 0; ; ++binCount) {
                //赋值下一个元素并判断是否为空
                if ((e = p.next) == null) {
                    //从尾部进行插入元素
                    p.next = newNode(hash, key, value, null);
                    //如果链表长度大于8时,将链表转红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //判断是否添加成功
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //更新p节点
                p = e;
            }
        }
        // 如果存在这个映射就覆盖
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            // 判断是否允许覆盖,并且value是否为空
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            // 回调以允许LinkedHashMap后置操作
            afterNodeAccess(e);
            return oldValue;
        }
    }
     // 更改操作次数
    ++modCount;
    // 大于临界值,进行扩容至两倍,并将原先的数组中的元素放到新数组中
    if (++size > threshold)
        resize();
    // 回调以允许LinkedHashMap后置操作
    afterNodeInsertion(evict);
    return null;
}
底层扩容

第一次put进行resize()会创建数组,再次进入则是扩容,扩容操作十分消耗性能

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //进行初始化数组大小和临界值,如果都没指定直接进入else使用默认值
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    //创建好数组
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //旧tab的值转移到新tab,重新计算下标
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    //进入split方法,当数节点小于等于6时,树结构转换为链表结构
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}
转换红黑树节点

将链表节点转为红黑树节点

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //数组为空或长度小于64,继续扩容
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            //将链表节点转红黑树节点
            TreeNode<K,V> p = replacementTreeNode(e, null);
            //tl为空是第一次遍历,将头结点赋值给hd
            if (tl == null)
                hd = p;
            else {
                //如果不是第一次遍历,则处理当前节点的prev属性和上一个节点的next属性
                p.prev = tl;
                tl.next = p;
            }
            //用于在下一次循环中作为上一个节点进行一些链表的关键操作p.prev = tl
            tl = p;
        } while ((e = e.next) != null);
        //将table该索引位置赋值给新转的TreeNode的头结点
        if ((tab[index] = hd) != null)
            //如果该节点不为空,则以以头结点hd为根节点,构建红黑树
            hd.treeify(tab);
    }
}
总结
jdk1.7和jdk1.8中HashMap的区别
  1. 调用HashMap()构造函数时: 1.7 创建数组;1.8 首次调用put()方法时,创建数组
  2. 数据结构的差异: 1.7 数组 + 链表 ;1.8 数组 + 链表或红黑树
  3. 底层的数组是: 1.7 Entry[];jdk 8 Node[],
  4. 链表的插入方式优化: 1.7 头插法; 1.8 尾插法
  5. 扩容的改进: 1.7 全部 rehash ;1.8 简单判断(判断新增位是 0 还是 1),要么原位置,要么【原位置 + 旧容量】的位置
  6. 插入数据的差别: 1.7 先判断是否要扩容再插入; 1.8 先插入,插入完成再判断是否需要扩容
链表和红黑树的互相转换
  • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
  • 当数组的某一个索引位置上的元素以红黑树形式存在的数据个数 <=6时,此时此索引位置上的所数据改为使用链表
常用方法:
  • 添加:put(Object key,Object value)
  • 删除:remove(Object key)
  • 修改:put(Object key,Object value)
  • 查询:get(Object key)
  • 长度:size()
  • 遍历:keySet() / values() / entrySet()

LinkedHashMap的底层实现原理

  • LinkedHashMap 是 HashMap 的子类

  • 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序

  • 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代顺序:迭代顺序与 Key-Value 对的插入顺序一致

源码中:

static class Entry<K,V> extends HashMap.Node<K,V> {
     Entry<K,V> before, after;//能够记录添加的元素的先后顺序
     Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
     }
 }

TreeMap

  • TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。TreeMap 可以保证所有的 Key-Value 对处于有序状态。

  • TreeSet底层使用红黑树结构存储数据

  • TreeMap 的 Key 的排序:

    • 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
    • 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口
  • TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。

public class User implements Comparable{
    private String name;
    private int age;

    public User() {}

    public User(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {return name;}
    public void setName(String name) {this.name = name;}
    public int getAge() {return age;}
	public void setAge(int age) {this.age = age;}

    @Override
    public String toString() {
        return "User{ name='" + name + '\'' +
                ", age=" + age + '}';
    }

    @Override
    public boolean equals(Object o) {
        System.out.println("User equals()....");
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        if (age != user.age) return false;
        return name != null ? name.equals(user.name) : user.name == null;
    }

    @Override
    public int hashCode() { //return name.hashCode() + age;
        int result = name != null ? name.hashCode() : 0;
        result = 31 * result + age;
        return result;
    }

    //按照姓名从大到小排列,年龄从小到大排列
    @Override
    public int compareTo(Object o) {
        if(o instanceof User){
            User user = (User)o;
//            return -this.name.compareTo(user.name);
            int compare = -this.name.compareTo(user.name);
            if(compare != 0){
                return compare;
            }else{
                return Integer.compare(this.age,user.age);
            }
        }else{
            throw new RuntimeException("输入的类型不匹配");
        }
    }
}

public class TreeMapTest {

    //向TreeMap中添加key-value,要求key必须是由同一个类创建的对象
    //自然排序
    @Test
    public void test1(){
        TreeMap map = new TreeMap();
        User u1 = new User("Tom",23);
        User u2 = new User("Jerry",32);
        User u3 = new User("Jack",20);
        User u4 = new User("Rose",18);

        map.put(u1,98);
        map.put(u2,89);
        map.put(u3,76);
        map.put(u4,100);

        Set entrySet = map.entrySet();
        Iterator iterator1 = entrySet.iterator();
        while (iterator1.hasNext()){
            Object obj = iterator1.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "---->" + entry.getValue());
        }
    }

    //定制排序
    @Test
    public void test2(){
        TreeMap map = new TreeMap(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                if(o1 instanceof User && o2 instanceof User){
                    User u1 = (User)o1;
                    User u2 = (User)o2;
                    return Integer.compare(u1.getAge(),u2.getAge());
                }
                throw new RuntimeException("输入的类型不匹配!");
            }
        });
        User u1 = new User("Tom",23);
        User u2 = new User("Jerry",32);
        User u3 = new User("Jack",20);
        User u4 = new User("Rose",18);

        map.put(u1,98);
        map.put(u2,89);
        map.put(u3,76);
        map.put(u4,100);

        Set entrySet = map.entrySet();
        Iterator iterator1 = entrySet.iterator();
        while (iterator1.hasNext()){
            Object obj = iterator1.next();
            Map.Entry entry = (Map.Entry) obj;
            System.out.println(entry.getKey() + "---->" + entry.getValue());

        }
    }
}

Hashtable

  • Hashtable是个古老的 Map 实现类,JDK1.0就提供了。

  • 与HashMap的相同

    • 与Hashtable实现原理和HashMap相同,功能相同。
    • 底层都使用哈希表结构,查询速度快,很多情况下可以互用。
    • 与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序
    • Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
  • 与HashMap的不同

    • Hashtable 不允许使用 null 作为 key 和 value
    • 不同于HashMap,Hashtable是线程安全的。

Properties

  • Properties 类是 Hashtable 的子类,该对象用于处理属性文件

  • 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型

  • 存取数据时,建议使用setProperty(String key,String value)方法和getProperty(String key)方法

Properties pros = new Properties();

pros.load(new FileInputStream("jdbc.properties"));

String user = pros.getProperty("user");

System.out.println(user);

问题总结:

  1. HashMap的⼯作原理(简单描述下回答思路)
  • 利用 key 的 hashCode 重新 hash 计算出当前对象的元素在数组中的下标
  • 存储时,如果出现 hash 值相同的 key,此时有两种情况。(1) 如果 key 相同,则覆盖原始值;(2) 如果 key 不同(出现冲突),则将当前的 key-value 放入链表中
  • 获取时,直接找到 hash 值对应的下标,在进一步判断 key 是否相同,从而找到对应值。
  • 理解了以上过程就不难明白 HashMap 是如何解决 hash 冲突的问题,核心就是使用了数组的存储方式,然后将冲突的 key 的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
  1. 底层数据结构

jdk7 Entry数组+链表

jdk8 Node数组+链表+红黑树

  1. 初始化核⼼参数

jdk7 创建数组和临界值

jdk8 无参构造只设置加载因子,有参构造

还会用位运算计算出大于输入参数且最接近他的2次幂的数作为数组大小

  1. hash算法

jdk7 用了向右移位的方式将高低位二进制特征混合起来,来解决低位变化不大的难点

jdk8 hashcode右移 16,再与原 hashcode 做异或运算,可以将高低位二进制特征混合起来

hash算法的运用也是数组大小要用2次幂的原因,也让kv值在数组中更均匀的分布

  1. 寻址算法

长度减一和hash进行位与运算,位运算比取模快

  1. hash冲突

HashMap 中哈希碰撞(冲突)的条件是指不同 key 被映射到同一个桶。 当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了。

链地址法:

所有哈希地址为 i 的元素构成一个成为同义词链的单链表,并将单链表的头指针存在哈希表的第 i 个单元中,因为查找、插入、删除主要在同义词链中进行。链地址法适用于经常进行插入和删除的情况。HashMap 中使用的就是链地址法

  1. 扩容机制

jdk7 当个数超过容量大小,或者超出临界值(且要存放的位置位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来。

jdk8 当个数大于临界值,进行扩容至两倍,并将原先的数组中的元素放到新数组中,重新计算位置,扩容发生在存放后

  1. put⽅法,get⽅法执⾏流程

get

1. 通过 hash 值获取 key 映射到的桶 
2. 根据该桶的存储结构决定是遍历红黑树还是遍历链表 
3. table [i] 的首个元素是否和 key 一样,如果相同则返回该 value 
4. 如果不同,先判断首元素是否是红黑树节点,如果是,则去红黑树中遍历查找,反之去链表中遍历查找。

put

1. 对 key 的 hashCode () 做 hash 运算,计算 index
2. 查看 table [index] 是否存在数据,没有,则构造一个 Node 节点存放在其中;
3. 存在数据,说明发生了 hash 冲突,继续判断 key 是否相等,相等,用新的 value 替换原数据;
4. 若不相等,判断当前节点类型是不是树形节点,如果是树形节点,创造树形节点插入红黑树中;(如果当前节点是树形节点证明当前已经是红黑树了)
5. 若不为树形节点,创建普通 Node 加入链表中;判断链表长度是否大于 8 并且数组长度大于 64,则链表转换为红黑树;
6. 插入后,判断当前节点数是否大于阈值,如果大于,则扩容为原数组的两倍
  1. hashMap是否是线程安全
    HashMap 的实现里没有锁的机制,因此它是线程不安全的。
    不安全情况1
    A线程:map.put(1,2);
    B线程:map.get(1);
    B 线程获取的值本来应该是 2,但是如果 B 线程在刚到达获取的动作还没执行的时候,
    线程执行的机会又跳到线程 A,此时线程 A 又对 map 赋值 如:map.put (“1”,“3”);
    然后线程虚拟机又执行线程 B,B 取到的值为 3,这样 map 中第一个存放的值 就会丢失。。。。。
    不安全情况2
    假如两个线程进行kv值插入,hash相同,当一个线程比较了hash值后被挂起,另一个线程正常插入,前一个线程继续执行,因为已经比较过hash了,会直接进行插入,导致值得覆盖
    不安全情况3
    map可以设置或者默认加载因子,默认为 0.75,那么阀值就是 12,所以在往 HashMap 中 put 的值到达 12 时,它将自动扩容两倍,如果两个线程同时遇到 HashMap 的大小达到 12 的倍数时,就很有可能会出现在将 oldTable 转移到 newTable 的过程中遇到问题,从而导致最终的 HashMap 的值存储异常。
    1.7中进行扩容时候因为头插法容易形成环形链和数据丢失
  2. HashMap 和 Hashtable的异同?
    1、Hashtable 是早期提供的接口,HashMap 是新版 JDK 提供的接口。
    2、Hashtable 继承 Dictionary 类,HashMap 继承了AbstractMap类,都实现 Map 接口。
    3、Hashtable 线程安全,HashMap 线程非安全。
    4、Hashtable 不允许 null 值,HashMap 允许 null 值。
    5、Hashtable 默认容量为11,,扩容时,将容量变为原来的 2 倍加 1,而 HashMap默认容量16, 扩容时,将容量变为原来的 2 倍。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值