Java集合(四)

Map接口

常用实现类的继承关系
继承关系

HashMap

继承关系

注意

  • HashMap对于添加进去的键值对,它是不保证顺序的,添加顺序是可能和存储顺序是不一致的
  • HashMap对于元素的添加和查找操作的时间复杂度是常数时间复杂度
  • 是线程不安全的
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable

成员变量

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//默认为16的初始化长度
static final int MAXIMUM_CAPACITY = 1 << 30;//最大为2^30
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认为0.75的装载因子
static final int TREEIFY_THRESHOLD = 8;//单点“挂点”(bin)阈值,超过这个值变为红黑树存储
static final int UNTREEIFY_THRESHOLD = 6;//红黑树中存储的节点数少于这个值就会转换为链表存储
static final int MIN_TREEIFY_CAPACITY = 64;//bins链表转换为红黑树时的最小hashmap的长度(也就是说如果bins中装的节点数超过了上面那个TREEIFY_THRESHOLD,但是hashmap长度还没有超过MIN_TREEIFY_CAPACITY,就要先扩容,而不是转换为红黑树)

transient Set<Map.Entry<K,V>> entrySet;
transient int size;//map 中实际有的键值对的数量
transient int modCount;//每次改变映射和内部结构就会+1
final float loadFactor;//装载因子
int threshold;//下一次需要扩展时候的map容量(一定是一个2的整数次幂);用tableSizeFor(initialCapacity)函数算出不小于initialCapacity的最小二进制数
transient Node<K,V>[] table;//Node数组,存放添加的元素

/*插入hashmap中的实际元素的结构,一个个的node节点
*/
static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;//指向同hash到同一位置的节点

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

        public final K getKey()        { return key; }
        public final V getValue()      { return value; }
        public final String toString() { return key + "=" + value; }

        public final int hashCode() {//计算添加元素的hash值相同时,需要调用equals判断两者到底想不想等,相等的话就不能添加
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }

        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }

        public final boolean equals(Object o) {//在两个元素的hash值相同时需要
            if (o == this)
                return true;
            if (o instanceof Map.Entry) {
                Map.Entry<?,?> e = (Map.Entry<?,?>)o;
                if (Objects.equals(key, e.getKey()) &&
                    Objects.equals(value, e.getValue()))
                    return true;
            }
            return false;
        }
    }

还有红黑树结构TreeNode,源码太长就不贴了,后面用到会截取分析

初始化

几个名词的解释:

  • capacityHashMap的容量
  • load factor:装载因子 = 实际元素个数 / 容量;当实际元素个数 > 容量 * 装载因子,HashMap就要扩容了,一把来说,这个值设置为 0.75
  • bin:挂在节点数组上的链表
public HashMap() {
        this.loadFactor = DEFAULT_LOAD_FACTOR; //其他成员变量赋值为默认值
    }
     public HashMap(int initialCapacity) {//自定初始容量
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    public HashMap(int initialCapacity, float loadFactor) {//自定初始容量和装载因子
        if (initialCapacity < 0)//容量小于零抛异常
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)//容量大于max则设置为max
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))//装载因子为0或者不是有效输入抛异常
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

添加与修改元素

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

插入过程:

  • 根据插入的(k,v)的键k,调用自己类中重写的hashCode()计算出一个哈希值
  • 哈希值根据一些算术运算对应出一个table中的位置
    • 如果这个位置没有元素就直接插入
    • 如果这个位置有元素b,c,d,e…了
      • 通过循环比较当前元素和这些元素的哈希值,如果哈希值都不同,插入到同一位置的链末尾上(注意在java7之前是使用头插的)
      • 如果存在该元素和链上某一个的元素哈希值相同,就调用equals()方法,如果不相等也就插入到同一位置的链上
      • 如果equals()也是相同的,那么表明两个元素是相同的元素,更新该位置上的键值
  • 代码如下:
