0
点赞
收藏
分享

微信扫一扫

【Java集合】Map接口、HashMap、HashTable源码详解

文章目录

一、Map

Java提供了专门的集合类用来存放这种对象关系的对象,即java.util.Map接口。

Map接口下的集合与Collection接口下的集合存储数据的形式不同。

Collection中的集合称为单列集合,Map中的集合称为双列集合。
Map中的集合不能包含重复的键,值可以重复,每个键只能对应一个值。
所以key只能有一个null,value可以有多个null。

Map接口中的集合都有两个泛型变量<K,V>,在使用时,要为两个泛型变量赋予数据类型。两个泛型变量<K,V>的数据类型可以相同,也可以不同。所以,key和value可以使任何引用类型,会封装到HashMap$Node对象中。 Node类实现了Map.Entry接口。
在这里插入图片描述

1.1 Entry键值对对象

为了方便遍历,Map还会创建一个EntrySet集合,该集合存放Entry类型的元素。EntrySetHashMap的一个内部类。所以实际上EntrySet里面存放的元素还是Node类型的元素。
在这里插入图片描述
在这里插入图片描述
在Entry类中,有两个方法,getKey()getValue(),可以获取Node中的key和value:
在这里插入图片描述

还有个方法keySet(),可以获得map中key的Set集合对象,拿到Set可以遍历得到对应的value值
在这里插入图片描述

常用方法

既然Entry表示了一对键和值,那么也同样提供了获取对应键和对应值得方法:

K getKey():获取Entry对象中的键
V getValue():获取Entry对象中的值

在Map集合中也提供了获取所有Entry对象的方法:
Set<Map.Entry<K,V>> entrySet():获取到Map集合中所有的键值对对象的集合(Set集合)

遍历键值对方式

键值对方式:即通过集合中每个键值对(Entry)对象,获取键值对(Entry)对象中的键与值。

步骤:

所以遍历Map的一种方法:

Map map = new HashMap();
map.put("aaa", "arbor");
map.put("bbb", "boyboy");
Set set = map.entrySet();
for (Object obj : set) {
	Map.Entry entry = (Map.Entry) obj;
	System.out.println("key: " + entry.getKey() + "\t value: " + entry.getValue());
}

在这里插入图片描述

1.2 Map常用方法

V put(K key, V value):把指定的键与指定的值添加到Map集合中。

V remove(Object key):把指定键对应的键值对元素在Map集合中删除,返回被删除元素的值。
clear():清空Map中的元素。

V get(Object key):根据指定的键,在Map集合中获取对应的值。
int size():获取元素个数。

containsKey(Object key):判断集合中是否包含指定的键。
boolean isEmpty():判断Map是否为空。

Set<K> keySet():获取Map集合中所有的键,存储到Set集合中(只是引用,并非复制)。
Collection<V> values():获取Map集合中所有的值,存储到Collection集合中(只是引用,并非复制)。

Set<Map.Entry<K,V>> entrySet():获取到Map集合中所有的键值对对象的集合(Set集合)。

public class MapDemo {
    public static void main(String[] args) {
        //创建 map对象
        Map<String, String> map = new HashMap<>();

        //添加元素到集合
        map.put("黄晓明", "杨颖");
        map.put("邓超", "孙俪");
        System.out.println(map);

        //String remove(String key)
        System.out.println(map.remove("邓超"));
        System.out.println(map);

        // 想要查看 黄晓明的媳妇 是谁
        System.out.println(map.get("黄晓明"));
        System.out.println(map.get("邓超"));    
    }
}

1.3 遍历Map

① 获取所有的Key,找对应的Value

Map map = new HashMap();
map.put("aaa", "AAA");
map.put("bbb", "BBB");
map.put("ccc", "CCC");
Set keySet = map.keySet();
// 这里也可以使用迭代器,不做演示啦
for (Object key : keySet) {
    System.out.println(key + "-->" + map.get(key));
}

② 直接取出所有的Value

Map map = new HashMap();
map.put("aaa", "AAA");
map.put("bbb", "BBB");
map.put("ccc", "CCC");
Collection values = map.values();
// 这里也可以使用迭代器,不做演示啦
for (Object value : values) {
    System.out.println(value);
}

