1. Map接口的基本介绍(jdk8为例)
- Map与Collection并列存在,用于保存具有映射关系的数据:key-value
- Map中的key和value可以是任何引用类型的数据,会封装到HashMap$Node对象中
- Map中的key不允许重复,原因和HashSet一样(如果出现一样的情况,后者会将前者覆盖)
- Map中的value可以重复
- Map中的key可以为null,value也可以为null,但是key为null只能有一个,value为null可以有多个
- 常用String类作为Map的key
- key和value之间存在单向的一对一关系,即通过key总能找到对应的value
- Map的存放是无序的,和Set一样
2. Map源码分析
2.1 HashMap接口的特点
Map存放数据的key-value示意图,一对key-value是存放在一个Node中的。因为Node类实现了Entry接口,所以也说一对key-value就是一个Entry。
注意:很多地方说到,Map的key是存放在Set的实现类里面,value是存放在Collection的实现类里,这样的说法十分不严谨,实际上,真正的key-value是存放在Node节点里面的。Set和Collection中只是引用指向Node节点上。key-value为了方便遍历,还会在底层创建EntrySet集合,该集合存放的元素的类型是Entry,而一个Entry对象就包含了key-value。Entry对象的key存放的值是Node节点的key,Entry对象的value存放的值是Node节点的value。那么为什么要这么设计呢?因为便于方便遍历,Entry对象中有两个方法getKey(),getValue()方法。总结来说,就是把Node转为Entry(为什么Node能转为Entry呢,因为Node实现了Entry接口,所以Node的实例能赋值给Entry),然后将Entry放到EntrySet集合中。
Entry的getKey(),getValue()方法
public class Map_ {
public static void main(String[] args) {
//
Map map = new HashMap();
map.put(1,"zjh");
map.put(2,"jxj");
System.out.println(map.toString());
Set set = map.entrySet();
for (Object entry:set){
Map.Entry me = (Map.Entry)entry;
System.out.println(me.getKey());
System.out.println(me.getValue());
}
}
}
结果
{
1=zjh, 2=jxj}
1
zjh
2
jxj
重点:其实EntrySet集合中的数据是指向Node的,并不是复制Node的值来放到EntrySet中
public class Map_ {
public static void main(String[] args) {
//
Map map = new HashMap();
map.put(1,"zjh");
map.put(2,"jxj");
System.out.println(map.toString());
Set set = map.entrySet();
for (Object entry:set){
Map.Entry me = (Map.Entry)entry;
System.out.println(me.getKey());
System.out.println(me.getValue());
}
}
}
Set set = map.entrySet();中打断点
由此可见,上述说法成立。
public class Map_ {
public static void main(String[] args) {
//
Map map = new HashMap();
map.put(1,"zjh");
map.put(2,"jxj");
Collection values = map.values();
Set keySet = map.keySet();
}
}
由此段代码可得知,map的values和keys存放的结构。(有兴趣可以深究源码)
2.2 HashMap底层机制及源码分析
- k-v是一个Node实现了Map.Entry,查看HashMap的源码可以看到
- jdk7的HashMap是数组+链表,jdk8的HashMap是数组+链表+红黑树
- HashMap底层维护了Node类型的数组table,默认为null
- 当创建对象时,将加载因子(loadfactor)初始化为0.75。加载因子应用于计算临界值,临界值 = 容量 * 加载因子
- 当添加key-value时,通过key的哈希值得到在table的索引位置,然后判断该索引位置是否有元素,如果没有元素则添加,如果有元素,则判断该元素的key是否和准备加入的key是否相同,如果相同,则替换value,如果不相同,则需要判断该索引处的Node节点是链表形式还是红黑树形式,并做出相应处理,如果添加时发现容量不够,则需要扩容。
- 第一次添加,则需要扩容的容量是16,临界值是12
- 以后再次扩容,则需要扩容table为原来的2倍,临界值为原来的2倍,即24,以此类推
- 在java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认为64),就会进行红黑树化(并不是将所有的链表进行红黑树化)
2.2.1 HashMap源码分析
public class Map_ {
public static void main(String[] args) {
Map map = new HashMap();
map.put("java",10);
map.put("php",10);
map.put("java",20);
System.out.println("map->"+map.toString());
}
}
对此代码进行debug分析
初始化HashMap,默认容量是16,加载因子为0.75
接下来是对基本数据类型的装箱,因为我们添加的value是10
接下来执行hash算法,计算key的hash值
之后获得hash值后进行putVal()方法,这是最核心的方法,为了一步步分析,将核心代码拷贝下来,不以图片的形式进行展示,方便做笔记
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
// 创建tab数组,和p节点以及辅助变量
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 判断tab是否为空或者tab的长度是否为0,如果符合条件将执行resize方法,resize方法就是对tab进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 此时tab表以及初始化为16,临界值为12,加载因子为0.75
// 通过hash得到索引位置,如果索引位置为空,则直接进入到此位置
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
// 进入到这一步就说明,得到的索引位置已经有Node节点了
// 定义e节点,注意此时的p节点已经是该索引位置上的第一个节点了
Node<K,V> e; K k;
// 判断p的hash值和即将存放的key的hash值是否相同,且判断key是否相同或者key不为空且内容相同,则将p赋值给e,即不添加
// 注意,覆盖的操作在下面
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果当前节点是TreeNode类型的数据,执行putTreeVal方法(如果当前节点已经是红黑树了,就按照红黑树的方式进行处理)
else if (p instanceof TreeNode)
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);
// 判断条件,判断是否需要树化 即判断单个链表的长度是否大于等于7-1
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
break;
}
// 在循环的时候发现整个链表中有和待加入节点有相同的,就break
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
// 覆盖操作
if (e != null) {
// existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
// 判断阈值,判断是否需要扩容
++modCount;
// threshold:临界值
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
2.3 HashTable底层机制及源码分析
- HashTable存放的元素是键值对,key-value
- HashTable的键值对都不能为null
- HashTable的使用方法基本和HashMap一样
- HashTable是线程安全的,HashMap是线程不安全的
2.3.1 HashTable源码分析
public class Table_ {
public static void main(String[] args) {
Hashtable hashtable = new Hashtable();
hashtable.put(1,"zjh");
hashtable.put(2,"jxj");
hashtable.put(1,"wc");
System.out.println(hashtable);
}
}
hashTable的初始化大小为11,加载因子为0.75
从这里既可以看出table数组存放的是Entry,而不是Node。且value不能为空,否则会报NullPointerException。
上图的for循环中就是判断是否需要进行value的覆盖,如果不需要就进入到addEntry方法
rehash()方法是扩容方法
扩容机制:int newCapacity = (oldCapacity << 1) + 1;
protected void rehash() {
// 将老的table的长度赋值给oldCapacity
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// overflow-conscious code
// 获取新的容量,即老的容量向左移位再加1,即oldCapacity *2+1
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
// 这里就是真正的扩容,下面就不继续深究
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
2.4 Properties源码分析
- Properties类继承自HashTable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
- 他的使用特点与Hashtable类似
- Properties还可以用于从 xxx.properties文件中,加载数据到Properties类对象,进行读取并修改
- 说明:xxx.properties文件通常作为配置文件
2.4.1 源码分析
略
2.5 TreeSet的源码分析
当使用无参构造器创建TreeSet的时候,仍然是无序的
public class TreeSet_ {
public static void main(String[] args) {
TreeSet<Object> treeSet = new TreeSet<>();
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("a");
treeSet.add("b");
System.out.println(treeSet);
}
}
如何使得TreeSet是有序的?使用TreeSet提供的构造器,传入一个比较器来实现有序
public class TreeSet_ {
public static void main(String[] args) {
TreeSet<Object> treeSet = new TreeSet<>(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo((String) o2);
}
});
treeSet.add("jack");
treeSet.add("tom");
treeSet.add("a");
treeSet.add("b");
System.out.println(treeSet);
}
}
源码分析
- TreeSet的底层是TreeMap
- 构造器里传入Comparable时,会将Comparable赋值给自己的熟悉comparable
add方法源码解析
进入到map的put方法
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
// 我们的比较器赋值给cpr
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
// 这里进行了比较
cmp = cpr.compare(key, t.key);
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);
}
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;
}
由上图可以看到将comparator赋值给了cpr,之后判断cpr是否为空,不为空则比较两个元素的key的值,因为set的底层时map,key才是真的值,value只是占位的常量
2.6 TreeMap源码分析
public class TreeMap_ {
public static void main(String[] args) {
TreeMap<Object, Object> treeMap = new TreeMap<>();
treeMap.put("jack","杰克");
treeMap.put("tom","汤姆");
treeMap.put("kristina","克瑞斯提莫");
treeMap.put("smith","史密斯");
}
}
如果使用默认的构造器创建treemap
可以发现是无序的
TreeMap的构造器中可以传入Comparable
public class TreeMap_ {
public static void main(String[] args) {
TreeMap<Object, Object> treeMap = new TreeMap<>(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return ((String)o1).compareTo(((String)o2));
}
});
treeMap.put("jack","杰克");
treeMap.put("tom","汤姆");
treeMap.put("kristina","克瑞斯提莫");
treeMap.put("smith","史密斯");
System.out.println(treeMap);
}
}
源码解读
- 构造器,把传入的Comparator传给了TreeMap
- 第一次添加,把k-v封装到entry中,放入到root
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
- 其余的添加和上面的TreeSet解读大致相同,因为TreeSet的底层时TreeMap
Comparator<? super K> cpr = comparator;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
3. Map接口和常用方法
map的体系继承图
- put:添加key-value
- remove:根据key删除映射关系
- get:根据key获取value
- size:获取元素个数
- isEmpty:判断个数是否为0
- clear:清除map
- containsKey:查找key是否存在
3.1 Map接口的6大遍历方法
上述已经讲解过此图,Map的遍历只需要牢牢地记住此图即可。
- containsKey:查找key是否存在
- keySet:获取所有的key,返回的是set集合
- entrySet:获取所有的关系
- values:获取所有的value,返回的是Collections
3.1.1 通过map的keySet()方法
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put(1,"zjh");
map.put(2,"jxj");
map.put(3,"zxw");
map.put(4,"hjx");
map.put(5,"zjy");
// 先获取所有的key,再取出对应的value
Set set = map.keySet();
// 增强for循环
for (Object key : set) {
System.out.println(map.get(key));
}
// 迭代器
Iterator iterator = set.iterator();
while (iterator.hasNext()){
System.out.println(map.get(iterator.next()));
}
}
}
通过map的keySet()方法来获取所有的key,然后一一取出数据。里面可以细分为通过增强for循环和迭代器来遍历keySet集合。
3.1.2 通过map的values()方法
// 获取所有的value,在不关心key的前提下
Collection collection = map.values();
for (Object o : collection) {
System.out.println(o);
}
Iterator iterator = collection.iterator();
while (iterator.hasNext()){
System.out.println(iterator.next());
}
3.1.3 通过map的entrySet()方法
Set entrySet = map.entrySet();
for (Object o : entrySet) {
Map.Entry entry = (Map.Entry) o;
System.out.println(entry.getKey()+"--"+entry.getValue());
}
Iterator iterator = entrySet.iterator();
while (iterator.hasNext()){
Map.Entry entry = (Map.Entry) iterator.next();
System.out.println(entry.getKey()+"--"+entry.getValue());
}
3.2 HashMap小结
- Map接口的常用实现类:HashMap,HashTable,Properties
- HashMap是Map接口使用频率最高的实现类
- HashMap是以key-value的方式来存储数据
- key不能重复,但是value可以重复,key,value允许null值
- 添加相同的key,后者会把前者覆盖(可以打断点进行查阅)
- 与HashSet一样,输出的时候不能保证输出的顺序是添加的顺序,因为底层是以hash表来存储的(jdk8的hashMap底层:数组+链表+红黑树)
- HashMap没有实现同步,因此是线程不安全的