/*hash:插入键的hash值
  key:键
  value:值
  onlyIfAbsent:true的话不改变现有的值
  evict:false就是在creation mode
*/
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;//初始为空的时候插入元素,设置为16的默认值
            //(length - 1) & hashCode()计算元素的对应防止位置
        if ((p = tab[i = (n - 1) & hash]) == null)//根据哈希值计算出来的位置没有元素,直接插入
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//要插入的元素和对应位置的元素key值相同
            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);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // 超过阈值,链表转换为红黑树存储同一位置节点了
                            treeifyBin(tab, hash);
                        break;
                    //如果是相同元素,标记更新
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)//允许更改旧值或旧值为空,添加新值;否则保持不变
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

由上面的插入过程可以看出,为了保证确定待插入元素key值是是否已经存在,需要先调用元素key对应的类中hashCode()方法得到对应的插入位置;遇到相同的哈希值的两个元素需要使用equals()方法判断两个元素是否相等;
所以被添加到HashMap中的key所对应的类必须重写hashCode()equals()方法

jdk8中哈希到同一位置的元素使用尾插的原因:未完待续

修改元素和插入过程是一样的,同样的元素put就会覆盖原有元素

在插入过程中但实际元素个数超过阈值loadFactor * threshold时,就会触发Node[]数组的扩容机制,具体的扩容机制如下:

final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;//创建一个临时Node数组指向现有的Node数组
        int oldCap = (oldTab == null) ? 0 : oldTab.length;//获得现有数组的长度;如果是默认初始化的,长度为0,否则为数组长度
        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; //阈值也变为双倍
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {//初始阈值就是用的默认初始化0,数组也没有插入过元素
            newCap = DEFAULT_INITIAL_CAPACITY;//新数组长度为默认的16长度
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//新阈值 = 0.75 * 16
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {//新数组长度确定之后重新哈希移动元素
        	//省略中间步骤......
        }
        return newTab;
    }

删除元素

public V remove(Object key) {//删除时查找元素要满足哈希值相等,并且equals()判等
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

遍历
我们在使用中都知道能使用HashMap中的entrySet()keySet()values()得到对应的(k,v)key的set或者values的Collection,然后利用Collection中的Iterator()遍历,就不赘述了。但是我还发现了一个问题就是以上的三个结构在put()中都没有提及,更不用说维护了,那为什么可以调用这些方法得到相应的set呢?我于是翻看了这几个的源码:

 public Set<Map.Entry<K,V>> entrySet() {
        Set<Map.Entry<K,V>> es;
        return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;//这里涉及到一个类:EntrySet,我们往下看
    }
    //EntrySet类中获取iterator的方法如下
    public final Iterator<Map.Entry<K,V>> iterator() {
            return new EntryIterator();
        }
        //继续看类EntryIterator,哇哈哈,终于找到了我们的而目标 HashIterator
        final class EntryIterator extends HashIterator
        implements Iterator<Map.Entry<K,V>> {
        public final Map.Entry<K,V> next() { return nextNode(); }
    }
    //在这个类的构造方法中我们找到了答案
    HashIterator() {
            expectedModCount = modCount;
            Node<K,V>[] t = table;//这里就是获取了我们一直在更新的 Node数组
            current = next = null;
            index = 0;
            if (t != null && size > 0) { // advance to first entry
                do {} while (index < t.length && (next = t[index++]) == null);
            }
        }

通过查看其他两种结构的源码都能发现他们都实现了这个HashIterator抽象类,归到低就是通过entrySet()keySet()values()这些结构遍历HashMap就是获取了我们一直维护的Node[] table而已,并没有额外的维护这些结构,从一个片面上看我梦也可以理解成这样:键值对(k,v存到HashMap中,key“保存”在一个Set集合中,是不能重复的,而values“保存”在Collection当中,可以重复没有顺序。

ConcurrentHashMap
HashMap一样的底层存储结构:jdk1.7之前使用的是分段锁机制,不同数据段的数据多线程访问不会出现竞争;jdk1.8之后并发控制使用 synchronized 和 CAS(compare and swap)算法来操作。操作时只会锁当前链表或者红黑树的头节点。Hashtable就是一把锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值