③ 使用Entry对象遍历

这个上面遍历键值对方式有写过

Map map = new HashMap();
map.put("aaa", "arbor");
map.put("bbb", "boyboy");
Set set = map.entrySet();
for (Object obj : set) {
	Map.Entry entry = (Map.Entry) obj;
	System.out.println(entry.getKey() + "-->" + entry.getValue());
}

二、HashMap

HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

HashMap是Map接口中使用频率最高的实现类。
Key不能重复,Value可以重复,可以使用null来做Key或者Value。

如果添加相同的Key,会替换原有该Key的Value值,相当于修改。
该源码在HashMap的putVal()方法:
在这里插入图片描述

和HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的。
HashMap没有实现同步,所以线程不安全。

HashMap底层存放Key和Value的是Node对象,该对象实现了Map.Entry<K,V>
HashMap在jdk7使用的是【数组+链表】,在jdk8使用的是【数组+链表+红黑树】。
在这里插入图片描述

2.1 HashMap底层机制

HashMap底层维护的是Node类型的数组table,默认为null。

当创建对象时,将加载因子(loadfactor)初始化为0.75。

当添加key-val时,通过key的哈希值得到在table的索引,然后判断该索引处是否有元素,如果没有,直接添加;如果有,则判断索引处元素的key是否和准备添加的key是否相等,如果相等,替换val,如果不相等,判断是树结构还是链表结构,做出相应的处理;如果添加时发现容量不足,则扩容。

第一次添加,table的容量会扩容为16,临界值(threshold)是12(容量 * 加载因子)。

之后扩容,table的容量为原来的2倍,临界值也是原来的2倍,依次类推。

在Java8中,如果一条链表的原数个数超过了TREEIFY_THRESHOLD(默认是8),并且table的容量大于等于MIN_TREEIFY_CAPACITY(默认为64),就会进行树化(红黑树)


执行下面代码:

HashMap map = new HashMap();
map.put("java", 10);
map.put("php", 10);
map.put("java", 20);

看源码:
① 执行构造方法,初始化加载因子,加载因子为0.75
在这里插入图片描述
并且此时的HashMap$Node[] table = null

② 执行put方法:
这里会先执行hash方法计算key的哈希值,然后再进入putVal方法
在这里插入图片描述
hash方法:
在这里插入图片描述

③ 第一次进入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;
    // 1. 因为第一次,table为空,所以第一个if条件成立
    if ((tab = table) == null || (n = tab.length) == 0)
   		// 2. 然后调用resize方法扩容table,走完这一步
   		// 走完这一步,table的容量为16,因为使用的是默认值
        n = (tab = resize()).length;
    // 3. 因为是第一次添加,所以计算出索引的位置是没有元素的
    if ((p = tab[i = (n - 1) & hash]) == null)
    	// 没有元素的话,直接把key-val在这里添加到索引的位置
        tab[i] = newNode(hash, key, value, null);
    else {
		// 这里还有代码,先省略...
    }
    // 4. 添加修改次数和长度
    ++modCount;
    // 5. 判断是否需要扩容数组:
    // threshold是临界值,16*0.75=12,因为第一次添加,不到临界值,不扩容
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize方法:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    // 1. 如果是第一次调用put方法,这里oldCap为0
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    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
    	// 2. 如果第一次调用put方法,会进入这里
    	// 会使用默认初始容量16
        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;
    // 3. new一个Node类型的数组【核心】
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    
    // ...这里还有很多代码  没看了  不是很重要
    
    return newTab;
}

④ 第二次进入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;
    // 1. 因为第二次,table不是null,长度也不为0,这里if不会进去
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 第二次添加的hash计算出的索引和第一次的不一样
    // 所以计算出索引的位置是没有元素的
    if ((p = tab[i = (n - 1) & hash]) == null)
    	// 没有元素的话,直接把key-val在这里添加到索引的位置
        tab[i] = newNode(hash, key, value, null);
    else {
		// 这里还有代码,先省略...
    }
    // 3. 添加修改次数和长度
    ++modCount;
    // 4. 判断是否需要扩容数组
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

