Java基础_容器(第二章Map)

目录

0:哈希表

0.1:什么是哈希表(散列表)

0.2:哈希冲突

0.3:HashMap数据结构(数组+链表)

1:什么是Map

1.1:Map容器结构和优点

1.2:如何学习Map 

 1.3:Map接口方法

2:HashMap(数组+链表)

2.1:HashMap特点

2.2:HashMap数据结构

2.3:HashMap线程安全

3:TreeMap

3.1:TreeMap特点

3.2:TreeMap数据结构

3.3:TreeMap线程安全

4:LinkedHashMap

4.1:LinkedHashMap特点

4.2:LinkedHashMap数据结构

4.3:LinkedHashMap线程安全

5:HashTable(线程安全)

5.1:HashTable特点

5.2:HashTable数据结构

5.2:HashTable线程安全

6:ConcurrentHashMap(线程安全)

5.1:ConcurrentHashMap特点

5.2:ConcurrentHashMap数据结构

5.2:ConcurrentHashMap线程安全

7:总结对比


0:哈希表

0.1:什么是哈希表(散列表)

不同的数据结构在内存上的物理存储只有两种:

顺序存储结构:顺序存储结构在内存上是连续的存储位置,顺序表的空间需要预先设置

优点:

(1):方法简单,各种语言都有数组,容易实现

(2):只需要存储数据,不用为节点的上下指针浪费内存开销

(3):按照下标访问,查询、修改速度快,耗时为:0(1)

缺点:

(1):添加、删除速度慢,因为数组字段需要移动,耗时为0(n)

(2):因为预设了数组长度,会造成内存浪费

链式存储结构:

优点:

(1):在内存上存在的区域不是连续的,便于利用碎片空间

(2):由于不是连续的所有插入,删除运算方便,耗时为0(1)

缺点:

(1):查询得时候需要遍历链式结构

(2):链表存储元素有元素前后缀指针,需要耗费多余的内存空间

哈希表也叫作散列表,哈希表的主干就是数组,哈希表的结构如下:

 存储位置 = f(关键字)

  其中,这个函数f一般称为哈希函数,这个函数的设计好坏会直接影响到哈希表的优劣。举个例子,比如我们要在哈希表中执行插入操作:

新增逻辑:

新增该元素通过哈希函数把该元素的hashcode映射到连续的数组某一个位置,当需要新增另外一个元素的时候,哈希函数把该元素的hashcode取模映射到连续的数组一个index位置,每次新增哈希函数保证生成一个唯一的值,然后映射道不同的位置

查询逻辑:

哈希函数根据查询的关键字,找到hashcode,然后再把哈希code映射到内存中的该区域,找到该元素

0.2:哈希冲突

然而万事无完美,如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办?也就是说,当我们对某个元素进行哈希函数运算,得到同一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突当插入的时候这个位置已经被占有了,那么哈希表如何解决这种冲突呢?

1:可以接着寻找下一块没有被占用的区域(开放地址法)

2:也可以在该位置下挂靠链表,将地址相同的元素挂靠的链表中(链地址法)

HashMap采用的就是链地址法

0.3:HashMap数据结构(数组+链表JDK1.8之前)

HashMap的数据结构采用的是链地址法,即数组+链表的数据结构,主干就是数组

插入:在插入时候由哈希函数根据生成的hashcode映射到数组中的某一个位置,然后插入,当这个位置有数据的时候,就在该数据的链表结构下挂靠新的元素

查询:根据哈希函数得到具体的地址位置,如果是链表就遍历,不是链表就取当前元素,

修改和删除也是类似

1:什么是Map

1.1:Map容器结构和优点

 相较于第一章的Collection中的List和Set,map提供了键值对的存储方式也就是(key-value)存储数据和获取数据是提供了更大的自由度,不用每一次都遍历整个容器的所有数据,只需要指定key的值即可,相对于List和Set,因此在一些情况下实用性更强

map结构树如下:根据JDK1.8源码分析,

图中的蓝色方框是接口绿色方框是实现类红色连线是继承(extends)蓝色连线是实现(implements)

1.2:如何学习Map 

Map接口有很多不同的实现类,这些不同的实现类有不同的优缺点,他们为什么有这些有些缺点呢?主要是

1:因为他们使用了不同的数据结构

