一、Map映射容器:

1、介绍:Map(映射)是一个可以根据键值进行存储的,它的一个 Key 对应的是一个存储的位置,所以Key值是唯一的,根据Key值可以获取到对应的存储的Value。这种存储的集合我们称为 “键-值” Map。

注意:
1)它不是集合Collection的子类;
2) 它的键值是唯一的,根据键值可以取出值;重复添加相同的key,后面的会覆盖前面的。
3) 根据值无法直接取出Key。

2、Map的几种实现方式:
(1)HashMap 按照散列存储,这样的存取较快,线程不安全的,允许存放null键,null值
(2)Hashtable :作为古老的实现类,线程安全,速度慢,不允许存放null键,null值
(3)TreeMap 使用二叉树的算法来实现键值的自然排序功能:key需要实现比较器。

(4)LinkedHashMap

3、Map的遍历:需要先取出所有的key,然后遍历key:

public static void main(String[] args) {
		Map<String, String> map = new HashMap<>();
		map.put("语文", "95分");
		map.put("数学", "80分");
		Set<Entry<String, String>> set = map.entrySet();
		Iterator iterator = set.iterator();
		//map的遍历
		while (iterator.hasNext()) {
			Entry<String, String> entry = (Entry<String, String>) iterator.next();
			System.out.println(entry.getKey() + entry.getValue());
		}
	}

二、HashMap详解:

1、介绍:

