Java集合框架总图
Map接口和具体实现类
Java集合总体分为两个根接口,Map和Collection,其中Collection是单列集合,Map是双列集合。
Map与List、Set接口不同,它并不继承自Collection,它是由一系列键值对组成的集合,提供了key到value的映射。在Map中一个key对应一个value,所以key的存储不可重复,但value可以。
哈希结构——通过关键码找到值的数据结构
哈希函数——建立关键字和值的映射关系
注:好的哈希函数能使值均匀的分步在哈希结构中
生成哈希函数的两种方式
(1)直接寻址法:f(x)=kx+b(k和b是常数)
(2)除留余数法:f(x)=x mod m (m<p)(m小于哈希长度p)
哈希冲突的两种解决方式
(1)链地址法:当发生哈希冲突时,将哈希到对应位置的值连接在该位置的数据后面。
(2)线性探测法
Map接口常用方法
int size(); //map集合中存储的键值对的个数
boolean isEmpty(); //判断map集合是否为空 true:空 false:不为空
boolean containsKey(Object key) //判断集合中是否存在该键key
boolean containsValue(Object value); ////判断集合中是否存在该值value
V get(Object key) //通过键获取对应的值
V put(K key, V value); //添加键值对
V remove(Object key); //删除操作 通过键删除键值对
常用三种遍历方式
System.out.println("通过键值对遍历");
//1、通过键值对遍历
Iterator <Map.Entry <String, String>> iterator = hashMap.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry <String, String> entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key+":"+value);
}
System.out.println();
System.out.println("通过键遍历");
//通过键遍历
Iterator <String> iterator1 = hashMap.keySet().iterator();
while (iterator1.hasNext()) {
System.out.println(iterator1.next());
}
System.out.println();
System.out.println("通过值遍历");
//通过值遍历
Iterator <String> iterator2 = hashMap.values().iterator();
while (iterator2.hasNext()) {
System.out.println(iterator2.next());
}
一、HashMap
HashMap底层是一个Entry数组,Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。当发生哈希冲突时,HashMap采用链表的方式来解决,在对应的数组位置存放链表的头结点。对链表而言,新加入的节点会从头结点加入。
1、继承关系
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable
2、属性分析
//默认的初始容量是16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
//最大容量
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的填充因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//当桶(bucket)上的结点数大于这个值时会转成红黑树
static final int TREEIFY_THRESHOLD = 8;
//当桶(bucket)上的结点数小于这个值时树转链表
static final int UNTREEIFY_THRESHOLD = 6;
//桶中结构转化为红黑树对应的table的最小大小
static final int MIN_TREEIFY_CAPACITY = 64;
//存储元素的数组,总是2的幂次倍
transient Node<k,v>[] table;
//存放具体元素的集
transient Set<map.entry<k,v>> entrySet;
//存放元素的个数,注意这个不等于数组的长度。
transient int size;
//每次扩容和更改map结构的计数器
transient int modCount;
//临界值
//实际大小(容量*填充因子)超过临界值时,会进行扩容
//threshold = capacity * loadFactor,
//当Size>=threshold的时候,就要考虑对数组扩容
//即这是衡量数组是否需要扩容的一个标准
int threshold;
//加载因子
//用来控制数组存放数据的疏密程度
//loadFactor越趋近于1,数组中存放的数据(entry)就越多,也就越密,会让链表的长度增加
//loadFactor越小,趋近于0,数组中存放的数据(entry)也就越少,就越稀疏
//loadFactor太大导致查找元素效率低
//loadFactor太小导致数组的利用率低,存放的数据会很分散
//loadFactor的默认值为0.75f是官方给出的一个比较好的临界值
//给定的默认容量为16,负载因子为 0.75
//Map在使用过程中不断的往里面存放数据,当数量达到了16*0.75=12
//就需要将当前16的容量进行扩容
//扩容过程涉及到rehash、复制数据等操作,非常消耗性能
final float loadFactor;
HashMap内部定义了一个hash表数组,元素通过哈希转换函数将哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,通过查看HashMap.Entry的源码可以看出是一个单链表结构:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
3、构造函数
//如果指定了加载因子和初始容量,就调用这个构造方法
public HashMap(int initialCapacity, float loadFactor) {
//判断合法性
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
// Find a power of 2 >= initialCapacity
//初始容量
int capacity = 1;
//确保容量为2的n次幂,使capacity为大于initialCapacity的最小的2的n次幂
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor = loadFactor;
threshold = (int)(capacity * loadFactor);
table = new Entry[capacity];
init();
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
threshold = (int)(DEFAULT_INITIAL_CAPACITY * DEFAULT_LOAD_FACTOR);
table = new Entry[DEFAULT_INITIAL_CAPACITY];
init();
}
4、常用方法
(1)put()
key不可重复但可以为空,value可重复可为空,插入无序。
public V put(K key, V value) {
//如果table数组为空数组,进行数组填充(为table分配实际内存空间),入参为threshold,此时threshold为initialCapacity 默认是1<<4
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
//如果key为null,存储位置为table[0]或table[0]的冲突链上
if (key == null)
return putForNullKey(value);
int hash = hash(key);//对key的hashcode进一步计算,确保散列均匀
int i = indexFor(hash, table.length);//获取在table中的实际位置
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
//如果该对应数据已存在,执行覆盖操作。用新value替换旧value,并返回旧value
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;//保证并发访问时,若HashMap内部结构发生变化,快速响应失败
addEntry(hash, key, value, i);//新增一个entry
return null;
}
注:
①inflateTable方法
用于为主干数组table在内存中分配存储空间,通过roundUpToPowerOf2(toSize)可以确保capacity为大于或等于toSize的最接近toSize的二次幂,比如toSize=13,则capacity=16;to_size=16,capacity=16;to_size=17,capacity=32。
private void inflateTable(int toSize) {
int capacity = roundUpToPowerOf2(toSize);//capacity一定是2的次幂
threshold = (int) Math.min(capacity * loadFactor, MAXIMUM_CAPACITY + 1);//此处为threshold赋值,取capacity*loadFactor和MAXIMUM_CAPACITY+1的最小值,capaticy一定不会超过MAXIMUM_CAPACITY,除非loadFactor大于1
table = new Entry[capacity];
initHashSeedAsNeeded(capacity);
}
②roundUpToPowerOf2方法
使数组长度一定为2的次幂,Integer.highestOneBit用来获取最左边的bit(其他bit位为0)所代表的数值。
private static int roundUpToPowerOf2(int number) {
// assert number >= 0 : "number must be non-negative";
return number >= MAXIMUM_CAPACITY
? MAXIMUM_CAPACITY
: (number > 1) ? Integer.highestOneBit((number - 1) << 1) : 1;
}
③hash函数
用异或、移位等运算,对key的hashcode进一步计算以及并调整二进制位,保证最终获取的存储位置尽量分布均匀。
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
//h&(length-1)保证获取的index一定在数组范围内
return h & (length-1);
}
以上两步获取到当前元素要存储的哈希表的索引位置。
拓展:不同场景下的put
hash (通过 key 计算出的 int 值)
key (传入的 key)
value (传入的 value)
场景1:存储时 HashMap的 Node数组未初始化。
初始化数组,直接放入。
场景2:当前坐标下不存在值。
直接存放到0。
场景3:当前坐标下只有一个值并且存储的 key相等。
将旧值覆盖。
场景4:当前坐标下只有一个值并且存储的 key不相等。
将新值存到下标为0的next属性处,形成链表结构。
场景5:当前坐标下有多数值(小于8)且其中有一个值和新传入的 key相同。
替换旧值,并复制旧值的next属性给新值。
场景6:当前坐标下有多数值(小于8)且 key 都不相同。
直接接到最后面,若连完大于8则转为红黑树。
场景7:当前坐标下有多数值(大于等于8(链表长度大于等于8的时候,当前数组长度大于64时才会转换成红黑树)),数据结构已经变为红黑树了。
来源:CSDN
作者:king9819
链接:https://blog.csdn.net/kiing9819/article/details/104104802