集合框架 Map
Map接口
HashMap 简要说明
package com.example.demo.day02;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.Properties;
import java.util.Map;
/**
* 1、Map接口以及实现类
* |-- Map:双列数据,存储 key-value 键值对的数据
* |--- HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储key或value为null的元素。
* |------ LinkedHashMap:HashMap的子类,能够保证在遍历元素时,可以按照添加的顺序实现遍历。
* 原因:在原有HashMap的基础上,添加了一对"指针",指向前一个和后一个元素。
* 对于频繁的遍历操作,LinkedHashMap执行效率高于HashMap。
* |--- TreeMap: 按照添加的 Key-Value 键值对进行排序,实现排序遍历。
* 此时考虑key的自然排序或定制排序。底层使用的是红黑树。
* |--- Hashtable: 作为Map的古老实现类;线程安全的,效率低;不能存储key或value为null
* |------ Properties: Hashtable的子类常用来处理配置文件,Key和Value都是String类型
*
* 2、key-value 键值对的理解 Map中的key是无序的不可重复的 value 是无序的可重复的。
* ---> 要求Key所在的类,必须重写equals() 和 hashcode() 方法 -- HashMap
* 一个 key-value 键值对 构成了一个Entry对象,Entry也是无序的不可重复的。
*
*
* HashMap的底层:数组+链表 JDK7及以前
* 数组+链表+红黑树 JDK8
*
*
*/
public class MapTest {
public void test(){
// HashMap的底层实现原理简单说明 以JDK7 为例
HashMap<String, String> map = new HashMap<>();
// 在实例化以后,底层创建了长度为16的一维Entry[]数组
// ... 可能已经执行过多次put...
// 首先,调用key 所在类的 hashCode() 计算出 key的哈希值,此哈希值经过算法计算以后,得到在Entry数组中的存放位置。
// 如果此位置没有数据 -- 此时 key-value 键值对添加成功。---- 情况1
// 如果此位置已经存在数据(意味着此位置上存在一个或者多个数据,以链表的形式存在),比较key和已经存在的一个或者多个数据的哈希值
// 如果key的哈希值与已经存在的数据的哈希值都不相同,-- 此时 key-value 键值对添加成功。---- 情况2
// 如果key的哈希值与已经存在的数据中的某一个哈希值相同,继续比较,调用key所在类的 equals()方法,
// equals()方法返回false key-value 键值对添加成功 -------情况3
// equals()方法返回true,使用value 替换原有key对应的值
// 补充:关于情况2和情况3:
// 此时key-value和原来的数据以链表的方式存储。
// 在不断添加的过程中,会涉及到扩容问题,当超出临界值时,要进行扩容。
// 默认的扩容方式中,扩容为原来的2倍,并将原有的数据复制过来(重新计算元素在数组中的位置)
// JDK8 相较于 JDK7 底层实现方面略有却别
// 1、new HashMap<>(),底层没有直接创建长度为16的数组,并且数组名称变为 Node[] 而不是Entry[]
// 2、首次调用put方法时,才创建长度为16的 Node[] 数组
// 3、JDK7底层结构只有:数组+链表,JDK8底层结构:数组+链表+红黑树,当数组的某一个索引位置上的元素,
// 以链表形式存在的个数大于8 且当前底层数组的长度还大于64,此时索引位置上的所有数据改为使用红黑树存储。
//
// HashMap源码中的重要常量
// DEFAULT_INITIAL_CAPACITY = 1 << 4 :HashMap的默认容量 16
// MAXIMUM_CAPACITY = 1 << 30 : HashMap中最大支持的容量 2^30
// DEFAULT_LOAD_FACTOR = 0.75f :HashMap的默认加载因子是0.75,HashMap 底层数组的扩容时,
// 并不像ArrayList一样,等到满了才扩容,默认情况下而是到 16 * 0.75 存储键值对的数量大于12进行扩容
// 这样做的好处是:key的经过算法计算出位置正好填满数组的概率低,会造成链表长度过长,导致性能下降,提前扩容
// 可以一定程度上减少链表过长的问题
// TREEIFY_THRESHOLD = 8 :Bucket(桶)中的链表长度大于该默认值,转化为红黑树
// UNTREEIFY_THRESHOLD = 6 :Bucket(桶)中红黑树存储的Node小于该默认值,转化为链表
// MIN_TREEIFY_CAPACITY = 64 Bucket(桶)中的Node被树化时最小的hash表容量(当Bucket(桶)中Node的数量大到需要转变为红黑树时
// ,若hash表容量小于MIN_TREEIFY_CAPACITY,此时执行resize()扩容操作,这个MIN_TREEIFY_CAPACITY的值至少是
// TREEIFY_THRESHOLD的4倍。)
// HashMap源码中的 属性
// Node<K,V>[] table : 存储元素的数组,总是 2的n次幂
// size : HashMap中存储的键值对的数量
// modCount : HashMap扩容和结构改变的次数
// entrySet: 存储具体元素的集合
// threshold : 扩充的临界值 = 容量 * 加载因子
// loadFactor : 加载因子
map.put("key","value");
// 底层put过程
// 无参数初始化
// public HashMap() {
// // 仅将默认加载因子赋值给 loadFactor
// this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
// }
//
// public V put(K key, V value) {
// hash(key) 拿到一个Hash值
// return putVal(hash(key), key, value, false, true);
// }
// 获取hash值的方法
//static final int hash(Object key) {
// int h;
// key的hashCode 异或 key.hashCode() 右移16位 直接说使用hashCode是不准确的
// >>表示右移,如果该数为正,则高位补0,若为负数,则高位补1;
// >>>表示无符号右移,也叫逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0。
// 参考 https://blog.csdn.net/cobbwho/article/details/54907203
// return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
// }
// map 的 put方法 针对首次调用 put 方法
// final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
// boolean evict) {
//
// HashMap.Node<K,V>[] tab; HashMap.Node<K,V> p; int n, i;
// 首次调用时 table未初始化 当时是null 直接进入resize()方法进行初始化 -> 进入resize() 方法查看
// if ((tab = table) == null || (n = tab.length) == 0)
// n = (tab = resize()).length;
// if ((p = tab[i = (n - 1) & hash]) == null) // 通过 & 运算 的方式确定在tab 当前数组中存在的位置
// tab[i] = newNode(hash, key, value, null); // 如果当前位置是null 添加成功
// else { // 当前位置已经存在数据
// HashMap.Node<K,V> e; K k;
// if (p.hash == hash &&
// ((k = p.key) == key || (key != null && key.equals(k)))) // 比较 hash值,调用equals() 方法
// e = p; // key相同 当前数组p 存进 e中
// else if (p instanceof HashMap.TreeNode) // 首次调用先不关心这个 因为还不是红黑树节点
// e = ((HashMap.TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
// else {
// for (int binCount = 0; ; ++binCount) { // 直接进入for循环
// if ((e = p.next) == null) { // 当前数组节点上存在链表,直接比较下一个 因为已经与数组上的元素比较过了
// p.next = newNode(hash, key, value, null); // 链表中不存在下一个元素,新造Node元素准备新增
// 并且作为 p的下一个元素
// if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 当超过8 准备转换成红黑树结构存储
// treeifyBin(tab, hash);
// break;
// }
// if (e.hash == hash &&
// ((k = e.key) == key || (key != null && key.equals(k)))) 存在相同的Key 终止循环
// break;
// p = e; // 将e赋值给p 返回for循环继续比较
// }
// }
// if (e != null) { // existing mapping for key 存在 key 替换 value
// V oldValue = e.value;
// if (!onlyIfAbsent || oldValue == null)
// e.value = value;
// afterNodeAccess(e);
// return oldValue;
// }
// }
// ++modCount;
// if (++size > threshold) // size是map存放键值对的数量 默认大于12 进行扩容
// resize();
// afterNodeInsertion(evict);
// return null;
// }
// 扩容方法
// final HashMap.Node<K,V>[] resize() {
// HashMap.Node<K,V>[] oldTab = table; // 首次调用 table 是null oldCap
// int oldCap = (oldTab == null) ? 0 : oldTab.length; //首次调用oldCap =0
// int oldThr = threshold; // 首次调用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 //临界值将会变成原来的2倍
// }
// else if (oldThr > 0) // initial capacity was placed in threshold
// newCap = oldThr;
// else { // zero initial threshold signifies using defaults
// 首次调用走到这里
// newCap = DEFAULT_INITIAL_CAPACITY; // 首次调用 默认值时 16
// newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); //首次调用 临界值12 出现
// }
// if (newThr == 0) {
// float ft = (float)newCap * loadFactor;
// newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
// (int)ft : Integer.MAX_VALUE);
// }
// threshold = newThr; // 临界值被重新赋值
// @SuppressWarnings({"rawtypes","unchecked"})
// HashMap.Node<K,V>[] newTab = (HashMap.Node<K,V>[])new HashMap.Node[newCap]; // 首次调用数组出现,
// // 并且初始化一个长度16 HashMap.Node[]数组
// table = newTab; // HashMap.Node[]类型的数组被初始化
// if (oldTab != null) { // 首次调用不用关心这个 扩容时会重新计算hash位置会改变
// for (int j = 0; j < oldCap; ++j) {
// HashMap.Node<K,V> e;
// if ((e = oldTab[j]) != null) {
// oldTab[j] = null;
// if (e.next == null)
// newTab[e.hash & (newCap - 1)] = e;
// else if (e instanceof HashMap.TreeNode)
// ((HashMap.TreeNode<K,V>)e).split(this, newTab, j, oldCap);
// else { // preserve order
// HashMap.Node<K,V> loHead = null, loTail = null;
// HashMap.Node<K,V> hiHead = null, hiTail = null;
// HashMap.Node<K,V> next;
// do {
// next = e.next;
// if ((e.hash & oldCap) == 0) {
// if (loTail == null)
// loHead = e;
// else
// loTail.next = e;
// loTail = e;
// }
// else {
// if (hiTail == null)
// hiHead = e;
// 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;
// newTab[j + oldCap] = hiHead;
// }
// }
// }
// }
// }
// return newTab;
// }
// 链表转换成为红黑树的方法
// final void treeifyBin(Node<K,V>[] tab, int hash) {
// int n, index; HashMap.Node<K,V> e;
// if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) // 当前数组长度小于64不转成红黑树而是数组扩容
// resize();
// else if ((e = tab[index = (n - 1) & hash]) != null) {
// HashMap.TreeNode<K,V> hd = null, tl = null;
// do {
// HashMap.TreeNode<K,V> p = replacementTreeNode(e, null);
// if (tl == null)
// hd = p;
// else {
// p.prev = tl;
// tl.next = p;
// }
// tl = p;
// } while ((e = e.next) != null);
// if ((tab[index] = hd) != null)
// hd.treeify(tab);
// }
// }
}
}
HashMap 总结
HashMap的存储结构简要说明 JDK1.8之前
HashMap的内部存储结构其实是数组和链表的组合,当实例化一个HashMap时,默认情况下JDK会自动创建一个长度为capacity 16的Entry数组,这个长度在哈希表中被称为容量(capacity),在这个数组中可以存放元素的位置称为“桶”(backet),每个桶都有自己的索引,JDK可以根据索引快速查找到桶中的元素,
每一个backet中存储一个元素,即一个Entry对象,但是每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此在一个backet中,是可以生成一个entry链。而且新添加的元素作为链表的head。
添加元素的过程简要说明
向HashMap中添加一个entry(key,value),首先需要计算entry中的key的哈希值(根据key所在类的hashcode()方法计算得到),此哈希值经过处理之后得到底层Entry[]数组中需要存储的位置i,如果位置i上没有元素,entry直接添加,如果位置i上已经存在元素(可能是以链表形式存在多个),需要通过循环的方法,依次比较,如果entry与其他元素的哈希值均不相同,则添加成功(以链表形式存储,且作为链表的头部)。如果哈希值相同,会调用key所在类的equals()方法,返回值为true,使用entry中的value替换原有元素的value,遍历完成后equals()方法返回false,entry依旧添加成功,以链表形式存储,且作为链表的头部。
HashMap的扩容
当HashMap中的元素逐渐增加,哈希冲突的机率将会增大,因为数组的长度是固定的,为了提高查询的效率,HashMap将会对数组进行扩容,而在HashMap的数组扩容之后,原有数组中的数据必须重新计算其在新数组中的位置,并且添加进去,这是比较消耗性能的。HashMap的扩容时机,当HashMap中的元素个数超过临界值(数组中的长度length * 设定的加载因子 loadFactor),就会对数组进行扩容,默认的加载因子是 0.75、数组长度是16,也就是当HashMap的元素个数超过12(临界值),HashMap就会触发扩容方法,将数组的大小扩容为原来的2倍,也就是32,然后重新计算每个元素在数组的位置,所以当我们大致确定HashMap中元素的个数,那么预设元素的个数能够有效提高HashMap的性能(尽量减少扩容次数)。
JDK1.8
HashMap的存储结构修改为数组+链表+红黑树的结合,默认情况下,当实例化一个HashMap时,JDK仅初始化了加载因子,当在第一次调用put方法时,JDK调用扩容方法,初始化一个长度为16的Node数组,这个长度在哈希表中被称为容量(capacity),在这个数组中可以存放元素的位置称为“桶”(backet),每个桶都有自己的索引,JDK可以根据索引快速查找到桶中的元素,
每一个backet中存储一个元素,即一个Node对象,但是每一个Node对象可以带一个引用变量(next),用于指向下一个元素,因此在一个backet中,是可以生成一个Node链,也可能是一个个的TreeNode对象,每一个TreeNode可以有两个叶子节点,left和right,所以在一个桶中,可以生成一个TreeNode树,而新添加的元素作为链表的last,或者树的叶子节点。
HashMap的扩容时机,当HashMap中的元素个数超过临界值(数组中的长度length * 设定的加载因子 loadFactor),就会对数组进行扩容,默认的加载因子是 0.75、数组长度是16,也就是当HashMap的元素个数超过12(临界值),HashMap就会触发扩容方法,将数组的大小扩容为原来的2倍,也就是32,然后重新计算每个元素在数组的位置,所以当我们大致确定HashMap中元素的个数,那么预设元素的个数能够有效提高HashMap的性能(尽量减少扩容次数)。到这里基本与JDK8之前一致,在JDK8中,当一个链表的元素的个数已经达到了8个,此时如果底层数组的长度没有超过64,HashMap会先扩容解决,如果已经达到了64,那么当前链表会转成红黑树存储,节点类型会由Node转为TreeNode类型。当然,如果当前映射关系被移除后,下次调用扩容方法,判断树的节点个数低于6个,也会把树转成链表存储。
LinkedHashMap 简要说明
@Test
public void testLinkedHashMap(){
// LinkedHashMap 是 HashMap的子类 没有重写hashMap的put方法
// 但是声明了一个 HashMap.Node<K,V>的一个子类 Entry<K,V>
// static class Entry<K,V> extends HashMap.Node<K,V> {
// // 除了 HashMap.Node之外还有两个属性 before 和 after 也就是
// 当前元素记录了上一个和下一个元素。
// Entry<K,V> before, after;
// Entry(int hash, K key, V value, Node<K,V> next) {
// super(hash, key, value, next);
// }
// }
//
LinkedHashMap<String, String> map = new LinkedHashMap<>();
// 在 newNode(hash, key, value, null); 时 调用了LinkedHashMap中的 Entry
// 添加的同时记录了上一个元素和下一个元素
map.put("key","value");
System.out.println(map);
}
@Test // 遍历map方式
public void test4() {
Map<String, String> map = new HashMap<String, String>() {{
put("k1", "v1");
put("k2", "v2");
put("k3", "v3");
}};
map.forEach((k, v) -> System.out.println(k + " " + v));
System.out.println("---------------------");
// 获取所有的key
Set<String> keys = map.keySet();
for (String k : keys) {
System.out.println(k + " " + map.get(k));
}
// 获取所有的value
System.out.println("---------------------");
Collection<String> values = map.values();
System.out.println(values);
System.out.println("---------------------");
// 获取所有的键值对
Set<Map.Entry<String, String>> entries = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = entries.iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
System.out.println(entry.getKey() + " " + entry.getValue());
}
}
TreeMap的两种添加方式
@Test
public void treeMapTest() {
// TreeMap 向TreeMap中添加 key-value键值对,要求key必须是由一个类创建的对象 原有是要按照key进行排序
// TreeMap 的两种排序方式:自然排序 自定义排序
// 自然排序
TreeMap<String, Integer> map = new TreeMap<String, Integer>() {{
put("AA", 60);
put("CC", 70);
put("BB", 90);
put("DD", 80);
}};
map.forEach((k, v) -> System.out.println(k + " " + v));
System.out.println("---------------------");
// 定制排序
TreeMap<String, Integer> map1 = new TreeMap<String, Integer>(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return -Integer.compare(o1.hashCode(), o2.hashCode());
}
}) {{
put("AA", 60);
put("CC", 70);
put("BB", 90);
put("DD", 80);
}};
map1.forEach((k, v) -> System.out.println(k + " " + v));
System.out.println("---------------------");
TreeMap<String, Integer> map2 = new TreeMap<String, Integer>((o1, o2) -> -Integer.compare(o1.hashCode(), o2.hashCode())) {{
put("AA", 60);
put("CC", 70);
put("BB", 90);
put("DD", 80);
}};
map2.forEach((k, v) -> System.out.println(k + " " + v));
}
Properties 的简单使用
@Test
public void testProperties() {
//Properties 常用来处理配置文件,Key和Value都是String类型 是 Hashtable的子类
Properties properties = new Properties();
FileInputStream inputStream = null;
try {
inputStream = new FileInputStream("/Users/zkt/project/demo/src/main/resources/jdbc.properties");
properties.load(inputStream);// 加载流对应的文件
System.out.println(properties.getProperty("name"));
System.out.println(properties.getProperty("password"));
} catch (IOException e) {
e.printStackTrace();
}finally {
if (null != inputStream){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
Collections工具类的简单使用
Collections 是一个操作 Set、List和Map等集合的工具类
Collections 中提供了一系列的静态方法对集合元素进行排序,查询和修改等操作,
还提供了对集合对象设置不可变、将集合对象实现同步控制等方法。
@Test
public void testCollections() {
// Collections 是一个操作 Set、List和Map等集合的工具类
// Collections 中提供了一系列的静态方法对集合元素进行排序,查询和修改等操作,
// 还提供了对集合对象设置不可变,对集合对象实现同步控制等方法。
List<Integer> list = new ArrayList<Integer>() {{
add(1);
add(85);
add(8);
add(11);
add(9);
add(22);
add(1);
}};
System.out.println(list);
// 排序操作
// Collections.reverse(List); 反转List中的元素
Collections.reverse(list);
System.out.println(list);
// Collections.shuffle(List<?> list); 对List集合中的元素进行随机排序
Collections.shuffle(list);
System.out.println(list);
// Collections.sort(List<T> list); 根据元素的自然顺序对指定List集合元素进行升序排序
Collections.sort(list);
System.out.println(list);
// Collections.sort(List<T> list, Comparator<? super T> c); 根据指定的Comparator产生的顺序对List集合元素进行排序
Collections.sort(list, (o1, o2) -> Integer.compare(o2, o1));
System.out.println(list);
// Collections.swap(List<?> list, int i, int j); 将指定的list集合中的i处元素与j处元素进行交换
Collections.swap(list, 0, 4);
System.out.println(list);
// 查找替换
// Collections.max(Collection<? extends T> coll) 根据元素的自然顺序 返回集合中的最大元素
Integer max = Collections.max(list);
System.out.println(max);
// Collections.max(Collection<? extends T> coll, Comparator<? super T> comp) 根据指定的Comparator指定的顺序 返回集合中的最大元素
Integer max1 = Collections.max(list, (Integer::compare));
System.out.println(max1);
// Collections.min(Collection<? extends T> coll) 根据元素的自然顺序 返回集合中的最小元素
Integer min = Collections.min(list);
System.out.println(min);
// Collections.min(Collection<? extends T> coll, Comparator<? super T> comp) 根据指定的Comparator指定的顺序 返回集合中的最小元素
Integer min1 = Collections.min(list, (Integer::compare));
System.out.println(min1);
// Collections.frequency(Collection<?> c, Object o) 返回集合中指定的元素出现的次数
int frequency = Collections.frequency(list, 1);
System.out.println(frequency);
// Collections.copy(List<? super T> dest, List<? extends T> src); 将src中的内容复制到dest中
// List<Integer> dest = new ArrayList<>(); 错误写法 // java.lang.IndexOutOfBoundsException: Source does not fit in dest
List<Integer> dest = Arrays.asList(new Integer[list.size()]);
System.out.println(dest);
Collections.copy(dest, list);
System.out.println("------------");
System.out.println(dest);
// Collections.replaceAll(List<T> list, T oldVal, T newVal) 使用新的值替换List对象中所有旧的值
boolean b = Collections.replaceAll(dest, 1, 100);
System.out.println(b);
System.out.println(dest);
// Collections.synchronizedXXX 该方法将指定的集合包装成线程同步的集合,一定程度上解决多线程并发访问集合时的线程安全问题
System.out.println("------------");
List<Integer> syncList = Collections.synchronizedList(dest);
System.out.println(syncList);
}