无序,即key值不排序;默认初始长度是16。并且每次自动扩展或是手动初始化时,长度必须是2的幂。HashMap通过 链式地址法 来解决“哈希冲突”;(Java8以后,在哈希冲突过多的时候,为了降低元素查询的时间复杂度,会将链表改为红黑树结果进行存储。树化的本质原因

2、特点:
  • 允许null值和null键
  • 动态扩容(扩容机制与ArrayList相同)
  • 不存在重复的元素
  • 无序的
3、底层原理: 

HashMap通过hash算法来确定每个键值对的位置,实现对于元素的增加,删除,修改等操作。HashMap 的性能表现非常依赖于哈希码的有效性。

JDK1.8 之前的实现方式:数组+链表;JDK1.8之后的实现方式:数组+链表+红黑树。

① HashMap是一个存储key-value键值对的集合,每一个键值对也叫做entry,这些entry分散存储在一个数组中,这个数组也是HashMap的主干,这个数组每个元素的初始值都是null。

② 数组(键值对entry组成的数组主干)+ 链表(元素太多时为解决哈希冲突数组的一个元素上多个entry组成的链表)+ 红黑树(当链表的元素个数达到8链表存储改为红黑树存储)

spring boot项目怎么将Map集合存到数据库字段 map集合如何存储数据_链表

(1)put方法原理:比如HashMap.put("zhangsan",0),这时需要利用哈希函数来确定entry的插入位置index,哈希函数算法:index=HashCode(zhangsan) & (Length - 1)。

这也是为什么数组是有序的而HashMap是无序的。

因为HashMap长度有限,当插入的entry越来越多时,再完美的哈希函数也难免会出现index冲突的情况(哈希冲突),这时就需要用到链表。HashMap数组的每一个元素上不只有一个entry对象,也是一个链表的头结点。每一个entry对象通过next指针指向它的下一个entry节点,新来的entry节点插入链表时是头插法插入到链表头结点(也就是数组那个位置)。之所以把新来的节点插入到头结点是因为HashMap作者认为新来的被查找的可能性更大,这就是HashMap的底层原理。

(2)get方法原理:比如HashMap.get("zhangsan"),根据key做哈希映射获得index,由于会有哈希冲突的情况,同一个index位置下可能有多个entry,这时候就需要顺着对应链表的头节点,一个一个向下来查找。

(3)元素位置算法通过hashcode定位到要存储在数组的位置,为了实现一个尽量均匀分布的Hash函数,我们通过利用Key的HashCode值来做某种运算。比如把Key的HashCode值和HashMap长度做取模运算,取模运算的方式固然简单,但是效率很低,因为位运算直接对内存数据进行操作,不需要转成十进制,所以位运算要比取模运算的效率更高。所以为了实现高效的Hash算法,HashMap的发明者采用了位运算的方式。这样做不但效果上等同于取模,而且还大大提高了性能。index= HashCode(Key) & (Length - 1)。

这也是为什么数组是有序的而HashMap是无序的、HashMap默认长度是16或者2的幂。

4、线程安全问题:

线程不安全

public class HashMapDemo {
    public static void main(String[] args) {
        Map<String, String> map = new HashMap();
        for (int i = 0; i < 10; i++) {
            String key = String.valueOf(i);
            new Thread(() -> {
                //向集合中添加内容
                map.put(key, UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
        /*
        Exception in thread "7" Exception in thread "1" Exception in thread "3" Exception in thread "4" Exception in thread "0" Exception in thread "5" java.util.ConcurrentModificationException
         */
    }
}

线程不安全解决方案:通过ConcurrentHashMap类解决

import java.util.concurrent.ConcurrentHashMap;

public class HashMapDemo {
    public static void main(String[] args) {
//        Map<String, String> map = new HashMap();
        Map<String, String> map = new ConcurrentHashMap();
        for (int i = 0; i < 10; i++) {
            String key = String.valueOf(i);
            new Thread(() -> {
                //向集合中添加内容
                map.put(key, UUID.randomUUID().toString().substring(0, 8));
                //从集合中获取内容
                System.out.println(map);
            }, String.valueOf(i)).start();
        }
    }
}
5、HashMap主要实现的接口

(1)Map接口:得到了map中定义的所有接口
(2)Cloneable接口:实现Cloneable接口并重写Object类中的clone方法,Cloneable接口中没有任何的实现方法,它属于一个标识性接口。
(3)Serializable接口:可以进行序列化,通过序列化后进行传输,典型应用就是hessian协议

public class HashMap<K,V> extends AbstractMap<K,V> 
        implements Map<K,V>, Cloneable, Serializable {
 
    //hashMap中的数组初始化大小:1 << 4=2^4=16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
 
    //1<<30 表示1左移30位,每左移一位乘以2,所以就是1*2^30=1073741824。
    static final int MAXIMUM_CAPACITY = 1 << 30;
 
    //默认装载因子:
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
 
    //HashMap默认初始化的空数组:
    static final java.util.HashMap.Entry<?,?>[] EMPTY_TABLE = {};
 
    //HashMap中底层保存数据的数组:HashMap其实就是一个Entry数组
    transient java.util.HashMap.Entry<K,V>[] table = (java.util.HashMap.Entry<K,V>[]) EMPTY_TABLE;
 
    //Hashmap中元素的个数:
    transient int size;
 
    //threshold:等于capacity * loadFactory,决定了HashMap能够放进去的数据量
    int threshold;
 
    //loadFactor:装载因子,默认值为0.75,它决定了bucket填充程度;
    final float loadFactor;
 
    //HashMap被操作的次数:
    transient int modCount;
 
}
6、扩容
6.1、扩容的条件:

table 是底层用于保存数据的数组,默认情况下会先赋值空数组,最终存储元素的是Entry[]数组,其中的对象就是Entry对象,在首次进行put的时候,才会进行数组的初始化,建立一个容量为16的数组。

在Java中,HashMap的扩容条件是基于当前HashMap容量(即内部数组的大小)和实际存储元素的数量。具体来说,在Java 7及以后版本中,以下两种情况都会触发HashMap扩容:

(1)装载因子阈值: 当HashMap中的元素数量(entry数量)超过当前容量与预设的负载因子(load factor)的乘积时,会触发扩容操作。默认负载因子DEFAULT_LOAD_FACTOR 为0.75,也就是说,当HashMap中的元素个数达到容量的75%时,就会进行扩容。

(2)插入新元素时: 在执行put()操作尝试插入一个新的键值对时,如果发现现有元素数量已经达到了扩容阈值,并且确实需要新增一个元素(不是替换已存在的元素),那么也会触发扩容操作。
扩容过程涉及创建一个新的、更大容量的数组,并将原数组中的所有键值对重新计算哈希值并移动到新的数组中。这个过程也称为“rehashing”,并且在Java 8中引入了优化,链表长度大于某个阈值时会转化为红黑树,以减少搜索、插入和删除的时间复杂度。

注意:HashMap初始化之后,并没有立即分配内存空间,初始化时并没有初始化数组 table,在 put 操作时才初始化。

6.2、扩容的过程

demo:

初始容量为16,当添加第13个元素时,

当初始容量为16的HashMap添加第13个元素时,由于默认负载因子是0.75,所以扩容阈值(threshold)计算公式为:capacity * loadFactor = 16 * 0.75 = 12。

因此,当添加第13个元素时,HashMap的实际存储元素数量超过了扩容阈值(即当前已存储12个元素+即将添加的第13个元素),将会触发扩容操作。具体扩容流程:

(1)创建新的Entry数组:

HashMap会创建一个新的Entry数组,其容量通常是原来容量的两倍,也就是newCapacity = oldCapacity << 1 = 16 * 2 = 32。

所以hashMap的长度一定是2的N次幂,对于Hash值的计算,hashMap中采用的是 与操作,相比于 取模运算 效率更高:

① 当length为2的N次方的时候,length一定是偶数,这样length-1一定是奇数,当奇数转换成二机制数的时候,最后一位永远是1,那么HashMap中每一个位置都是奇数,当通过Hash值 与 length-1 进行与操作的时候,结果可能是偶数,也可能是奇数,因此散列性比较好,可以有效的降低哈希冲突
② 当length为奇数是的时候,length-1 就一定是偶数,当偶数转换成二机制的时候,最后一位就是0,任何值和0进行与运算,结果都会是 0 ,因此无论是hash值是啥,进行与操作后结果都是偶数,从而造成一半的数组位都是浪费的。从而增加 哈希冲突的概率,从而降低HashMap的性能。

(2)重新哈希所有元素:

遍历原HashMap中的每个Entry(键值对),使用新的容量重新计算它们的索引位置,并将这些Entry放入新的数组中。这个过程被称为“rehash”。

(3)链表拆分或树形结构调整:

如果在扩容过程中某个桶(bucket)下形成了链表结构,那么在这个桶下的链表会根据新的索引被拆分成两个链表,分别存放在新数组的相应位置。在Java 8及以上版本中,如果链表长度超过8且HashMap允许转化为红黑树(通过treeifyThreshold控制,默认也是8),则链表会被转换为红黑树。

(4)替换引用:

扩容完成后,HashMap会将内部引用指向新的Entry数组,旧数组将不再使用,随后垃圾回收机制会在适当的时候回收旧数组。

三、 HashTable

1、介绍:

在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。

HashTable逐渐被ConcurrentHashMap取代,但是Hashtable的子类Properties依然沿用,Properties集合也是唯一一个和IO流相结合的集合。

2、特点:
  • 线程安全的,单线程集合
  • 速度快
  • 不允许null键和null值
3、原理

HashTable与HashMap的结构一致,都是哈希表实现。

与HashMap不同的是,在HashTable中,所有的方法都加上了synchronized锁,用锁来实现线程的安全性。由于synchronized锁加在了HashTable的每一个方法上,所以这个锁就是HashTable本身--this。因而效率不高。

四、ConcurrentHashMap

1、介绍:

ConcurrentMap是一个接口,支持并发访问的Map的集合。在Map接口上增加了4个扩展方法。主要的实现类就是ConcurrentHashMap。ConcurrentHashMap是一个线程安全并且高效的HashMap。

public interface ConcurrentMap<K, V> extends Map<K, V> {
 
    //插入元素
    V putIfAbsent(K key, V value);
 
    //移除元素
    boolean remove(Object key, Object value);
 
    //替换元素
    boolean replace(K key, V oldValue, V newValue);
 
    //替换元素
    V replace(K key, V value);
}

 五、TreeMap:

1、介绍:

(1)唯一;

(2)有序,可排序。如获取一周菜单Map <String,MenuDTO>,希望按照周一到周日的顺序排列,就可以使用TreeMap。有序的前提是key需要实现比较器。

2、底层原理:

红黑树。TreeMap的键值对是存放在红黑树中的,key的顺序通过红黑树的自平衡实现的。

spring boot项目怎么将Map集合存到数据库字段 map集合如何存储数据_数组_02

spring boot项目怎么将Map集合存到数据库字段 map集合如何存储数据_开发语言_03

源码:

1.TreeMap每一个节点内部属性
K key;			//键
V value;		//值
Entry<K,V> left;	//左子节点对象
Entry<K,V> right;	//右子节点对象
Entry<K,V> parent;	//父节点对象
boolean color;		//节点颜色




2.TreeMap成员变量
public class TreeMap<K,V>{  
    //比较器对象
    private final Comparator<? super K> comparator;
	//根节点
    private transient Entry<K,V> root;
	//集合长度
    private transient int size = 0;

   
3.空参数构造
	 public TreeMap() {
    //表示没有比较器对象
        comparator = null;
    }
		
	
4.带参数构造
	//自己传递的比较器对象
	public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
		
5.添加元素
	public V put(K key, V value) {
        return put(key, value, true);
    }

//参数1: 键
//参数2: 值
//参数3: 当键重复时,是否需要覆盖值
    	true:覆盖
        false:不覆盖
		
	private V put(K key, V value, boolean replaceOld) {
		//获取根节点的地址值,赋值给局部变量t
        Entry<K,V> t = root;
		//判断根节点是否为null
		//如果为null,表示第一次添加,会把当前要添加的元素,做根节点
		//如果不为null,表示当前不是第一次添加,跳过这个判断,续执行下面的代码
        if (t == null) {
			//创建一个Entry对象,将键值传递过去,当做根节点
            addEntryToEmptyMap(key, value);
			//表示此时没有覆盖任何元素
            return null;
        }
		//表示两个元素的键比较之后的结果
            //负数/正数/0
        int cmp;
		//当前要添加节点的父节点
        Entry<K,V> parent;
		
		//表示记录了比较规则
		//如果是自然排序,comparator是null,cpr也是null
         //如果记录比较器排序方式,此时记录的是比较器
        Comparator<? super K> cpr = comparator;
		//判断当前是否有比较器对象
		//如果传递了比较器对象,就执行if代码,此时以比较器为准
		//没有传递比较器则执行else代码,此时以自然排序为准
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else {
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        } else {
			//把键进行强转成Comparable类型
			//要求: 键 须实现Comparable接口,不然强转会报错
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
				//把根节点当做当前节点的父节点
                parent = t;
				//调用compareTo比较根节点与当前要添加节点的大小关系
                cmp = k.compareTo(t.key);
				
                if (cmp < 0)
					//结果负数
					//去根节点左边去找
                    t = t.left;
                else if (cmp > 0)
					//结果为正(根节点比当前小)
					//去根节点右边去找
                    t = t.right;
                else {
					//结果为0,会覆盖
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        }
		//当前节点 按照 指定规则进行添加
        addEntry(key, value, parent, cmp < 0);
        return null;
    }	
	
	
	
	 private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (addToLeft)
            parent.left = e;
        else
            parent.right = e;
		//添加完毕后,按照红黑树规则进行调整
        fixAfterInsertion(e);
        size++;
        modCount++;
    }
	
	
	
	private void fixAfterInsertion(Entry<K,V> x) {
		//红黑树节点默认为红色
        x.color = RED;

		//按照红黑规则调整
		
		//parentOf:获取父节点
		//parentOf(parentOf(x)): 爷爷节点获取
		//leftOf:左子节点获取
        //当前节点不为空,且不是根节点,且父节点为红色时,进入循环
        while (x != null && x != root && x.parent.color == RED) {
			
			
			//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
			//目的: 为了获取当前节点的叔叔节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
				//表示当前节点的父节点是爷爷节点的左子节点
				//下面可以用rightOf获取当前节点的叔叔节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
					//叔叔节点为红色
					
					//将父节点设置为黑色
                    setColor(parentOf(x), BLACK);
					//将叔叔节点设置为黑色
                    setColor(y, BLACK);
					//将爷爷节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
					
					//把爷爷节点设置为当前节点
                    x = parentOf(parentOf(x));
                } else {
					
					//叔叔节点为黑色
					
					
					//判断当前节点是否为父节点的右子节点
                    if (x == rightOf(parentOf(x))) {
						
						//表示当前节点是父节点的右子节点
                        x = parentOf(x);
						//以要添加的节点的父节点 左旋,因为上面这个代码把当前节点变成了要添加元素的父节点
                          //所以下面直接是以x左旋 == 要添加的节点的父节点
                        rotateLeft(x);
                    }
                    //父节点设置为黑色
                    setColor(parentOf(x), BLACK);
                    //爷爷节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //以要添加元素的祖父节点当做轴 右旋
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
				//表示当前节点的父节点是爷爷节点的右节点
				//下面可以用leftOf获取当前节点叔叔节点
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    //叔叔节点是红色时
                    //父节点设置为黑色
                    setColor(parentOf(x), BLACK);
                    //叔叔节点设置为黑色
                    setColor(y, BLACK);
                    //爷爷节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //爷爷节点设置为当前节点,进行下一轮循环
                    x = parentOf(parentOf(x));
                } else {
                    //叔叔节点为黑色
                    if (x == leftOf(parentOf(x))) {
                        //当前节点为父节点的左子节点时
                        //把当前节点设置为父节点
                        x = parentOf(x);
                        //并且以父节点右旋
                        rotateRight(x);
                    }
                    //设置父节点为黑色
                    setColor(parentOf(x), BLACK);
                    //设置爷爷节点为红色
                    setColor(parentOf(parentOf(x)), RED);
                    //以爷爷节点去左旋
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
		
		//如果当前添加节点是根节点
        root.color = BLACK;
    }
	
 setColor(parentOf(parentOf(x)), RED);
                    //以爷爷节点去左旋
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }		
		//如果当前添加节点是根节点
        root.color = BLACK;
    }