2:底层源码的处理方式不同

所以我们学习HashMap,TreeMap,LinkedHashMap,HashTable和ConcurrentHashMap是主要从使用的数据结构和源码这两个维度来分析,在最后对Map类做一个横向对比总结,来结束Map的大体学习。

 1.3:Map接口方法

map接口定义了操作map的通用方法,比如添加,删除,获取,比较等等方法,这些方法在不同的实现类有不同的方法实现,不同的实现决定了这些容器不同的特性和属性,这里就用JDK_API里边Map接口方法示例。

equals(Object o)
将指定的对象与此映射进行比较以获得相等性。
forEach(BiConsumer<? super K,? super V> action)
对此映射中的每个条目执行给定的操作,直到所有条目都被处理或操作引发异常。
get(Object key)
返回到指定键所映射的值,或 null如果此映射包含该键的映射。
hashCode()
返回此地图的哈希码值。
isEmpty()
如果此地图不包含键值映射,则返回 true 。
keySet()
返回此地图中包含的键的Set视图。
merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)
如果指定的键尚未与值相关联或与null相关联,则将其与给定的非空值相关联。
put(K key, V value)
将指定的值与该映射中的指定键相关联(可选操作)。
remove(Object key)
如果存在(从可选的操作),从该地图中删除一个键的映射。
remove(Object key, Object value)
仅当指定的密钥当前映射到指定的值时删除该条目。
replace(K key, V value)
只有当目标映射到某个值时,才能替换指定键的条目。
size()
返回此地图中键值映射的数量。
values()
返回此地图中包含的值的Collection视图。

HashMap,TreeMap,LinkedHashMap,HashTable和ConcurrentHashMap

2:HashMap(数组+链表)

2.1:HashMap特点(在JDK1.8)

1:由于hashmap是数组+链表组成的(JDK1.8之前),数组是主体初始长度为16(2的四次方),链表为了解决哈希冲突,在极端情况下可能数组上只有一个位置有数据,其他的数据在数组的桶结构上形成了一长串的链表,这是极端情况,会造成查询复杂度剧增的极限情况。所以1.8优化了hashmap的结构

2:在JDK1.8之后。HashMap时候数组+链表+红黑树,数据遍历解析是无序的(跟插入顺序不一致)

3:如果该位置不是链表,查询和添加都很快,为0(1),如果是链表需要遍历链表,或者红黑树也比较快为0(n),

4:修改删除都是先查看是否有链表,有的话遍历,没有的话就直接操作,数据无序

5:可以存储null键和null值,初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂,扩容因子是0.75.超过四分之三就回扩容

2.2:HashMap数据结构

jdk1.8之前的结构:采用数组+链表

即使用链表处理冲突,同一hash值的节点都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。

jdk1.8之后的结构:采用数组+链表+红黑树:

通过数据结构红黑树的大体了解,我们知道平衡二叉数红黑树解决了二叉树层级过高的性能退化问题,即当一个hash值映射到同一个物理地址的时候,当数据量大于8的时候,会把链表转换成红黑树,结构如图

源码分析:可以每一个Node的结构如下,实现了Map.Entry接口

//添加方法参数key的哈希code=hash,键=key,值=value
	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)
			//第一步:tab 为空,初始化长度为16的node数组
            //第一次添加初始化长度为DEFAULT_INITIAL_CAPACITY = 1 << 4;
			n = (tab = resize()).length;
            //第二步:根据hashcode判断数组该位置是否有数据
		if ((p = tab[i = (n - 1) & hash]) == null)
			//第三步:没有的话。在该位置添加数据
			tab[i] = newNode(hash, key, value, null);
		else {
			//第四部:如果数据key的值存在并且相等,则tab覆盖数据
			Node<K, V> e;
			K k;
            //第五步:判断key是否相等,相等覆盖 使用==和equal的原因是Integer的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 {
                //第七步:在链表末尾中插入数据,next不存在直接插入,否则检查key的值
				for (int binCount = 0;; ++binCount) {
					if ((e = p.next) == null) {
						p.next = newNode(hash, key, value, null);
						if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //第八步:长度超过TREEIFY_THRESHOLD=8,转换成红黑树
							treeifyBin(tab, hash);
						break;
					}
                    //
					if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
						break;
					p = e;
				}
			}
            //如果key的值相等,覆盖数据,但是mod不用++,结束程序
			if (e != null) { // existing mapping for key
				V oldValue = e.value;
				if (!onlyIfAbsent || oldValue == null)
					e.value = value;
				afterNodeAccess(e);
				return oldValue;
			}
		}
		++modCount;//多线程并发容易出问题 size和modCount容易被相互覆盖,或者不同步
		if (++size > threshold)
			//如果size长度超过threshold(16),则扩容,扩容因子是0.75F
			resize();
		afterNodeInsertion(evict);
		return null;
	}

