1. Map
- 存储有映射关系的数据
key-value
; - key不允许重复,value可以重复,相同的key值,后加入的value会替换掉之前的value;
- key和value都可以为null,但key只能有一个null。
1.1 Map接口常用方法
-
V put(K key, V value)
添加k-v,返回添加value值 -
V remove(Object key)
:根据key删除,返回删除value值 -
boolean isEmpty()
:判断是否为空 -
void clear()
:清空map -
boolean containsKey(Object key)
:判断是否存在key -
Set<Map.Entry<K,V>> entrySet()
:返回指向键值对的entrySet
-
Set<K> keySet()
:返回存放key集合的set
-
Collection<V> values()
:返回存放value的集合。
1.2 HashMap
Java7及之前的
HashMap
的底层结构是数组+链表,在Java8及之后底层由数组+链表+红黑树组成。
1.2.1 存储结构
-
Map
中存放的键值对都是放在一个实现类了Map.Entry
接口的Node
对象中:
// 静态内部类,存放 k-v 键值对
static class Node<K,V> implements Map.Entry<K,V> {
// 根据key计算出的hash
final int hash;
final K key;
V value;
// 指向下一个节点
HashMap.Node<K, V> next;
Node(int hash, K key, V value, HashMap.Node<K, V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
- 并且为了方便遍历在
HashMap
存在一个EntrySet
集合,看似存放的是Map.Entry<K,V>
实际上 “存放” 的是向上转型之后的HashMap.Node<K,V>
(在EntrySet
中没有创建新的Entry
对象,而是指向HashMap
中创建的Node
对象),常用方法getKey()
和getValue()
都是由这个Entry
提供的:
transient Set<Map.Entry<K,V>> entrySet;
部分方法:
interface Entry<K,V> {
K getKey();
V getValue();
}
1.2.1 存储和扩容机制
因为
HashSet
底层是使用HashMap
实现的,所以这部分可以参考上期博客。
- 构造函数:初始化加载因子为0.75
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
-
put
请参考上篇博客。 - 扩容和树化触发:
树化方法(部分):只有table的长度超过64时才会进行树化,否则进行扩容。
final void treeifyBin(Node<K,V>[] tab, int hash) {
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
}
扩容方法:如果在某一索引后面链接的节点超过8个就会对table进行扩容(默认扩大两倍)
测试代码(Hash值相同):因为修改了所用作为key的A对象的hash值相等,也就意味着除第一个存在table表中,其它的都会以链表形式链接在后面,等到链接数量超过8个就会对table进行扩容,知道table的长度超过64时会进行树化。
/**
* @Description HashMap 树化 和 扩容
* @date 2022/4/4 9:01
*/
public class HashMapToTree {
public static void main(String[] args) {
HashMap<A,String > map = new HashMap<>();
for (int i = 0; i < 12; i++) {
// 对象的hashCode 相同, 但是equals不同,所以这里也有12条数据
map.put(new A(i),"Hello Map");
}
System.out.println(map);
}
}
class A{
private int num;
public A(int num){
this.num = num;
}
// 重写当前对象的hashCode,使其全部一样
// hash值相同,新加入对象就不会占用table空间,是以链表的方式添加到后面
@Override
public int hashCode() {
return 100;
}
@Override
public String toString() {
return "A{" +
"num=" + num +
'}';
}
}
测试代码(Hash值不同):如果hash值不同的情况下,会将这个对象放在不同的索引位置,直到 table 长度超过临界值 ,会对table进行扩容。
public static void diffHash(){
HashMap<Integer ,String > map = new HashMap<>();
// 因为不能保证hash值一定不同,多运行几次。
for (int i = 0; i < 20; i++) {
map.put((int) (Math.random() * 100),"A");
}
}
1.2 HashTable
- 不允许在键和值上放null,否则抛出空指针异常;
- 使用
synchronized
来保证线程安全。
1.2.1 扩容机制
新创建的HahsTable
:
当长度超过临界值时进行扩容之后:
- 添加方法:显然
HashTable
是一个头插法的单向链表+数组实现的。
/**
* 添加方法
* @param key k
* @param value v
* @return v
*/
public synchronized V put(K key, V value) {
// 值为空抛出异常
if (value == null) {
throw new NullPointerException();
}
Hashtable.Entry<?,?> tab[] = table;
// 获取hashCode
int hash = key.hashCode();
// 根据hashCode计算索引
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
// 获取当前索引位置的对象
Hashtable.Entry<K,V> entry = (Hashtable.Entry<K,V>)tab[index];
// 判断当前索引位置是否有对象
// 如果有对象,就用next获取它的链表对象
for(; entry != null ; entry = entry.next) {
// 如果计算出的hash值相同,并且key相同
if ((entry.hash == hash) && entry.key.equals(key)) {
// 新值替换旧值
V old = entry.value;
entry.value = value;
// 返回旧值
return old;
}
}
// 当前索引如果没有对象
addEntry(hash, key, value, index);
return null;
}
/**
* 添加entry
* @param hash
* @param key
* @param value
* @param index
*/
private void addEntry(int hash, K key, V value, int index) {
// 修改次数++
modCount++;
Hashtable.Entry<?,?> tab[] = table;
// 判断元素数量是否大于临界值
if (count >= threshold) {
// 执行扩容方法
rehash();
// 修改之后的table赋值给成员变量
tab = table;
// 执行扩容之后修改的hash
hash = key.hashCode();
// 计算新的索引
index = (hash & 0x7FFFFFFF) % tab.length;
}
@SuppressWarnings("unchecked")
// 将当前索引的对象赋值给对象e
Hashtable.Entry<K,V> e = (Hashtable.Entry<K,V>) tab[index];
// 将新的节点变为当前索引位置的节点,原先的节点作为新节点的next
tab[index] = new Hashtable.Entry<>(hash, key, value, e);
// 数量++
count++;
}
- 扩容方法:
/**
* 扩容方法
*/
protected void rehash() {
// 记录当前table长度
int oldCapacity = table.length;
// 存储table值
Hashtable.Entry<?,?>[] oldMap = table;
// 新的容量为 原先容量 * 2 + 1
int newCapacity = (oldCapacity << 1) + 1;
// 如果新容量大于最大容量
if (newCapacity - MAX_ARRAY_SIZE > 0) {
// 如果原先容量已经是最大容量,无法再进行扩容,直接返回
if (oldCapacity == MAX_ARRAY_SIZE)
return;
// 扩容到最大容量
newCapacity = MAX_ARRAY_SIZE;
}
// 创建一个新容量大小的entry数组
Hashtable.Entry<?,?>[] newMap = new Hashtable.Entry<?,?>[newCapacity];
// 修改次数++
modCount++;
// 计算新的临界值
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
// 把新的数组赋值给table
table = newMap;
// 循环赋值,将原先的table中的数据复制给新的table
for (int i = oldCapacity ; i-- > 0 ;) {
for (Hashtable.Entry<K,V> old = (Hashtable.Entry<K,V>)oldMap[i]; old != null ; ) {
// 将以链表形式存在的节点也重新赋值
Hashtable.Entry<K,V> e = old;
old = old.next;
// 重新计算hash
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Hashtable.Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
1.3 Properties
也是以键值对方式存储,主要用于配置文件。
key和value都不可以用null
没啥可写的。
1.4 TreeSet和TreeMap
最大的特点是有序且唯一(set值唯一,map的key唯一 )的,可以默认排序,也可以指定排序规则,最强的是根据规则可以指定是否能加入。
1.4.1 构造函数
实际上
TreeSet
的底层用的是TreeMap
。也就意味着他俩的构造函数都是调用的TreeMap
的。
- 无参构造
public TreeSet() {
this(new TreeMap<E,Object>());
}
- 传入比较器
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
指定排序规则
int compare(T o1, T o2);
1.4.2 添加和扩容机制
- 添加元素
/**
* 添加元素
* @param e 元素
* @return true / false
*/
public boolean add(E e) {
// 这里实际上调用的是TreeMap.put方法
return m.put(e, PRESENT)==null;
}
2022.04.04 没有学习树的相关,姑且暂做分析。
/**
* 添加方法
* @param key k
* @param value v
* @return v
*/
public V put(K key, V value) {
// 获取根节点
TreeMap.Entry<K,V> t = root;
// 如果根节点等于空直接加入
if (t == null) {
compare(key, key);
root = new TreeMap.Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
// 记录父母节点
TreeMap.Entry<K,V> parent;
// 将传入的比较器赋值
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
// 判断比较规则返回的值
cmp = cpr.compare(key, t.key);
// 小就和父节点的左边比较,大就和右边比较
// 如果相等直接返回 0 :TreeSet(无法加入)、(TreeMap会执行替换)
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);// 从根节点依次往下找。
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
// 默认比较器
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
// 新建节点对象
TreeMap.Entry<K,V> e = new TreeMap.Entry<>(key, value, parent);
// 根据判断的大小排序,确定放在左边或者右边
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
完结了。