双列集合特点
- 双列集合一次需要添加一对数据,分别是键和值
- 键不能重复,值可以重复
- 键和值是一一对应的,每一个键只能找到自己对应的值
- 键 + 值这个整体称为“键值对”或者“键值对对象”,Java中叫“Entry对象”
双列集合的体系结构

Map的常见API
- V put(K key,V Value):添加元素【如果键存在,会把原有的键值对覆盖,并返回被覆盖的键值对的值,如果不存在,返回null】
- V remove(Object key):根据键删除键值对,并返回被删除的键值对的值
- void clear():清空所有的键值对
- boolean containsKey(Object key):判断集合是否包含指定的键【键存在:true 不存在:false】
- boolean containsValue(Object key):判断集合是否包含指定的值【键存在:true 不存在:false】
- boolean isEmpty():判断集合是否为空
- int size():集合的长度,集合中键值对的个数
Map集合的遍历方式
通过键找值
Set<String> keys = map.keySet();
for (String key : keys) {
System.out.println(map.get( key ));
}
键值对遍历
Set<Map.Entry<String, String>> set = map.entrySet();
for (Map.Entry<String, String> entry : set) {
System.out.println(entry.getKey()+":"+entry.getValue());
}
Lambda表达式遍历
map.forEach( (key,value)-> System.out.println(key+":"+value) );
HashMap
- HashMap是Map下面的实现类
- 直接使用Map里面的方法即可
- 键无序【存和取得顺序】、不重复、无索引
- HashMap根HashSet底层原理一样,都是哈希表结构,唯一不同的是,会创建Entry对象,通过键计算哈希值,如果计算出来的哈希值是一样的,新的Entry对象会覆盖原有的Entry对象,其他的根HashSet一样的
- 依赖hashCode方法和equals方法保证键的唯一
- 如果键存储的是自定义对象,需重写hashCode方法和equals方法,如果值存储的自定义对象,就不需要重写hashCode方法和equals方法
LinkedHashMap
- 由键决定:有序【存和取的顺序】、不重复、无索引
- 原理:底层数据结构是哈希表,只是每个键值对额外多一个双向链表机制记录存储的顺序
TreeMap
- TreeMap和TreeSet底层原理一样,都是红黑树结构
- 由键决定:不重复、无索引、可排序【对键进行排序】
- 默认按照键的从小到大进行排序,也可以自定义规则排序
两种排序方式
- 实现Comparable接口,指定比较规则
- 创建集合时传递Comparator比较器对象,指定比较规则
HashMap源码分析
Map<String,String> map = new HashMap<>();
map.put( "ab","ba" );
map.put( "ac","ca" );
map.put( "ad","da" );
map.put( "ae","ea" );
public V put(K key, V value) {//key是键,value是值
//如果覆盖了原来的键值对,就返回被覆盖的键值对的值,没有覆盖就返回null
return putVal(hash(key), key, value, false, true);
}
//利用键计算对应的哈希值,在把计算出的哈希值做一些额外的处理
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//hash:键的哈希值 key:键 value:值 onlyIfAbsent:如果键重复了是否保留【true:保留 false:不保留】
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab;//定义局部变量用来记录哈希表中数组的地址值
Node<K,V> p;//临时的变量,用于记录键值对对象的地址值
int n, i;//n:数组的长度 i:索引
//tab = table将哈希表中数组的地址值赋值给局部变量tab
if ((tab = table) == null || (n = tab.length) == 0)
//1.如果是第一次添加元素,底层会创建一个默认长度16,加载因子0.75的数组
//2.如果不是第一次添加,会看数组中的元素是否到达需要扩容的条件,
//如果没有达到,会做任何操作,
//如果达到了,底层会将数组扩容到原来的2倍,并把数据全部转移到新的哈希表中
//并把当前数组的长度赋值给n
n = (tab = resize()).length;
//用数组的长度和键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
i = (n - 1) & hash]
//获取数组中对应元素的数据
p = tab[i]
if (p == null)
//底层会创建一个键值对对象,直接放到数组中
tab[i] = newNode(hash, key, value, null);
//不是第一次添加元素是就走else
else {
Node<K,V> e; K k;
//数组中键值对的哈希值与当前需要添加键值对的哈希值
boolean f = p.hash == hash;
if (f && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)
//判断数组中获取出来的键值对是不是红黑树中的节点
//如果是,则调用putTreeVal,把当前节点按照红黑树的规则添加到树中
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果从数组中获取出来的键值对不是红黑树中的节点
//表示此时下面挂的是链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//此时会创建一个新节点,挂在下面形成链表
p.next = newNode(hash, key, value, null);
//判断当前链表长度是否超过8,如果超过8,调用方法treeifyBin
//treeifyBin底层还会继续判断
//判断数组长度是否大于等于64
//如果同时满足这两个条件,就会把这个链表转成红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//如果哈希值相同,就会调用equals方法比较内部的属性值是否相同
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//覆盖值需要走此处的if语句
//如果e为null,表示不需要覆盖任何元素
//如果e不为null,表示当前的键之一样的,值就会被覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
//左边:当前要添加的值
//右边:e的value值
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
//threshold:记录的是数组的长度 * 0.75,就是哈希表的扩容机制
if (++size > threshold)
resize();
afterNodeInsertion(evict);
//表示当前没有覆盖任何元素
return null;
}
TreeMap源码分析
//TreeMap集合的get方法源码分析
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;//表示键
V value;//表示值
Entry<K,V> left;//表示左节点的地址
Entry<K,V> right;//表示右节点的地址
Entry<K,V> parent;//表示父节点的地址
boolean color = BLACK;//表示当前节点的颜色
}
public V get(Object key) {
//调用getEntry方法获取Entry对象
Entry<K,V> p = getEntry(key);
//判断对象是否为null,为null返回null,不是则返回对象中的值
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
//判断有没有传入comparator
if (comparator != null)
//调用getEntryUsingComparator方法,使用比较器做查询
return getEntryUsingComparator(key);
//判断传入的键是否为null
if (key == null)
//如果查询的键为null,抛出空指针异常
throw new NullPointerException();
@SuppressWarnings("unchecked")
//把Object类型的键向下转型为Comparable
Comparable<? super K> k = (Comparable<? super K>) key;
//先把二叉树的根节点赋值给p
Entry<K,V> p = root;
//如果p不为空,一直循环作比较
while (p != null) {
//调用compareTo方法进行比较
int cmp = k.compareTo(p.key);
//如果局部变量cmp小于0,表示要查询的键小于节点的数值
if (cmp < 0)
//把p的左节点赋值给p对象
p = p.left;
//如果局部变量cmp大于0,表示要查询的键大于节点的数值
else if (cmp > 0)
//把p的右节点赋值给p对象
p = p.right;
else
//要查找的键等于节点的值,就把当前Entry对象直接返回
return p;
}
//没有找到要查找的结点的值,就会返回null
return null;
}
//传入比较器的情况下
final Entry<K,V> getEntryUsingComparator(Object key) {
@SuppressWarnings("unchecked")
//把Object类型的key向下转型为对应的键的类型
K k = (K) key;
//初始化比较器对象【起名字】
Comparator<? super K> cpr = comparator;
if (cpr != null) {
//把二叉树的根节点赋值给p对象
Entry<K,V> p = root;
//循环用要查找的键的值和节点中键的值作比较
while (p != null) {
//调用比较器的compare方法
int cmp = cpr.compare(k, p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
}
return null;
}
//put()添加方法源码分析
public V put(K key, V value) {
//获取根节点赋值给t
Entry<K,V> t = root;
//判断根节点是否为空
if (t == null) {
//为空就对key进行非空和类型检验
compare(key, key);
//新建节点
root = new Entry<>(key, value, null);
//把集合的长度设置为1
size = 1;
//记录集合被修改的次数
modCount++;
//添加成功就返回null
return null;
}
//如果根节点不为空则执行以下代码
int cmp;
Entry<K,V> parent;
//把比较器对象赋值给cpr
Comparator<? super K> cpr = comparator;
//判断比较器对象为空则执行一下代码
if (cpr != null) {
do {
//把当前节点赋值给变量parent
parent = t;
//比较当前节点的键和要存储的键的大小
cmp = cpr.compare(key, t.key);
//要存储的键小于当前节点的键的值,则继续和当前节点的左节点进行比较
if (cmp < 0)
t = t.left;
//要存储的键大于当前节点的键的值,则继续和当前节点的右节点进行比较
else if (cmp > 0)
t = t.right;
//要存储的键等于当前节点的键的值,则调用setValue方法设置新的值,并结束循环
else
return t.setValue(value);
} while (t != null);//循环直到遍历到叶子节点结束循环
}
//如果比较器对象不为空,执行以下代码
else {
//如果要保存的键为空,判处空指针异常
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
//把键转型为Comparable类型
Comparable<? super K> k = (Comparable<? super K>) key;
do {
//把当前节点赋值给变量parent
parent = t;
//比较要存储的键和当前节点的键的值
cmp = k.compareTo(t.key);
//要存储的键小于当前节点的键的值,则继续和当前节点的左节点进行比较
if (cmp < 0)
t = t.left;
//要存储的键大于当前节点的键的值,则继续和当前节点的右节点进行比较
else if (cmp > 0)
t = t.right;
//要存储的键等于当前节点的键的值,则调用setValue方法设置新的值,并结束循环
else
return t.setValue(value);
} while (t != null);//循环直到遍历到叶子节点结束循环
}
//创建新的节点对象,保存键值对,设置父节点
Entry<K,V> e = new Entry<>(key, value, parent);
//新的键的值小于父节点键的值
if (cmp < 0)
//就把要保存的键值对设置为当前节点的左节点
parent.left = e;
else
//如果要保存的键的值大于父节点键的值,则把要保存的节点设置为当前节点的右节点
parent.right = e;
//保持红黑树的平衡
fixAfterInsertion(e);
//集合长度加一
size++;
//集合修改次数加一
modCount++;
//返回被覆盖的值
return null;
}