HashMap的get方法

2.3:HashMap线程安全

线程不安全,导致线程不安全的原因(包括前边的ArrayList和LinkedList)都是因为在类中有全局变量,但是添加和删除等方法都没有加锁,导致运行添加等方法的时候,最造成多个线程操作到相size,互相覆盖导致数据不一致的情况

还有一些知识点:


    /*
    *
    * 1:这里为什么不直接返回key.hashCode() 而是要进行^运算
    *
    * h >>> 16的意思是将hashcode右移16位,高位补0,高位全部是0,低位16是原来的高位
    *
    *  key.hashCode()高位不变,h >>> 16高位是0,低位变成高位
    *
    * ^的意思是 如果相对应位值相同,则结果为0,否则为1
    *
    * 运算结果高16位不变,低16位变化很大(将高低位进行混合 充分利用高位忒性得到新的低位 这样做的目的是下边的&运算 能防止hash容易碰撞)
    *
    *
    * tab[i = (16 - 1) & hash  屏蔽了hash的高位,只和低位运算
    *
    * 16-1=15  二进制是1111
    *
    * &的作用是如果相对应位都是1,则结果为1,否则为0
    *
    * 15&hash 只有最后的4位进行异或运算 找到表上的位置
    *
    *
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

3:TreeMap

3.1:TreeMap特点

1:数据是无序的,插入顺序和解析解析顺序不一致,但是用迭代器查询是有顺序的:因为红黑树节点大小有规律,如果key是int型的话

2:底层数据结构的红黑树

3:查询修改速度快,添加和删除略慢一点。时间复杂度不是0(1),也不是0(n),是0(logN)

4:线程不安全

3.2:TreeMap数据结构

TreeMap的底层实现是红黑树,红黑树源码结构如下:

添加方法源码,删除方法类似,效率也是比较快的,需要重新着色和偏移

//红黑树的插入方法
public V put(K key, V value) {
	//第一步:第一次插入,根节点为空,
    Entry<K,V> t = root;
    if (t == null) {
        compare(key, key); // type (and possibly null) check
        //第二步:创建根节店,颜色为黑
        root = new Entry<>(key, value, null);
        //长度为1
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    //比较器选择
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
      //次处省略了,定义比较器方法和默认比较器方法一致。
    }
    else {
    	//采用默认比较器
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
        	//do while 循环,
        do {
        	//根节店
            parent = t;
            //比较算法,和父节店循环比较
            cmp = k.compareTo(t.key);
            if (cmp < 0)
            	//比父节点小,插入左边
                t = t.left;
            else if (cmp > 0)
            	//比父节点大,插入右边
                t = t.right;
            else
            	//相等则覆盖value
                return t.setValue(value);
        } while (t != null);
    }
    //最后遍历到整个树节点的最终需要插入的位置
    Entry<K,V> e = new Entry<>(key, value, parent);
    //然后在这个节点中插入数据
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    //对数据进行调色和旋转处理,变成红黑树
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

查找方法和修改方法会很快,遍历层级结构即可,由于红黑树层级结构一般都很低。

3.3:TreeMap线程安全

treemap线程不安全,在多个线程操作的时候,其他线程对他进行增加删除操作,可能会影响到其他的线程

4:LinkedHashMap

4.1:LinkedHashMap特点

1:继承了HashMap,数据有序,底层是双向链表,跟LinkedList底层数据结构相似

2:添加数据后再after之后添加,保证顺序,查询和删除效率都不高,优点是能保证顺序

3:线程不安全

4:数据结构底层是双向链表,初始化数组为16

4.2:LinkedHashMap数据结构

底层是双向链表

底层数据结构在hashmap的基础上维护了一个双向链表:

增加方法:

LinkedHashMap并没有重写任何put方法。但是其重写了构建新节点的newNode()方法. 
newNode()会在HashMap的putVal()方法里被调用,putVal()方法会在批量插入数据putMapEntries(Map<? extends K, ? extends V> m, boolean evict)或者插入单个数据public V put(K key, V value)时被调用。

LinkedHashMap重写了newNode(),在每次构建新节点时,通过linkNodeLast(p);将新节点链接在内部双向链表的尾部。

  

删除方法:

LinkedHashMap也没有重写remove()方法,因为它的删除逻辑和HashMap并无区别。 
但它重写了afterNodeRemoval()这个回调方法。该方法会在Node<K,V> removeNode(int hash, Object key, Object value, 
boolean matchValue, boolean movable)方法中回调,removeNode()会在所有涉及到删除节点的方法中被调用,上文分析过,是删除节点操作的真正执行者。

查询方法:

遍历双链表,效率比较低

4.3:LinkedHashMap线程安全

线程不安全

5:HashTable(线程安全)

5.1:HashTable特点

0:默认构造函数长度是11,加载因子是0.75

1:线程安全,即所有的操作增删方法,都加了方法锁(synchronized),保证在多线程下只有一个线程能访问,效率比较低

2:底层数据结构也是哈希表(数组+单链表)。跟jdk1.8之前的hashmap结构一致,但是线程安全。

3:键值都不能为null,数据无序,源码会检查

4:初始size为11,扩容:newsize = oldsize*2+1;扩容因子也是0.75查过四分之三就会扩容

5.2:HashTable数据结构

底层是哈希表(数组+链表)

1:构造函数和代码分析



//添加方法加锁
public synchronized V put(K key, V value) {
        // Make sure the value is not null
		//value不能为空,值
        if (value == null) {
            throw new NullPointerException();
        }

        // Makes sure the key is not already in the hashtable.
        //默认长度是11
        Entry<?,?> tab[] = table;
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        //根据哈希code映射到数组的下标
        Entry<K,V> entry = (Entry<K,V>)tab[index];
        //如果下标有值,遍历循环,找到key相等,然后覆盖老的entity
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }
        //直接添加新的entry
        addEntry(hash, key, value, index);
        return null;
    }


//添加逻辑方法
private void addEntry(int hash, K key, V value, int index) {
    modCount++;
    Entry<?,?> tab[] = table;
    //元素页数小于阀值,默认阀值为8
    if (count >= threshold) {
        // 元素超过法制扩容哈希和设置新的阀值
        rehash();

        tab = table;
        hash = key.hashCode();
        //将元素映射到新的下标
        index = (hash & 0x7FFFFFFF) % tab.length;
    }
    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //在该下标添加元素
    tab[index] = new Entry<>(hash, key, value, e);
    //元素个数加一
    count++;
}

数据模型图 添加逻辑如下:

首先我们假设一个容量为5的table,存在8.8、10.10、13.13、16.16、17.17、21.21。他们在table中位置如下:

假如我们想要添加16的值,会根据key的哈希code找到1的位置,然后遍历桶(单链表结构,找到key相等的数据,然后新value覆盖老的value)其他的修改和删除逻辑类似

5.2:HashTable线程安全

hashmap线程安全,因为方法都加了锁,防止多线程同时操作带来的风险,会造成效率偏低

6:ConcurrentHashMap(线程安全)

5.1:ConcurrentHashMap特点

1:线程安全,即所有的操作增删方法,都加了分段锁(synchronized)和CAS,保证在多线程下只有一个线程能访问,首先在多线程的条件下,在数组上使用cas操作没有数据add数据,尝试添加数据,然后冲突的话才会使用synchronize获取锁

2:底层数据结构也是哈希表(数组+单链表+红黑树)jdk1.8升级。跟jdk1.8之前的hashmap(数组+链表)结构一致,但是线程安全。

3:键值都不能为null,初始长度为16,put、remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。

4:数据无序,key value都不能为空

5.2:ConcurrentHashMap数据结构

jdk1.8:数组+单链表+红黑树

jdk1.8之前:数组+链表

5.2:ConcurrentHashMap线程安全

采用分段锁机制,不在方法上加锁,锁了桶,提升了并发效率,并且采用CAS添加桶上明日有数据的槽位。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值