文章目录
-
-
-
- Java集合架构图
- Map接口简介
- Map接口的基本操作
- Map接口直接或者间接的实现类
-
- HashMap
- Hashtable
- LinkedHashMap
- TreeMap
- WeakHashMap
- EnumMap
- IdentifyHashMap
- ConcurrentHashMap
- 四种遍历Map接口的方式
-
-
==比较的是地址值,而不是HashCode,所以这里以后千万不要掉进误区了。!!!
Java集合架构图
点击放大查看

List , Set, Map都是接口,List , Set继承至Collection接口(Collection继承至Iterable),Map为独立接口
Map接口简介
- Map不是collection的子接口或者实现类。Map是一个接口。
- Map 的 每个
都持有两个对象,也就是Entry
,Map 值可以相同,但键肯定是唯一的一个键一个值
- Map 接口最流行的几个实现类是
。HashMap、LinkedHashMap、Hashtable 和 TreeMap
(HashMap、TreeMap最常用)
Map接口的基本操作
方法 | 描述 |
---|---|
int size()获取Map集合大小(即元素数量) | |
boolean isEmpty() | 判断是否为空 |
boolean containsKey(Object key) | 判断是否包含某个键 |
boolean containsValue(Object value) | 判断是否包含某个值 |
V get(Object key) | 获取某个键对应的值 |
V put(K key, V value) | 添加键值对(K,V) |
V remove(Object key) | 移除某个键对应的键值对 |
void putAll(Map<? extends K, ? extends V> m) | 添加另一个Map集合 |
void clear() | 清空所有键值对 |
Set keySet() | 获取键的集合 |
Collection values() | 获取值的集合 |
Set<Map.Entry<K, V>> entrySet() | 获取键值对实体的集合 |
interface Entry<K,V> | Map中的内部接口 |
Map接口直接或者间接的实现类
map集合 | 说明 |
---|---|
HashMap | Map基于 的实现。插入和查询“键值对”的开销是固定的。可以通过构造器设置 和 ,以调整容器的性能。 |
LinkedHashMap | 类似于HashMap,但是迭代遍历它时,取得“键值对”的顺序是其 ,或者是最近最少使用(LRU)的次序。 。而在 时反而更快,因为它 。 |
TreeMap | 底层是 , ,可用于给Map集合中的 。 |
EnumMap | 集合中的所有 必须是 的实例。当EnumMap创建后,会表现成一个数组array,这种表现方式是紧凑高效的。EnumMap的顺序,不是由添加时顺序决定,而是由枚举类内部定义的枚举字段顺序决定。 |
HashTable | HashMap是 ,非线程安全的实现他们都实现了map接口,主要区别是HashMap键值可以为空null,效率可以高于Hashtable。 |
ConcurrentHashMap | ConcurrentHashMap通常只被看做 ,用来 其他线程安全的Map容器,比如Hashtable和Collections.synchronizedMap。 |
WeakHashMap | , : 这是为解决特殊问题设计的。如果没有map之外的引用指向某个“键”,则此“键”可以被垃圾收集器回收 |
IdentifyHashMap | 使用 hash map |
ArrayMap(安卓) | ArrayMap是一个<key,value>映射的数据结构,它设计上更多的是考虑 , ,一个数组 ,另外一个数组 ,它和SparseArray一样,也会对 使用 进行 排序,在添加、删除、查找数据的时候都是 得到相应的 ,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,如果在 的情况下,那么它的性能将退化至少 。 |
SparseArray(安卓) | SparseArray ,在某些条件下性能更好,主要是因为它 了对 (int转为Integer类型),它 则是通过 来进行数据 的,一个 ,另外一个 ,为了优化性能,它内部对数据还采取了 的方式来表示稀疏数组的数据,从而节约内存空间。 |
HashMap
- 底层结构
版本 结构 数组类型 初始化容量 Java1.6/1.7 位桶(数组) + 链表 Entry 16 Java1.8 位桶(数组) + 链表 + 红黑树
)(当链表长度超过阈值 “8” 时,将链表转换为红黑树
Node
1.8前使用位桶和链表实现
1.8中HashMap是以数组+链表+红黑树的模式存储
(当链表长度超过阈值 “8” 时,将链表转换为红黑树)
,当同一个hash值(Table上元素)的链表节点数不小于8时,将不再以单链表的形式存储了,它是
线程不安全
的Map,并允许使用
Null值
和
Null键,
方法上都没有synchronize关键字修饰,具体可以参考HashMap1.7和1.8的区别
Hashtable
- Hashtable是
的一个Map,,它在各个方法上添加了线程安全
。synchronize关键字
,因为现在有了但是现在已经不再推荐使用HashTable了
这个专门用于ConcurrentHashMap
下的map实现类,其大大优化了多线程下的性能。多线程场景
-
和key
不允许为空,线程安全,效率低value
LinkedHashMap
- LinkedHashMap底层数据结构是
,哈希表和链表
保证键哈希表
,唯一
保证链表
键
,根据有序
存储插入顺序
LinkedHashMap<Integer, String> linkedHashMap= new LinkedHashMap<Integer, String>();
linkedHashMap.put(01, "张三1");
linkedHashMap.put(02, "张三2");
linkedHashMap.put(03, "张三3");
linkedHashMap.put(04, "张三4");
linkedHashMap.put(05, "张三5");
Set<Integer> keys = linkedHashMap.keySet();
for (Integer key : keys) {
System.out.println(key + "|" + linkedHashMap.get(key));
}
执行结果:
TreeMap
- 基于
(Red-Black tree)的 NavigableMap 实现。该映射根据其红黑树
进行排序,或者根据创建时提供的键的自然顺序
进行排序Comparator (内比较器)
- 排序方式类似于TreeSet,分为
和自然排序Comparable
,具体排序取决于在构造器中使用使用比较器比较器排序Comparator
默认自然排序(自然顺序,从小到大)
TreeMap<Integer, String> treeMap= new TreeMap<Integer, String>();
treeMap.put(24, "Hello1");
treeMap.put(14, "Hello2");
treeMap.put(34, "Hello3");
treeMap.put(124, "Hello4");
treeMap.put(24, "Hello5");
treeMap.put(24, "Hello6");
treeMap.put(24, "Hello7");
treeMap.put(244, "Hello8");
treeMap.put(624, "Hello9");
treeMap.put(24, "Hello10");
Set<Integer> keys = treeMap.keySet();
for (Integer key : keys) {
String value = treeMap.get(key);
System.out.println(key + "|" + value);
}
执行结果
比较器排序(使用Comparatpr倒序)
TreeMap<Integer, String> treeMap = new TreeMap<Integer, String>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
treeMap.put(24, "Hello1");
treeMap.put(14, "Hello2");
treeMap.put(34, "Hello3");
treeMap.put(124, "Hello4");
treeMap.put(24, "Hello5");
treeMap.put(24, "Hello6");
treeMap.put(24, "Hello7");
treeMap.put(244, "Hello8");
treeMap.put(624, "Hello9");
treeMap.put(24, "Hello10");
Set<Integer> keys = treeMap.keySet();
for (Integer key : keys) {
String value = treeMap.get(key);
System.out.println(key + "|" + value);
}
执行结果
- TreeMap是NavigableMap接口实现类,NavigableMap接口继承了SortedMap(继承了Map)即一个支持排序的map,所以treemap才会支持排序
Treemap四个构造方法
- 无参 默认实现key的自然排序
- 有参 参数为comparator比较器 实现定制排序
- 有参 参数为map 实现key的自然排序
- 有参 参数为map,comparator比较器 实现key的定制排序
自然排序: 要求待添加元素必须实现compareable接口并实现compareTo方法
自定义排序: 要求待添加元素无序实现compareable接口,但创建Treemap对象时必须传入comparator的比较器,并实现compare方法(匿名内部类)
WeakHashMap
- WeakHashMap 以
实现的,它是弱键
都可以是null,的非同步且无序的散列表(hash哈希表)Key和Value
- Map中如果这个Key值指向的对象没被使用
,此时触发了GC,该对象就会被回收掉的。(除了自身有对key的引用外,此key没有其他引用那么此map会自动丢弃此值,然后被GC回收 )
- HashMap的key保留了对实际对象的强引用,这意味着只要HashMap对象不被销毁,还HashMap的所有key所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些key所对应的key-value对;
- WeakHashMap的key只保留对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,WeakHashMap也可能自动删除这些key所对应的key-value对。
- WeakHashMap中的每个key对象只持有对实际对象的弱引用,因此,当垃圾回收了该key所对应的实际对象之后,WeakHashMap会自动删除该key对应的key-value对。
- 原理主要是使用的
和WeakReference
实现的,其key就是ReferenceQueue
,而weakReference
中保存了被回收的 Key-Value。ReferenceQueue
- 如果当其中一个
不再使用被回收时,就将其加入Key-Value
中。当下次再次调用该ReferenceQueue队列
时,就会去更新该map,比如WeakHashMap
,将其中包含的key-value全部删除掉。这就是所谓的ReferenceQueue中的key-value
。“自动删除”
HashMap和WeakHashMap的区别也在于此,HashMap的key是对实际对象的
强引用
。
- 弱引用(WeakReference)的特性是:当gc线程发现某个对象只有弱引用指向它,那么就会将其销毁并回收内存。WeakReference也会被加入到引用队列queue中。
WeakHashMap<String,String> whm = new WeakHashMap<>();
whm.put(new String("hello1"), "world1");
whm.put(new String("hello2"), "world2");
whm.put(new String("hello3"), "world3");
whm.put("hello4", "world3");
System.out.println(whm);
// System.gc()来通知JVM进行垃圾回收
System.gc();
System.runFinalization();
System.out.println(whm);
执行结果
EnumMap
- EnumMap,该类是专门
的一个集合类。创建EnumMap是必须针对枚举类设计
,从而将该EnumMap和指定枚举类关联起来。指定一个枚举类
- 该Map在内部以
的形式保存,所以这种实现形式非常紧凑、高效数组
- EnumMap不允许key为空,value可以为空
- EnumMap的顺序,不是由添加时顺序决定,而是由枚举类内部定义的
决定。枚举字段顺序
- 线程不安全,最好在创建的时候调用
方法来进行同步。Collections.synchronizedMap
class TestEnumMap {
public static void main(String[] args) {
//在创建EnumMap时必须显示或隐式指定它对应的枚举类
EnumMap<Direction, String> enumMap = new EnumMap<>(Direction.class);
// 所有的key都必须是单个枚举类的枚举值
enumMap.put(Direction.UP, "向上移动");
enumMap.put(Direction.DOWN, "向下移动");
enumMap.put(Direction.LEFT, "向左移动");
enumMap.put(Direction.RIGHT, "向右移动");
//EnumMap根据key的自然顺序(枚举值在枚举类的定义顺序)来维护key-value对的顺序
for (Map.Entry<Direction, String> entry : enumMap.entrySet())
System.out.println(entry.getKey() + "-----" + entry.getValue());
}
/**
* 内部枚举类
*/
enum Direction {
UP,
LEFT,
DOWN,
RIGHT;
}
}
执行结果:
插入EnumMap的顺序是UP,DOWN,LEFT,RIGHT 但是实际打印顺序是 UP,LEFT,DOWN,RIGHT,可以看出 EnumMap的顺序是不是由添加时顺序决定, 而是有枚举类中定义的字段顺序决定
IdentifyHashMap
IdentityHashMap不是Map的通用实现,它有意违反了Map的常规协定。是一个可以
添加重复key
的Map实现类, 并且
key/value都允许为空
,且是
线程不安全
的
- IdentityHashMap 和 HashMap区别
- IdentifyHashMap和HashMap差不多,唯一的区别就是在
中底层
判断key的方式不一样
- HashMap类判断键k1和k2相等的条件为
: 先判断hashCode是否相同,如果hashCode相同,在用equals判断值是否相同p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))
- IdentifyHashMap判断k1和k2相等的条件是
:判断内存地址是否相等(item == k)
(上面红色均为HashMap,IdentityHashMap部分源码)
- IdentifyHashMap和HashMap差不多,唯一的区别就是在
- 对比HashMap和IdentityHashMap
添加
元素区别
HashMap(java.8)
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
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);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
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;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
可以得出结论
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
1. 通过新key的hashCode()方法,计算出哈希码,然后从Node数组中找到对应的位置,若为null就放进去。若已经有值了,请看第二步
2. 调用新key的equals()方法去和已经存在的key比较,如果返回ture 。则视新键与已经存在的键相同,用新值去更新旧值,然后put方法返回旧值
3. 若调用equals()返回false,则认为新键和已存在的键不一样,那就会新建一个Node节点,放在此链表里
HashMap的put()方法返回null的特殊情况:
1. 要是已经存在键的映射,但是值是null,那么调用put()方法再更新键的值时, put()方法会把旧值null返回(因为旧值为null,所以很特殊)
2. 要是找到的位置上没有键的映射,put()方法也是返回null
IdentityHashMap
public V put(K key, V value) {
final Object k = maskNull(key);
retryAfterResize: for (;;) {
final Object[] tab = table;
final int len = tab.length;
int i = hash(k, len);
for (Object item; (item = tab[i]) != null;
i = nextKeyIndex(i, len)) {
if (item == k) {
@SuppressWarnings("unchecked")
V oldValue = (V) tab[i + 1];
tab[i + 1] = value;
return oldValue;
}
}
final int s = size + 1;
// Use optimized form of 3 * s.
// Next capacity is len, 2 * current capacity.
if (s + (s << 1) > len && resize(len))
continue retryAfterResize;
modCount++;
tab[i] = k;
tab[i + 1] = value;
size = s;
return null;
}
}
可以得出结论
if (item == k) {}
IdentityHashMap比较key值,直接使用的是==,只要两个对象的内存地址相同即会覆盖旧的key/value
示例代码
测试对象Demo,用于给HashMap以及IdentityHashMap做key
public class Demo {
private Integer num;
public Demo(Integer num) {
this.num = num;
}
/**
* 重写了equals方法,只要值相同就可以认为是同一个对象
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (obj instanceof Demo) {
Demo demo = (Demo) obj;
if (num.equals(demo.num)) {
return true;
}
}
return false;
}
/**
* 重写hashCode方法,返回当前num值
* @return
*/
@Override
public int hashCode() {
return num;
}
}
测试HashMap 添加元素
@Test
public void testHashMap() {
// HashMap put方法去重
//通过新key的hashCode()方法,计算出哈希码,然后从Node数组中找到对应的位置,若为null就放进去。若已经有值了,请看第二步
//调用新key的equals()方法去和已经存在的key比较,如果返回ture 。则视新键与已经存在的键相同,用新值去更新旧值,然后put方法返回旧值
//调用equals()返回false,则认为新键和已存在的键不一样,那就会新建一个Node节点,放在此链表里
Map<String, String> testMap1 = new HashMap<>();
testMap1.put("a", "1");
testMap1.put("a", "2");
testMap1.put("a", "3");
System.out.println(testMap1.size()); //长度1
Map<String, String> testMap2 = new HashMap<>();
//String 底层重写了hashCode/equals方法,所有值相同的对象都会返回相同的HashCode并且equals返回true
testMap2.put(new String("a"), "1");
testMap2.put(new String("a"), "2");
testMap2.put(new String("a"), "3");
System.out.println(testMap2.size()); //长度1
// Integer 底层重写了hashCode/equals方法,所有值相同的对象都会返回相同的HashCode并且equals返回true
Map<Integer, String> testMap3 = new HashMap<>();
testMap3.put(new Integer(200), "1");
testMap3.put(new Integer(200), "2");
testMap3.put(new Integer(200), "3");
System.out.println(testMap3.size()); //长度1
//Demo类 重写了hashCode/equals方法,所有值相同的对象都会返回相同的HashCode并且equals返回true
Demo demo1 = new Demo(1);
Demo demo2 = new Demo(1);
Demo demo3 = new Demo(1);
Map<Demo, String> testMap4 = new HashMap<>();
testMap4.put(demo1, "1");
testMap4.put(demo2, "2");
testMap4.put(demo3, "3");
System.out.println(testMap4.size()); //长度1
}
执行结果
测试IdentityHashMap 添加元素
@Test
public void testIdentityHashMap() {
// IdentityHashMap put方法去重 使用 == 判断两个key使用相同, 如果内存地址相同覆盖旧key/value
// 常量 "a" 第一次使用时如果常量池中没有则会添加一个,后续如果在使用会直接使用常量池中已有的变量,所以内存地址是指向同一处地方
Map<String, String> testMap1 = new IdentityHashMap<>();
testMap1.put("a", "1");
testMap1.put("a", "2");
testMap1.put("a", "3");
System.out.println(testMap1.size()); //长度1
// 使用 new String() 会在堆中创建一个对象,然后如果常量池没有会在常量池中创建 "a" ,因此内存地址是指向不是同一处地方
Map<String, String> testMap2 = new IdentityHashMap<>();
testMap2.put(new String("a"), "1");
testMap2.put(new String("a"), "2");
testMap2.put(new String("a"), "3");
System.out.println(testMap2.size()); //3
//,new Integer()会直接在堆中创建对象,并将引用赋值给栈中,因此内存地址是指向不是同一处地方
Map<Integer, String> hashMap2 = new IdentityHashMap<>();
hashMap2.put(new Integer(200), "1");
hashMap2.put(new Integer(200), "2");
hashMap2.put(new Integer(200), "3");
System.out.println(hashMap2.size()); //长度3
// IdentityHashMap底层使用 == 判断新key与旧key内存地址是否一致,只有内存地址一致才会覆盖旧key/value
Demo demo1 = new Demo(1);
Demo demo2 = new Demo(1);
Demo demo3 = new Demo(1);
Map<Demo, String> testMap3 = new IdentityHashMap<>();
testMap3.put(demo1, "1");
testMap3.put(demo1, "1");
testMap3.put(demo2, "2");
testMap3.put(demo2, "2");
testMap3.put(demo3, "3");
testMap3.put(demo3, "3");
System.out.println(testMap3.size()); //长度3
}
执行结果:
注意:
- IdentityHashMap重写了equals和hashcode方法,不过需要注意的是hashCode方法并不是借助Object的hashCode来实现的,而是通过
方法来实现的。System.identityHashCode
- 该类不是线程安全的,如果要使之线程安全,可以调用
方法来实现。Collections.synchronizedMap(new IdentityHashMap(…))
ConcurrentHashMap
- Hashtable之所以效率低下主要是因为其实现使用了
关键字对synchronized
等操作进行加锁,而synchronized关键字加锁是对put
进行加锁,也就是说在进行put等修改Hash表的操作时,整个对象
,从而使得其表现的效率低下;因此,在锁住了整个Hash表
版本,Java使用了Java1.5~1.7
机制实现ConcurrentHashMap.分段锁
- 简而言之,ConcurrentHashMap在对象中保存了一个
,默认长度为16。即将整个Segment数组
划分为Hash表
;而多个分段
Segment元素,即每个分段则类似于一个每个
;这样,在执行put操作时首先根据Hashtable
到元素hash算法定位
哪个属于
,然后Segment
即可。因此,ConcurrentHashMap在多线程并发编程中可是实现多线程put操作。对该Segment加锁
(segment有多少个,理论上就可以同时有多少个线程来持有它这个资源。)
- 在JDK1.7之前,ConcurrentHashMap是通过分段锁机制来实现的,所以其最大并发度受
。因此,在JDK1.8中,ConcurrentHashMap取消了Segment的个数限制
,底层采用与HashMap类似的数组+链表+红黑树的方式实现,而加锁则采用CAS和synchronized实现。Segment分段锁字段
Value和next(这里注意Node其实就是保存一个键值对的最基本的对象。其中
volatile都是使用的
关键字进行了修饰,以确保线程安全。)
四种遍历Map接口的方式
- Entry
- 由于Map中存放的元素均为
,故每一个键值对必然存在一个映射关系。键值对
- Map中采用
来表示一个键值对,键值对包含Key和Value (我们总说键值对键值对, 每一个键值对就是一个Entry内部类
)Entry
- Map.Entry里面包含getKey()和getValue()方法
- 由于Map中存放的元素均为
Iterator<Map.Entry<Integer, Integer>> it=map.entrySet().iterator();
while(it.hasNext()) {
Map.Entry<Integer,Integer> entry=it.next();
int key=entry.getKey();
int value=entry.getValue();
System.out.println(key+" "+value);
}
- entrySet
- entrySet是 java中 键-值 对的集合,Set里面的类型是Map.Entry,一般可以通过map.entrySet()得到。
- entrySet实现了Set接口,里面存放的是键值对。一个K对应一个V。
Set<Map.Entry<String, String>> entryseSet=map.entrySet();
for (Map.Entry<String, String> entry:entryseSet) {
System.out.println(entry.getKey()+","+entry.getValue());
}
- keySet
- keySet是键的集合,Set里面的类型即key的类型
Set<String> set = map.keySet();
for (String s:set) {
System.out.println(s+","+map.get(s));
}
- 四种遍历Map方式对比
public static void main(String[] args) {
Map<String, String> map = new HashMap<String, String>();
map.put("1", "value1");
map.put("2", "value2");
map.put("3", "value3");
//第一种:键找值方式遍历,普遍使用,二次取值
System.out.println("通过Map.keySet遍历key和value:");
for (String key : map.keySet()) {
System.out.println("key= "+ key + " and value= " + map.get(key));
}
//第二种:遍历entrySet迭代器遍历方式
System.out.println("通过Map.entrySet使用iterator遍历key和value:");
Iterator<Map.Entry<String, String>> it = map.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = it.next();
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第三种:遍历entrySet方式,推荐,尤其是容量大时
System.out.println("通过Map.entrySet遍历key和value");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println("key= " + entry.getKey() + " and value= " + entry.getValue());
}
//第四种:keySet迭代器遍历
Iterator<String> iterator=map.keySet().iterator();
System.out.println("通过Map.keySet使用iterator遍历key和value:");
while (iterator.hasNext()) {
String key=iterator.next();
String value=map.get(key);
System.out.println("key: " + key +",value: "+value);
}
}
推荐使用第三种方式遍历map集合