Map&Set的理解

Set子接口

  • 特色:无序、无下标、元素不可重复。
  • 方法:所有继承自Collection中的方法。

Set实现类

  • HashSet:
    • 基于HashCode实现了不重复。
    • 当存入元素的哈希码相同时,会调用equals进行确认,如结果为true,则拒绝后者存入。
  • TreeSet:
    • 基于排列顺序实现元素不重复。
    • 实现了SortedSet接口,对集合元素自动排序。
    • 元素对象的类型必须实Comparable接口,指定排序规则。
    • 经过CompareTo方法肯定是否为重复元素。

HashSet分析

  • 无序性
    • 不等于随机性.存储的数据在底层数组中并不是按照数组索引的顺序添加,而是根据数据的哈希值存放
  • 不可重复性
    • 保证添加的元素按照equals()判断时,不能返回true.(即相同元素只能添加一个.)
# 添加元素的过程
	向HashSet添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,
	此哈希值接着经过某种算法计算出在HashSet底层数组中的存放位置,判断数组此位置上是否已经有元素:
		若是此位置上没有其余元素,则元素a添加成功.  ---> 状况1
		若是此位置上有其余元素b(或以链表形式存在的多个元素),则比较元素a与元素b的hash值:
			若是hash值不相同,则元素a添加成功.  ---> 状况2
			若是hash值相同,进而须要调用元素a所在类的equals()方法:
				equals()返回true,元素a添加失败
				equals()返回false,则元素a添加成功.  --->状况2

-	对于添加成功的状况2和状况3而言:元素a与已存在指定索引位置上数据以链表的方式存储.
	jdk 7: 元素a放到数组中,指向原来的元素.
	jdk 8: 原来的元素在数组中,指向新放入的元素a

Map父接口

  • 特色:存储一对数据(Key-Value),无序,无下标,键不可重复,值可重复。
  • 方法:
    • V put(K key,V value) //将对象存入到集合中,关联键值。key重复则覆盖原值。
    • Object get(Object key) //根据键获取对应的值。
    • Set keySet() //返回全部key
    • Collection values() //返回包含全部值的Collection集合
    • Set<Map.Entry<K,V>> entrySet() //键值匹配的Set集合。

Map集合的实现类

  • HashMap:
    • JDK1.2版本,线程不安全,运行效率快;容许用null 做为key或是value。
  • Hashtable:
    • JDK1.0版本,线程安全,运行效率慢;不容许null做为key或是value。

Map实现的底层原理:java

Map是个链表数组,数组中的每一个元素都是一个Map.Entry对象,同时Entry对象是个链表的节点,在这个链表的节点上能够经过next指向他的下一个节点。node

1.7的结构以下:算法

Map实现类的结构:

# 1 Map实现类的结构
- |---Map:存储键值对的数据  ---相似于高中的函数 y=f(x)
	|---HashMap:做为Map的主要实现类;线程不安全 可是效率高;存储null的k和v
		|---LinkedHashMap:保证在遍历map元素时,能够按照添加的顺序实现遍历.
			缘由:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个元素和后一个元素
	|---TreeMap:保证按照添加的k-v对进行排序,实现排序遍历.此时考虑key的天然排序或定制排序
		底层使用红黑树
	|---HashTable:做为古老的实现类:线程安全,效率低;不能存储null的key和value
		|---Properties:经常使用来处理配置文件.key和value都是String类型
		
* HashMap的底层: 数组+链表(jdk 7 以前)
			  数组+链表+红黑树 (jdk 8)
			  
# 2 Map结构的理解
	Map中的key : 无序的、不可重复的,使用Set存储全部的key
		---> 当使用hasnMap时key所在的类要重写equals()和hashCode()
	Map中的value:无序的、不可重复的、使用Collection存储全部的value
	一个键值对:key-value构成了一个Entry对象
	Map中的entry : 无序的、不可重复的,使用Set存储全部的entry
# 3 HashMap的底层实现原理
	在实例化之后,底层建立了长度是16的一维数组Entry[] table.
	...执行屡次put后...
	map.put(key1,value1);
	1). 首先调用key1所在类的hashCode() 计算key哈希值,此哈希值通过某种算法计算之后,获得在Entry数组中的存放位置.
	2). 若是此位置上的数据为空,此时的key1-value1添加成功 ---> 状况一
		若是此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式存在)),比较key1和已经存在的一个或多个数据的哈希值:
			3). 若是key1的哈希值与已经存在的数据的哈希值都不相同,此时key1-value1添加成功 ---> 状况2
				若是key1的哈希值和已经存在的某一个数据(key2-value2)的哈希值相同,继续比较,调用key1所在类的equals(key2)方法:
					若是equals()返回false:此时key1-value1添加成功 --->状况3
					若是equals()返回true:使用value1替换value2.而且return value2

