集合:Map
Map是什么
特殊的集合接口
Map的特点
-
Map中的元素被称为键值对(key-value),一个key对应一个value,例如电话簿中每一个名字对应一个电话号码(key不可重复,value可重复)
- key:Set组成
- value:List组成
-
底层实现是散列表,即数组+链表(List+Set)+[树]
Map的实现类和子接口
- HashMap:线程不安全,无序,key不可重复(不可重复的原理与HashSet相同)
- Hashtable:线程安全,类似StringBuffer、Vector
- SortedMap
- TreeMap:可排序(原理与TreeSet相同),对key进行排序
- LinkedHashMap
TreeMap排序时,若选择传入构造器Comparator,则构造器的泛型类型必须与key的泛型类型一致
Map的迭代
- Map不能使用迭代器(没有实现Iterable接口)
- **!!!**遍历key:Set keySet()
package demo;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Demo1<E> {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("1","a");
map.put("2","b");
map.put("3","c");
map.put("4","d");
//返回key的set集合
Set<String> keys = map.keySet();
for(String set : keys) {
System.out.println(set+"="+map.get(set));
}
}
}
- 遍历value:Collection values()
package demo;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class Demo1<E> {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("1","a");
map.put("2","b");
map.put("3","c");
map.put("4","d");
Collection<String> values = map.values();
for(String str : values) {
System.out.println(str);
}
}
}
无法通过value得到key,所以这种方式的遍历只能获得Map的value
- **!!!**遍历key-value:Set< Entry<k,v> > entrySet()
package demo;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class Demo1<E> {
public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("1","a");
map.put("2","b");
map.put("3","c");
map.put("4","d");
Set<Entry<String,String>> entrys = map.entrySet();
for(Entry<String,String> entry : entrys) {
System.out.println(entry);
}
}
}
Entry<k,v>是Map的一个内部类
Map的API
- put(key,value):添加新的键值对(hashCode()根据key计算位置)
- 可能会出现的两种情况:
- key不同,hashCode()的计算结果相同:链表方式存储,原来的元素向后推
- key值相同:新的键值对覆盖原来的键值对
- 可能会出现的两种情况:
注意1(重要):key和value可以为null
注意2:HashSet:新元素不会替换旧元素
- get(key):通过key获得对应的value
- remove(key):通过key删除整个键值对,返回被删除的value
- 如果查询不到键值对,返回null
- contains…(…):
- containsKey(key)
- containsValue(value)
- isEmpty()
- putAll(Map map)
HashMap
HashMap的底层实现
数组+单向链表+红黑树
- 当链表中的数量超过8个时,链表转换为红黑树,所以如果哈希冲突(不同元素的hashCode()得到相同的结果)多的话,数组的元素将会是红黑树
HashMap的长度与扩容方式
- HashMap的初始长度:16
- HashMap的加载因子:0.75
- 加载因子:当Map中的键值对数量达到长度的0.75时,Map自动扩容(Hashtable相同)
- HashMap的扩容方式:old*2
- 例如,Map的初始长度为16,当Map中的键值对个数达到12个时,Map的长度自动扩容为32
选择加载因子为0.75的原因:
- HashMap扩容时,键值对的位置都要进行重新计算
- 新插入的键值对的位置也要进行计算,当剩余空间太小时,计算所需时间会变长
综合考虑以上两点,当加载因子为0.75时,效率比较高
Hashtable的初始长度:11
Hashtable的加载因子:0.75
Hashtable的扩容方式:old*2+1
Set的初始长度、加载因子、扩容方式都与Map相同
LinkedHashMap
LinkedHashMap是什么
LinkedList与HashMap的结合,底层仍然是HashMap
LinkedHashMap的实现
双向链表+散列表
LinkedHashMap的特点
- key不可重复
- 有序(有序指元素的顺序与添加的先后顺序一致,并非是排序)
- 节点多了before和after属性(对应LinkedList的provious和next)
Map实现类的对比
HashMap | Hashtable | LinkedHashMap | TreeMap | |
---|---|---|---|---|
元素排序 | 无序 | 无序 | 加入顺序 | 字典顺序 |
元素 | key和value可以为null | 不接受null | key和value可以为null | 仅接受value为null |
底层实现 | 数组+单向链表+红黑树 | 数组+链表 | 散列表+双向链表 | 红黑树 |
线程安全 | 非synchronized | synchronized | 非synchronized | 非synchronized |
线程同步 | 不同步 | 同步 | 不同步 | 不同步 |
效率 | 高 | 低 | ||
默认长度 | 16 | 11 | ||
加载因子 | 0.75 | 0.75 | ||
扩容方式 | old*2 | old*2+1 |
扩展:ConcurrentHashMap
出现原因
HashMap线程不安全,Hashtable性能不好
底层实现
由16个segment组成,每个sgment都是线程安全的HashMap(锁分段技术)
- 将数据分成一段一段(segment)地存储,然后给每个数据段配一把锁,当一个线程占用锁访问其中某个数据段时,其它数据段的数据也能被其它线程访问
上述实现是基于JDK1.7,在JDK1.8中ConcurrentHashMap完全是在HashMap的基础上进行加锁
特点
- 对segment的改写操作,不影响其它segment的使用
- 理想情况下,最多支持6个线程并发修改segment,这16个线程分别访问不同的segment
- segment加锁时,所以读线程是不会受到阻塞的
使用场景
高并发
方法的实现原理
- get():不加锁,先定位到segment,然后再找到头结点进行读取操作。因为value是volatile变量,所以可以保证在竞争条件时读取到最新的值,如果读到的value是null,则数据可能正在被修改,那么就调用方法ReadValueUnderLock(),加锁保证读到的数据是正确的。
- Put():加锁,一律添加到hash链的头部
- Remove():加锁,由于next是final类型不可改变,所以必须把删除的节点之前的节点都复制一遍。