1. Map集合
Map集合提供的是一种key-value键值对元素的存储容器,key值不允许重复,重复的key值会导致元素覆盖!
Map接口提供的能力:
public interface Map<K,V> {
/*判断map容器是否为空*/
boolean isEmpty();
/*判断key是否存在map中*/
boolean containsKey(Object key);
/*判断value是否存在map中*/
boolean containsValue(Object value);
/*通过key获取value*/
V get(Object key);
/*将key-value键值对更新到map中*/
V put(K key, V value);
/*将对应key的键值对从map中移除*/
V remove(Object key);
/*将Map m的内容全部更新到当前map中去*/
void putAll(Map<? extends K, ? extends V> m);
/*清空map*/
void clear();
/*获取当前map中所有key值的set集合*/
Set<K> keySet();
/*获取当前map中所有value值的集合,可以重复*/
Collection<V> values();
/*获取当前map中entry的set集合*/
Set<Map.Entry<K, V>> entrySet();
/*判断两个map集合是否相等*/
boolean equals(Object o);
/*返回map集合的hashCode*/
int hashCode();
/*Java8,Map接口中新增的default方法*/
/*默认值为defaultValue的get方法*/
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
/*指定action函数式接口的foreach*/
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
/*通过函数式接口function获取需要替换到Map中value值*/
default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Objects.requireNonNull(function);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
// ise thrown from function is not a cme.
v = function.apply(k, v);
try {
entry.setValue(v);
} catch(IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
}
}
/*key值无效或者value为null,put成功*/
default V putIfAbsent(K key, V value) {
V v = get(key);
if (v == null) {
v = put(key, value);
}
return v;
}
/*key,value都匹配删除成功,否则进行remove行为*/
default boolean remove(Object key, Object value) {
Object curValue = get(key);
if (!Objects.equals(curValue, value) ||
(curValue == null && !containsKey(key))) {
return false;
}
remove(key);
return true;
}
/*key,value都匹配成功的时候,进行value值的替换*/
default boolean replace(K key, V oldValue, V newValue) {
Object curValue = get(key);
if (!Objects.equals(curValue, oldValue) ||
(curValue == null && !containsKey(key))) {
return false;
}
put(key, newValue);
return true;
}
/*key值有效的时候,进行替换,如果是新增key值,不会新建键值对*/
default V replace(K key, V value) {
V curValue;
if (((curValue = get(key)) != null) || containsKey(key)) {
curValue = put(key, value);
}
return curValue;
}
/*见compute*/
default V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
Objects.requireNonNull(mappingFunction);
V v;
if ((v = get(key)) == null) {
V newValue;
if ((newValue = mappingFunction.apply(key)) != null) {
put(key, newValue);
return newValue;
}
}
return v;
}
/*见compute*/
default V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue;
if ((oldValue = get(key)) != null) {
V newValue = remappingFunction.apply(key, oldValue);
if (newValue != null) {
put(key, newValue);
return newValue;
} else {
remove(key);
return null;
}
} else {
return null;
}
}
/*通过remappingFunction函数式接口对value进行二次计算,然后再进一步操作键值对*/
default V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
V oldValue = get(key);
V newValue = remappingFunction.apply(key, oldValue);
if (newValue == null) {
// delete mapping
if (oldValue != null || containsKey(key)) {
// something to remove
remove(key);
return null;
} else {
// nothing to do. Leave things as they were.
return null;
}
} else {
// add or replace old mapping
put(key, newValue);
return newValue;
}
}
/*通过remappingFunction函数式接口,对value进行二次计算,再put*/
default V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
Objects.requireNonNull(remappingFunction);
Objects.requireNonNull(value);
V oldValue = get(key);
V newValue = (oldValue == null) ? value :
remappingFunction.apply(oldValue, value);
if(newValue == null) {
remove(key);
} else {
put(key, newValue);
}
return newValue;
}
}
Java8在Map接口中新增的default方法:
Map接口的遍历:
public class MapTest
{
public static void main(String args[])
{
Map<String,String> mp = new HashMap<>();
mp.put("key1","value1");
mp.put("key2","value2");
mp.put("key3","value3");
/*通过Map的keySet遍历*/
for (String s : mp.keySet())
{
System.out.println("key:" + s + " value:" + mp.get(s));
}
/*通过Map的values遍历*/
for (String s : mp.values())
{
System.out.println("value:" + s);
}
/*通过Map的entrySet遍历*/
for (Map.Entry<String,String> entry : mp.entrySet())
{
System.out.println("key:" + entry.getKey() + " value:" + entry.getValue());
}
/*通过Map接口的forEach默认方法*/
mp.forEach((key,value)->{
System.out.println("key:" + key + " value:" +value);
});
}
}
2. HashMap集合:
2.1 HashMap集合简述:
- HashMap是继承自AbstractMap抽象类,AbstractMap抽象类继承自Map接口;
- HashMap是线程不安全集合;
- HashMap可以接收null元素,key值,value值均可以为null;
- HashMap是一个无序的集合;
2.2 HashMap的实现原理:
2.2.1 HashMap的原则:
一个key对应一个唯一的value;当key重复的时候,覆盖原有的value;HashMap的key的唯一性取决于HashCode值相等并且equals返回true!
2.2.2 HashMap的默认配置:
/*Map容量,默认值为16,会被自动转换为最近的2的N次幂*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
/*Map容器最大容量*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/*当达到容量的75%的时候,启动容器扩容,一次扩一倍*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/*Java8后,link长度达到8的时候,满足链表转红黑树条件1*/
static final int TREEIFY_THRESHOLD = 8;
/*Java8后,link长度从8降到6的时候,满足红黑树转换链表条件*/
static final int UNTREEIFY_THRESHOLD = 6;
/*Java8后,当容器达到64的时候,满足链表转红黑树条件2*/
static final int MIN_TREEIFY_CAPACITY = 64;
2.2.3 HashMap的存储形式:
- Java8以前,HashMap的存储结构是哈希+链表,将元素key值的hashcode转换为哈希桶的索引;当未发生哈希碰撞的时候,元素直接入桶,当发生哈希碰撞的时候(即哈希桶索引相同、hashcode可能相同,也可能不相同),冲突元素遍历哈希桶中的链表,如果发现hashcode相等且equals相等的元素直接覆盖,未发现hashcode相等同时equals也相等的元素,将元素插入哈希桶中的链表!查找元素的时间复杂度为O(1)+O(n),如果哈希碰撞严重的情况时间复杂度就接近O(n)了,性能不优!所以有了后面的演进!
- Java8以后,HashMap的存储结构是哈希+链表&红黑树,入桶规则和Java8以前相同,但是当Map容器达到64,哈希桶中链表元素超过8的时候,会自动将链表转换为红黑树,这样当哈希碰撞严重的时候,查找元素的时间复杂度就接近O(nlogn),比O(n)更优!
Ps:注意HashMap中的哈希碰撞有两种形式:
- 一种是hashcode不相等,但是转换到哈希桶索引的时候,索引相等!这种碰撞由于hashcode不相同,红黑树是接近对称的,查找元素的复杂度接近O(nlogn)!
- 另一种是hashcode相等时候发生的碰撞,这种碰撞会导致红黑树在遍历相等hashcode的时候更接近一个链表的遍历,因为相等hashcode的元素会一直在左树,这样查找元素的时间复杂度又接近O(n)了!但是可以让key值支持Comparable接口,支持相同hashcode但是equals不相等的场景的比较,这样在hashcode相等equals不相等的时候,HashMap会自动根据key对象提供的Comparable能力确认节点在红黑树的左边还是右边!这样查找元素的时间复杂度又提升到了O(nlogn)了!
2.2.4 hashcode()方法和equals()方法重写的注意事项:
- HashMap实现通过key值对象的hashcode和equals来确认key值的唯一性,两者均相等的时候,确认为一个唯一key!
- hashcode相等,一定会发生哈希碰撞;hashcode不相等,也可能会发生哈希碰撞,只是会降低碰撞的概率!碰撞越多,查询时,性能越差,所以不同的key值,最好使用不同的hashcode,来降低碰撞概率!
- 当业务形态中,认为两个key值对象equals相等就是相同key的时候;建议重写equals()的同时一定要同步重写hashcode(),并且最好equals不相等,hashcode也不相等;equals相等,hashcode也相等,这样既满足设计也能保证性能最优!
简单分析原因(前提条件:当业务形态中,认为两个key值对象equals相等就是相同key的时候):
- equals相等,hashcode相等:由于equals、hashcode都相等,HashMap会用后来的key元素覆盖,原有的key元素!(元素位置:相同哈希桶的相同hashcode位置)
- equals相等,hashcode不相等:业务侧认为equals相等的,key就是相同的;但是由于hashcode不相等,HashMap会认为是两个key,分别都存放在容器里面,不符合设计预期!(元素位置:在不同的哈希桶或者相同哈希桶的不同hashcode位置)
- equals不相等,hashcode相等:由于hashcode相等,equals不相等,在发生哈希碰撞后,哈希桶中的红黑树会存在相同hashcode不同equals的节点;再获取数据的时候,需要遍历这些相同hashcode不同equals的节点,由于在key不支持Comparable接口的时候,左树右树的选择是通过hashcode判断的,导致相同hashcode会一直查找左树;如果这样的节点数目增多,红黑树的查找复杂度就接近链表查找的复杂度O(n)了,导致查询效率降低!(元素位置,在相同哈希桶的不同hashcode位置)
- equals不相等,hashcode不相等:由于hashcode不相等,哈希桶中的红黑树元素可以通过hashcode直接区分,不存在性能降低的问题,推荐使用!(元素位置,在不同的哈希桶或者相同哈希桶的不同hashcode位置)
2.2.5 HashMap扩容问题:
由于这里不想对HashMap的resize()进行展开描述,仅仅记录下Java8以前和Java8以后的实现差异;
- Java8以前,在数据转移的时候,使用头插法,同时并发调用put()方法触发扩容,可能会造成逆序环形链表的问题,造成死链!
- Java8以后,将头插法改为了尾插法,不会再出现逆序环形链表的问题,不会造成死链!
2.2.6 key值的不可变性:
HashMap中的元素位置主要是依赖于key值对象的hashcode和equals进行判断选择的,所有针对key值对象的选择,尽可能选择不可变对象;如果选择可变对象,必须保证在元素进入HashMap后,对象不再改变!因为如果key值对象改变导致hashcode和equals改变了,就会造成通过key值无法获取到正确元素的情况!
2.2.7 HashMap线程安全性问题讨论:
HashMap是个线程不安全的集合,多线程同时操作可能会造成数据异常;如链表与红黑树相互转换的时候并发、HashMap扩容的时候并发、链表插入读取的时候并发、红黑树插入读取的时候并发等场景;
如果想让HashMap支持线程安全,可以使用Collections.synchronizedMap(HashMap)进行包装!
2.2.8 HashXXX集合中contains()方法、equals()方法的特殊性:
由于HashXXX这类集合的特殊性:通过使用hashcode确定元素位置的方式来提高元素查询的效率!导致key相等的判断不仅仅通过equals方法判断,同时需要hashcode相等!这样的话HashSetXXX的contains()、HashMapXXX的containsKey()、HashXXX的equals()等涉及到key值判断的方法,都不再像其他集合那样仅仅判断eqauls就可以了,还需要同步判断hashcode!
2.2.9 hashcode重写建议:
原则:equals相等的hashcode必须相等,equals不相等的hashcode尽可能不相等!
- 将对象中每个有意义的变量计算出一个int类型的hashCode值
- 对每个hashCode值乘上一个不同的质数,最后求和
Ps:由于这里针对HashMap讨论比较简单,并未深入源码层面,推荐两篇比较详细的博文<HashMap看这篇就够了> <一文搞定HashMap的实现原理和面试>
3. LinkedHashMap集合:
3.1 LinkedHashMap集合简述:
LinkedHashMap是HashMap的一个子类,是一个有序的HashMap(可以根据插入顺序,遍历Map);LinkedHashMap通过重写了HashMap中的newNode()方法,每次调用put插入数据的时候,都会同时将插入顺序记录到一个链表中;当需要遍历Map的时候,通过链表中的顺序进行遍历,来实现Map的有序性!
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
4. Hashtable集合:
4.1 Hashtable集合简述:
- Hashtable继承自Map接口和Dictionary抽象类
- Hashtable是线程安全集合;
- Hashtable不可以接收null元素,key值,value值都不可以为null,否则会报空指针异常;
- Hashtable是一个无序的集合;
4.2 Hashtable的实现:
- 存储形式:哈希+链表,细节实现与Java8以前的HashMap类似
- 同步方式:通过synchronized同步方法,实现线程安全
4.3 Hashtable与HashMap比较:
- HashMap线程不安全,Hashtable线程安全
- HashMap可以接收null元素,Hashtable不可以接收null元素
- HashMap中引入红黑树提升查询性能,Hashtable依然使用链表存储哈希桶中的数据
- HashMap与Hashtable都是无序的Map集合
Ps:由此可以看出,HashMap更加优选于Hashtable(Hashtable的查询效率很低、同步效率也很低、目前处于半废弃状态),如果需要线程安全的HashMap,可以使用Collections.synchronizedMap()进行包装或者使用ConcurrentHashMap集合!
5. TreeMap集合:
5.1 TreeMap集合简述:
- TreeMap继承自Map接口和SortedMap抽象类
- TreeMap是线程不安全集合;
- TreeMap的key值不可以为null,value值可以为null;
- TreeMap是一个有序的集合,顺序为key元素排列升序;
5.2 TreeMap的实现:
- TreeMap不同于XXXHashMap,不是通过Hash桶实现的Key值的存在;TreeMap是一个有序Map,存储结构为红黑树;
- TreeMap元素红黑树的确认规则:如果TreeMap选择自然排序(即构造函数中未设置Comparator对象),则通过每个key元素的compareTo方法进行元素大小关系判断,进而确认元素位置,所以在自然排序的TreeMap每个key元素必须是Comparable接口的实现类!如果TreeMap选择定制排序,则可以通过自己对Comparator的实现,来决定两个key元素判断标准,如果不再通过key元素的compareTo方法进行判断,key元素可以不再是Comparable的实现类!
- TreeMap的key元素相等判断规则:仅仅根据元素的compareTo方法或者Comparator实现来判断,不使用equals进行判断;但是为了保证逻辑的一致性,建议compareTo方法或者Comparator实现逻辑与equals逻辑一致!当compareTo方法返回0的时候,判断为两个key元素相等,与HashMap不同的是,此时仅仅更新value的内容,key元素不再刷新!
public class TreeMapTest
{
public static void main(String args[])
{
/*自然排序*/
TreeMap<TmKey,String> tmap = new TreeMap<>();
TmKey key1 = new TmKey(100);
TmKey key2 = new TmKey(200);
TmKey key3 = new TmKey(300);
tmap.put(key2, "key2");
tmap.put(key1, "key1");
tmap.put(key3, "key3");
System.out.println(tmap.size());
for (Map.Entry<TmKey, String> entry : tmap.entrySet())
{
System.out.println(entry.getKey().num);
System.out.println(entry.getValue());
}
}
}
class TmKey implements Comparable<Object>
{
public int num;
public TmKey(int num)
{
this.num = num;
}
@Override
public int compareTo(Object obj) {
return 0;
}
@Override
public boolean equals(Object obj) {
return true;
}
}
输出:
Ps:Comparable接口中obj1.compareTo(obj2)方法,返回正整数表示obj1大于obj2;返回负整数表示obj1小于obj2;返回0表示obj1和obj2相等!
5.3 TreeMap的使用:
public class TreeMapTest
{
public static void main(String args[])
{
/*自然排序*/
TreeMap<TmKey,String> tmap = new TreeMap<>();
TmKey key1 = new TmKey(100);
TmKey key2 = new TmKey(200);
TmKey key3 = new TmKey(300);
tmap.put(key2, null);
tmap.put(key1, null);
tmap.put(key3, null);
System.out.println(tmap.size());
for (Map.Entry<TmKey, String> entry : tmap.entrySet())
{
System.out.println(entry.getKey().num);
}
/*定制排序*/
TreeMap<TmKey,String> tmap2 = new TreeMap<>((obj1,obj2)->{
return obj1.num > obj2.num ? 1 : obj1.num < obj2.num ? -1 : 0;
});
tmap2.put(key2, null);
tmap2.put(key1, null);
tmap2.put(key3, null);
System.out.println(tmap2.size());
for (Map.Entry<TmKey, String> entry : tmap2.entrySet())
{
System.out.println(entry.getKey().num);
}
}
}
class TmKey implements Comparable<Object>
{
public int num;
public TmKey(int num)
{
this.num = num;
}
@Override
public int compareTo(Object obj) {
TmKey m = (TmKey)obj;
return this.num > m.num ? 1 : this.num < m.num ? -1 : 0;
}
public boolean equals(Object obj) {
if (obj == this)
{
return true;
}
if (obj != null && obj.getClass() == TmKey.class)
{
TmKey m = (TmKey)obj;
return m.num == this.num;
}
return false;
}
}
Ps:通过上面TreeMap自然排序和定制排序的例子,可以说明TreeMap的遍历输出都是升序输出!
5.4 TreeMap提供的方法:
TreeMap类中除了实现了Map接口中的方法,由于它Key元素的有序性,还提供了大量的关于Entry和Key数值比较相关的方法,此处就不再展开赘述了!
常用方法:
6. WeakHashMap集合:
WeakHashMap与HashMap基本完全相同,只是HashMap的key保留对象实例的强引用;WeakHashMap的key保留对象的弱引用!当key被Jvm垃圾回收机制回收后,WeakHashMap会自动清理该key的键值对!
7. IdentityHashMap集合:
IdentityHashMap与HashMap基本完全相同,只是HashMap判断key相等的条件是hashcode相等同时equals相等;IdentityHashMap判断key相等的条件必须是两个key为同一个对象实例(key1==key2)!
8. EnumMap集合:
- EnumMap继承自Map接口;
- EnumMap是线程不安全集合;
- EnumMap的key值不可以为null,value值可以为null;
- EnumMap是一个有序的集合,集合顺序为key元素枚举值的数值升序;
- EnumMap中的key元素只能是同一类枚举类型的枚举实例,EnumMap构造的时候已经确定了key值元素的枚举类型,如果类型不一致则抛出异常!
- EnumMap是通过位运算进行存储,有着占用内存小,操作效率的优势;但是局限为key值只能是枚举类型!