Map
- 用于保存Key-value形式的数据
- Map的Key用Set存放,不允许重复
- key、value可以是任意形式的数据,且必须存在一一对应的关系,一对<Key,value>称为一个Entry
HashMap
- HashMap是使用频率最高的实现类
Hash
数组优劣势:快速查找;需要开辟一整块内存空间,插入删除元素及“”扩容“”’起来比较困难
链表优劣势:不用开辟整块内存空间,插入删除元素较方便;查找元素需要按头、尾指针逐个遍历各个元素,比较慢
HashMap:
综合了数组和链表的优点,采用数组+链表的形式实现,java8以后采用数组+链表+红黑树的形式实现,发生hash碰撞时,数组同一个位置上的元素以链表的形式进行存储,当链表的长度达到8时,该位置的链表升级为红黑树
**红黑树:**自平衡的二叉查找树,引入红黑树就是为了解决链化严重的问题,提高查找效率
Hash特点:
将任意长度的输入,通过某种映射规则变成固定长度的输出
映射规则就是对应的Hash算法,映射后的二进制串就是哈希值
- 执行高效,长文本也能很快计算出哈希值
- 不能通过哈希值反推出原始数据
- 输入数据微小变化都会得到不同的哈希值,相同数据具有相同的哈希值
- hash的原理是将输入空间的值映射到hash空间内,而hash空间远小于输入的空间,所以,哈希碰撞在所难免-----这就是为什么添加过程中hash key的哈希值相同,需要使用equals()方法,equals方法不同的话,数据依旧可以添加成功
HashMap
使用方法:
HashMap<Integer, String> map = new HashMap<>();
// 添加元素
map.put(1, "a");
map.put(2, "b");
map.put(3, "c");
map.put(4, "d");
map.put(5, "e");
map.put(6, "f");
map.put(7, "g");
// 输出列表形式
System.out.println("toString形式:");
System.out.println(map);
// key以Set的形式进行存储,不可重复
// 当前key值类型为整型
Set<Integer> keys = map.keySet();
System.out.println("KEY:");
System.out.println(keys);//使用迭代器循环
System.out.println("迭代器遍历形式:");
// value以Collection 的形式存储,可以重复
// 当前value类型为String
Collection<String> values = map.values();
Iterator<String> iterator = values.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
注:
-
所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写:equals()和hashCode()
-
所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类要重写:equals()
-
一个key-value构成一个entry,所有的entry构成的集合是Set:无序的、不可重复的
-
HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true,hashCode 值也相等。
-
HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
重要常量:
//默认数组长度为16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
// 最大容量:2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认设定的负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//树化阈值,链表长度超过8,链表转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//树降级成为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 第二个树化阈值----数组元素个数至少要达到64,且链表长度达到8,才能转化成红黑树
static final int MIN_TREEIFY_CAPACITY = 64;
//其余重要概念:
table:存储元素的数组,总是2的n次幂
entrySet:存储具体元素的集
size:HashMap中存储的键值对的数量
modCount:HashMap扩容和结构改变的次数。
threshold:扩容的临界值,=容量*填充因子
loadFactor:填充因子
HashMap的扩容原理:
前面说过,hash空间远小于输入空间,同时由于数组的长度是固定的,因此,当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,使得链化逐渐严重,而链表查询需要逐个比较各个链表元素,极大地降低了查询效率。
所以需要对HashMap的数组进行扩容,提升查询效率。在HashMap数组扩容之后,原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize()方法。
何时扩容及扩容细节?
数组长度为length(默认值为16),负载因子为loadFactor(默认值为0.75),当HashMap中的元素个数超过length* loadFactor时(16* 0.75=12),数组进行扩容。扩容后,数组的大小扩展为原来长度的2倍,即 2*16=32,然后重新计算每个元素在数组中的位置,这是一个非常消耗性能的操作,所以如果预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。
当HashMap中的其中一个链的对象个数如果达到了8个,此时如果capacity没有达到64,那么HashMap会先扩容解决,如果已经达到了64,那么这个链会变成树,结点类型由Node变成TreeNode类型。当然,如果当映射关系被移除后,下次resize方法时判断树的结点个数低于6个,也会把树再转为链表。
JDK1.8相较于之前的变化:
- HashMap map = new HashMap();//默认情况下,先不创建长度为16的数组
- 当首次调用map.put()时,再创建长度为16的数组
- 数组为Node类型,在jdk7中称为Entry类型
- 形成链表结构时,新添加的key-value对在链表的尾部(七上八下)
- 当数组指定索引位置的链表长度>8时,且map中的数组的长度> 64时,此索引位置上的所有key-value对使用红黑树进行存储。
负载因子对HashMap的影响:
负载因子的大小决定了HashMap的数据密度。
- 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
- 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
- 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。
LinkedHashMap
是HashMap的子类,在HashMap存储结构的基础上,使用一对双向链表来记录添加元素的顺序
与LinkedHashSet相似,LinkedHashMap可以维护Map的迭代顺序-----与Key-value对的插入顺序一致
TreeMap
- TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。
TreeMap 可以保证所有的 Key-Value 对处于有序状态。 - TreeSet底层使用红黑树结构存储数据
TreeMap 的Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有的 Key 应该是同一个类的对象,否则将会抛出ClasssCastException
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现Comparable 接口TreeMap判断两个key相等的标准:两个key通过compareTo()方法或者compare()方法返回0。