- 补充:关于状况2和状况3:此时key1-value1和原来的数据以链表的方式存储.
- 在不断额添加过程当中,会涉及到扩容问题,当超出临界值(平衡因子 * Entry[]的长度)而且将要放入的元素非空时扩容,扩容后须要从新计算hash,并从新摆放Entry的位置
	默认的扩容方式:扩容为原来的2倍,并将原有的数据复制过来.

# 4 jdk8 相较于jdk7在底层实现方面的不一样:
	1. new HashMap():底层没有建立一个长度为16的数组(与ArrayList类似)
	2. jdk8底层的数组是:Node[],而非Entry[]
	3. 首次调用put() 方法时,底层建立长度为16的数组
	4. jdk7 底层结构只有:数组+链表.jdk8中底层结构:数组+链表+红黑树.
		当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64 时,
		此时索引位置上的全部数据改成使用红黑树存储.
> jdk7中createEntry的过程(往链表插入节点的方式)
	采用头插法,即将新元素放到数组头上,而后旧的链表连到新元素后面
> jdk8中createEntry的过程
>	采用尾插法,即将新元素放到数组列表的后面,由于引入红黑树以后,就须要判断单链表的节点个数(超过8个后要转成红黑树),因此干脆使用尾插法,正好遍历单链表,读取节点个数.也正是由于尾插法,使得HashMap在插入节点时,能够判断是否有重复节点.