⑤ 第三次进入putVal方法:
这里第三次的key和第一次的key是一样的

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    // 1. table不为null,if不会进去
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 这里通过hash计算出的索引和第一次的一样
    // 第一次已经在该索引处添加了一个元素了,所以这里if进不去
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    	// 因为索引的位置已经有元素了,所以会进入else里面
        Node<K,V> e; K k;
        // 3. 如果hash相同,内容也相同,就使用现在的val替换掉原来的val
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        // 4. 如果当前索引的位置是一个红黑树,则按照红黑树的方式添加元素
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        // 5. 如果现在该索引位置还是链表,则会和链表中的元素一个个比较
        // 比较是否和链表中的元素相同,相同的话替换val
        // 不同的话则添加到链表末尾
        // 如果table满足树化要求的话,则会进行树化
        else {
            for (int binCount = 0; ; ++binCount) {
            	// 5.2 如果没有与原来key相同的,就将新的元素放在链表的最后
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    // 5.3 加入链表后,判断链表的节点数是否到达8个了
                    // 如果到了8个,就进入转换为红黑树的方法
                    // 进入该方法不一定会树化,还需要table的长度大于等于64
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                // 5.1 这里就是比较key与原来已经存在的key是否相同
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        // 6. 在这里会被替换掉原来的val
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    // 7. 添加修改次数和map的长度并判断是否需要扩容数组
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

剪枝:
假如有一条链表已经变成一颗红黑树,并且删除了一定元素后,会将红黑树重新转换为链表(数据量小时,没必要红黑树)。

2.2 触发扩容、树化情况

模拟树化情况,重写hashCode方法:

class A {
    private int num;
    public A(int num) {
        this.num = num;
    }
    @Override
    public int hashCode() {
        return 100;
    }
}

这样,每个A对象的hashCode值都是100,会更容易触发树化机制。
因为没有重写equals方法,所以每个对象又不相同。

在map中存入12个对象:

HashMap map = new HashMap();
for (int i = 0; i < 12; i++) {
    map.put(new A(i), "hello");
}

① 当一次次put时,可以看到所有的key-val都挂在了索引为4的链表上
在这里插入图片描述
② 当i等于8时,也就是第9次进行put,此时索引为4的链表是已经8个节点了,此时的第9次put,会触发扩容机制

  • 先判断链表的长度是否大于等于8
  • 如果大于等于8,进入treeifyBin方法
  • 再判断table表的长度是否大于等于64
  • 如果没有大于等于64,则进行扩容

可以看到,元素为9的时候,table的长度为32,以此类推,元素数量为10的时候,table表会扩容到64。
在这里插入图片描述
在这里插入图片描述

③ 此时,这个map满足table的长度大于等于64,链表的长度大于等于8,如果再添加元素,则会将该链表进行树化。但是table不会再被扩容了。

④ 如果再次加入元素,putVal方法会走这条线:

因为此时的p节点已经是红黑树了
在这里插入图片描述

⑤ 而table的扩容是数组的元素到了临界值(数组的长度 * 0.75)就会进行扩容。

三、HashTable

基本介绍:

HashTbale在方法上加了synchronized关键字。
在这里插入图片描述

HashTable底层

① HashTable底层也是个数组,初始化大小为11,数组的类型是HashTable$Entry,这个Entry类实现了Map$Entry
在这里插入图片描述
② 加载因子是0.75,所以第一次的临界值是8 = 11 * 0.75

扩容机制

  • 数组的元素要超过8个(临界值)才进行第一次扩容
  • 第一次扩容后,table的长度为23,(原先的长度 * 2 + 1)
  • 新的临界值为:17 = 23 * 0.75

当执行put方法时,实际上添加到table的方法是addEntry方法。
在这里插入图片描述
在这里插入图片描述

四、HashMap和HashTable的比较

JDK版本线程安全(同步)效率允许null键null值
HashMap1.2不安全可以
HashTable1.0安全较低不可以
举报

相关推荐

0 条评论