put源码分析

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab;  //引用当前hashMap的散列表
        Node<K,V> p;  //表示当前散列表的元素
        int n, i;  //n: 当前散列表的长度  i: 寻址结果
        
        //延迟初始化逻辑,第一次调用putVAl时会初始化hashMap对象中的最耗费内存的散列表
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        
        //状况1: 寻址到的位置恰好是null,这个时候,直接put
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            //e: 不为null的话,找到了一个与当前要插入的key-value一致的key的元素  k: 临时的key
            Node<K,V> e; K k;
            
            //表示桶位中的该元素,与你当前插入的元素的key彻底一致,表示后续须要进行替换操做
            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 {
                //链表的状况,并且链表的头元素与咱们要插入的key不一致
                //binCount记录链表的长度
                for (int binCount = 0; ; ++binCount) {
                    //开始遍历链表,而且没找到一个与要插入的key一致的node
                    if ((e = p.next) == null) {
                        //链表尾插节点
                        p.next = newNode(hash, key, value, null);
                        //若是说链表长度超过了阈值,就变成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    
                    //找到了相同的key的node元素,须要进行替换操做
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    
                    p = e;
                }
            }
            
            //找到相同key的替换操做
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        //modCount: 表示散列表结构被修改的次数,替换Node元素的value不算
        ++modCount;
        //size自增,并判断是否是须要扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

resize源码分析

final Node<K,V>[] resize() {
    //oldTab: 引用扩容前的哈希表
    Node<K,V>[] oldTab = table;
    //oldCap: 扩容前数组长度
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    //oldThr: 扩容前的阈值
    int oldThr = threshold;
    //扩容以后的数组长度和阈值
    int newCap, newThr = 0;
    
    //若是散列表已经初始化过了,正常扩容
    if (oldCap > 0) {
        
        //若是已经达到了散列表的最大长度将再也不扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            //将阈值设为最大值
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        
        //新的最大值为旧的最大值翻倍
        //16 = 0000 1000  左移一位后  0001 0000 = 32
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)  //若是说你设置的初始大小小于了默认大小16,将不会进行扩大阈值
            newThr = oldThr << 1; // double threshold
    }
    
    //散列表未初始化的状况  oldCap == 0  说明hashMap中的散列表是null
    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);
    }
    
    //newThr为0时,计算出一个newThr
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    //根据以前计算的cap,创造出一个更大的数组
    @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    //若是扩容以前有数据
    if (oldTab != null) {
        
        //遍历散列表
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;  //当前node节点
            
            //若是说这个桶位有数据(多是单个节点,也多是链表或者是红黑树)
            if ((e = oldTab[j]) != null) {
                //将旧的桶位置空,方便GC回收内存
                oldTab[j] = null;
                
                //1. 单个节点的状况
                if (e.next == null)
                    //从新计算hash值,并放入该位置
                    newTab[e.hash & (newCap - 1)] = e;
                
                //2. 红黑树的状况
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                
                //3. 链表的状况
                /**
                *	以hash值为15的桶为例,扩容前的hash值为: 1111
                *	扩容后的hash值可能为: 01111  或者  11111 两种状况
                */
                else { // preserve order
                    
                    //低位链表: 存放在扩容以后的数组的下标位置,与当前数组的下标位置相同
                    //例如 01111 存放在原位置
                    Node<K,V> loHead = null, loTail = null;
                    //高位链表: 存放在扩容以后的数组的下标位置为: 当前数组下标的位置 + oldCap
                    //  11111  存放在 31号桶
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        //hash-> ....1 1111 & 1 0000 = 1
                        //hash-> ....0 1111 & 0 0000 = 0
                        if ((e.hash & oldCap) == 0) {
                            //低位链表初始化
                            if (loTail == null)
                                loHead = e;
                            //低位链表添加next节点
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            //高位链表初始化
                            if (hiTail == null)
                                hiHead = e;
                            //高位链表添加next节点
                            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;
                        //放到当前数组下标的位置 + oldCap
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

get源码分析

final Node<K,V> getNode(int hash, Object key) {
    //tab: 引用当前hashMap的散列表
    Node<K,V>[] tab;
    //first: 桶位中的头元素  e: 临时node节点
    Node<K,V> first, e;
    //n: 散列表长度
    int n; K k;
    
    //若是说当前散列表不为空,而且当前桶有数据
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (first = tab[(n - 1) & hash]) != null) {
        
        //1. 头节点为要查找的元素,则直接返回
        if (first.hash == hash && // always check first node
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        
        //若是说当前桶位不止一个元素
        if ((e = first.next) != null) {
            //2. 若是说当前桶位是树的状况
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            
            //3. 若是说当前桶位造成了链表就遍历查找元素
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    return null;
}

remove源码分析

/**
 * Implements Map.remove and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to match if matchValue, else ignored
 * @param matchValue if true only remove if value is equal
 * @param movable if false do not move other nodes while removing
 * @return the node, or null if none
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    //tab: 引用当前hashMap中的散列表
    //p: 当前node元素
    //index: 寻址结果
    Node<K,V>[] tab; Node<K,V> p; int n, index;
    
    //当前散列表不为空,而且所要删除元素的桶位也不为空
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) {
        //node: 查找到的结果  e: 当前node的下一个元素
        Node<K,V> node = null, e; K k; V v;
        
        //1. 找到了匹配的元素
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;
        
        //若是说当前桶位不止一个元素
        else if ((e = p.next) != null) {
            //2. 桶位是树的状况
            if (p instanceof TreeNode)
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            //3. 桶位是链表的状况
            else {
                //遍历链表,找到要删除的元素
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e;
                        break;
                    }
                    p = e;
                } while ((e = e.next) != null);
            }
        }
        
        //若是node不为空的话,说明按照key查找到须要删除的数据了
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            //若是是树的状况,执行树节点的移除
            if (node instanceof TreeNode)
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            
            //当前桶位上的头元素为须要删除的节点,将头结点的下一个节点设为头元素
            else if (node == p)
                tab[index] = node.next;
            //链表的状况,链表删除该节点
            else
                p.next = node.next;
            
            ++modCount;
            --size;
            afterNodeRemoval(node);
            
            //返回被删除的元素
            return node;
        }
    }
    return null;
}

相关问题

Q0:HashMap是如何定位下标的?
A:先获取Key,而后对Key进行hash,获取一个hash值,而后用hash值对HashMap的容量进行取余(实际上不是真的取余,而是使用按位与操做,缘由参考Q6),最后获得下标。数组

Q1:HashMap由什么组成?
A:数组+单链表,jdk1.8之后又加了红黑树当链表节点个数超过8个(m默认值)而且容量大于64之后,开始使用红黑树,使用红黑树一个综合取优的选择,相对于其余数据结构,红黑树的查询和插入效率都比较高。而当红黑树的节点个数小于6个(默认值)之后,又开始使用链表安全

这两个阈值为何不相同呢?markdown

主要是为了防止出现节点个数频繁在一个相同的数值来回切换,举个极端例子,如今单链表的节点个数是9,开始变成红黑树,而后红黑树节点个数又变成8,就又得变成单链表,而后节点个数又变成9,就又得变成红黑树,这样的状况消耗严重浪费,所以干脆错开两个阈值的大小,使得变成红黑树后“不那么容易”就须要变回单链表,一样,使得变成单链表后,“不那么容易”就须要变回红黑树。数据结构

Q2:Java的HashMap为何不用取余的方式存储数据?
A:实际上HashMap的indexFor方法用的是跟HashMap的容量-1作按位与操做,而不是%求余。(这里有个硬性要求,容量必须是2的指数倍,缘由参考Q6)app

Q3:HashMap往链表里插入节点的方式?
A:jdk1.7之前是头插法,jdk1.8之后是尾插法,由于引入红黑树以后,就须要判断单链表的节点个数(超过8个后要转换成红黑树),因此干脆使用尾插法,正好遍历单链表,读取节点个数。也正是由于尾插法,使得HashMap在插入节点时,能够判断是否有重复节点。函数

Q4:HashMap默认容量和负载因子的大小是多少?
A:jdk1.7之前默认容量是16,负载因子是0.75。源码分析

Q5:HashMap初始化时,若是指定容量大小为10,那么实际大小是多少?
A:16,由于HashMap的初始化函数中规定容量大小要是2的指数倍,即2,4,8,16,因此当指定容量为10时,实际容量为16。

源码以下:

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;
    }

☆☆☆☆Q6:容量大小为何要取2的指数倍?
A:两个缘由:1,提高计算效率:由于2的指数倍的二进制都是只有一个1,而2的指数倍-1的二进制就都是左全0右全1。那么跟(2^n - 1)作按位与运算的话,获得的值就必定在【0,(2^n - 1)】区间内,这样的数就刚合适能够用来做为哈希表的容量大小,由于往哈希表里插入数据,就是要对其容量大小取余,从而获得下标。因此用2^n作为容量大小的话,就能够用按位与操做替代取余操做,提高计算效率。2.便于动态扩容后的从新计算哈希位置时能均匀分布元素:由于动态扩容仍然是按照2的指数倍,因此按位与操做的值的变化就是二进制高位+1,好比16扩容到32,二进制变化就是从0000 1111(即15)到0001 1111(即31),那么这种变化就会使得须要扩容的元素的哈希值从新按位与操做以后所得的下标值要么不变,要么+16(即挪动扩容后容量的一半的位置),这样就能使得本来在同一个链表上的元素均匀(相隔扩容后的容量的一半)分布到新的哈希表中。(注意:缘由2(也能够理解成优势2),在jdk1.8以后才被发现并使用)

Q7:HashMap知足扩容条件的大小(即扩容阈值)怎么计算?
A:扩容阈值=min(容量负载因子,MAXIMUM_CAPACITY+1),MAXIMUM_CAPACITY很是大,因此通常都是取(容量负载因子)

Q8:HashMap是否支持元素为null?
A:支持。

☆☆☆Q9:HashMap的 hash(Obeject k)方法中为何在调用 k.hashCode()方法得到hash值后,为何不直接对这个hash进行取余,而是还要将hash值进行右移和异或运算?

源码以下:

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

A:若是HashMap容量比较小而hash值比较大的时候,哈希冲突就容易变多。基于HashMap的indexFor底层设计,假设容量为16,那么就要对二进制0000 1111(即15)进行按位与操做,那么hash值的二进制的高28位不管是多少,都没意义,由于都会被0&,变成0。因此哈希冲突容易变多。那么hash(Obeject key)方法中在调用 k.hashCode()方法得到hash值后,进行的一步运算:(h = key.hashCode()) ^ (h >>> 16)有什么用呢?

首先,(h = key.hashCode()) ^ (h >>> 16)是将h的二进制中高位右移变成低位。其次异或运算是利用了特性:同0异1原则,尽量的使得key.hashCode()) ^ (h >>> 16)在未来作取余(按位与操做方式)时都参与到运算中去。综上,简单来讲,经过key.hashCode()) ^ (h >>> 16);运算,可使k.hashCode()方法得到的hash值的二进制中高位尽量多地参与按位与操做,从而减小哈希冲突。

Q10:哈希值相同,对象必定相同吗?对象相同,哈希值必定相同吗?
A:不必定。必定。

Q11:HashMap的扩容与插入元素的顺序关系?
A:jdk1.7之前是先扩容再插入,jdk1.8之后是先插入再扩容。

Q12:HashMap扩容的缘由?
A:提高HashMap的get、put等方法的效率,由于若是不扩容,链表就会愈来愈长,致使插入和查询效率都会变低。

Q13:jdk1.8引入红黑树后,若是单链表节点个数超过8个,是否必定会树化? A:不必定,它会先去判断是否须要扩容(即判断当前节点个数是否大于扩容的阈值),若是知足扩容条件,直接扩容,不会树化,由于扩容不只能增长容量,还能缩短单链表的节点数,一箭双雕。

相关文章
相关标签/搜索