集合(2):Map接口
二、Map接口
Map 接口的继承树:
1.Map 接口的特点(JDK8)
- Map与Collection并列存在。用于保存具有映射关系的数据:key-value ;
- Map 中的 key 和 value 都可以是任何引用类型的数据 ,会封装到 HashMap$Node对象中;
- Map 中的 key 用Set来存放,不允许重复(原因和HashSet一样),即同一个 Map 对象所对应 的类,须重写hashCode()和equals()方法 ;
- Map 中的 Value 可以重复;
- Map 中的key 可以为null ,value 也可以为null ,注意key 为null ,只能有一个, value 为null 可以为多个;
- 常用String类作为Map的“键” ;
- key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到 唯一的、确定的 value ;
- Map接口的常用实现类:HashMap、TreeMap、LinkedHashMap和 Properties。其中,HashMap是 Map 接口使用频率最高的实现类。
由于 HashMap$ Node 实现了 HashMap$Entry 接口,也可以说 一对 k-v 就是一个 Entry 。
static class Node<K, V> implements Entry<K, V>{}
添加元素时,底层通过创建 HashMap$Node 类的对象实现,同时为了方便遍历,还创建了一个 EntrySet集合,该集合存放的数据是 Entry类型的,一个Entry 对象都有一个 k,v,EntrySet< Entry< K,V>> 即: transient Set<Entry<K, V>> entrySet;
newNode(hash, key, value, (HashMap.Node)null);
public class MapSource {
public static void main(String[] args) {
Map map= new HashMap();
map.put("No1","Tom");
Set set = map.entrySet();
System.out.println(set.getClass());
for (Object entry:set){
System.out.println(entry.getClass());
Map.Entry entry1 = (Map.Entry) entry;
System.out.println(entry1.getKey()+"-"+entry1.getValue());
}
}
}
结果:
实际上,entrySet 里面存放的依然是 HashMap$Node 类型。使用Entry 原因在于其定义了一些方便遍历的方法,如 getKey、getValue等。
但是,需要注意的是,在底层并没有创建一块新的内存,复制存放相同的数据,Entry 指向的是同一数据。
分析如下:
public class MapSource {
public static void main(String[] args) {
Map map= new HashMap();
map.put("No1","Tom");
map.put("No2",new Car("特斯拉"));
}
}
class Car{
private String name;
public Car(String name) {
this.name = name;
}
}
结果:它们的地址是相同的,因此是同一个对象。可以看作是,entrySet 存放的是 Node元素的引用。
2.Map接口 常用方法
● 添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
● 元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
● 元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
3.Map接口 六种遍历方式
public class MapFor {
public static void main(String[] args) {
Map map = new HashMap();
map.put("张杰","谢娜");
map.put("邓超","孙俪");
map.put("吴亦凡",null);
map.put(null,"迪丽热巴");
map.put("黄晓明","杨颖");
}
}
(1)方式一:先取出所有的 key ,再根据 key 获取对应的 Value
① 使用 增强for 遍历
② 使用 iterator 迭代器遍历
Set keyset = map.keySet();
//使用增强for
System.out.println("第一种方式:");
for(Object key:keyset){
System.out.println(key+"-"+map.get(key));
}
//使用迭代器
System.out.println("第二种方式:");
Iterator iterator1 = keyset.iterator();
while (iterator.hasNext()) {
Object key = iterator.next();
System.out.println(key+"-"+map.get(key));
}
(2)方式二:直接取出所有的 value
③ 使用 增强for 遍历
④ 使用 iterator 迭代器遍历
Collection values = map.values();
System.out.println("第三种方式:");
for (Object value:values){
System.out.println(value);
}
System.out.println("第四种方式:");
Iterator iterator2 = values.iterator();
while (iterator.hasNext()) {
Object value = iterator.next();
System.out.println(value);
}
(3)方式三 : 通过 EntrySet 来获取 k-v
⑤ 使用 增强for 遍历
⑥ 使用 iterator 迭代器遍历
此处的 entrySet() 方法获取的 entrySet 集合中存放的是 Node 类型的数据,而Node 类型是 Map.Entry 接口的实现类 ,这个接口提供了方便我们遍历的 getKey() 、getValue() 等方法,因此此处先将 Node 向下转型为 Map.Entry
Set entrySet = map.entrySet();
System.out.println("第五种方式:");
for (Object entry:entrySet){
//将entry转成 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey()+"-"+m.getValue());
}
System.out.println("第六种方式:");
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object entry = iterator3.next();
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey()+"-"+m.getValue());
}
★ 4.Map实现类之一:HashMap ★
● HashMap是 Map 接口使用频率最高的实现类。
● 允许使用null键和null值,与HashSet一样,不保证映射的顺序。
● 所有的key构成的集合是Set:无序的、不可重复的。所以,key所在的类要重写: equals()和hashCode()
● 所有的value构成的集合是Collection:无序的、可以重复的。所以,value所在的类 要重写:equals()
● 一个key-value构成一个entry
● 所有的entry构成的集合是Set:无序的、不可重复的
● HashMap 判断两个 key 相等的标准是:两个 key 通过 equals() 方法返回 true, hashCode 值也相等。
● HashMap 判断两个 value相等的标准是:两个 value 通过 equals() 方法返回 true。
● HashMap 没有实现同步,因此是线程不安全的。
HashMap 的底层机制和源码剖析
扩容机制和 HashSet 相同。
底层机制:
1)HashMap 底层维护了 Node类型的数组table ,默认为 null ;
2)当创建对象时,将加载因子(loadfactor)初始化为0.75;
3)当添加 k-v 键值对时,通过 key 计算 哈希值得到在table 表中存放位置的索引。然后判断该索引位置是否有元素,如果没有元素直接添加。
如果该索引位置有元素,判断该元素的 key 和准备添加的 key是否相等,如果相等 ,则直接替换 value ;如果不相等,需要判断是树结构还是链表结构,做出相应的处理。如果添加时发现容量不够,则需要扩容。
4)在Java8中,如果一条链表的元素个数超过 TREEIFY_THRESHOLD(默认值为8),并且 table的大小 >= MIN_TREEIFY_CAPACITY(默认值64),就会进行树化(红黑树)
源码解析:
public class HashMapSource {
public static void main(String[] args) {
HashMap hashMap = new HashMap();
hashMap.put("java",10);
hashMap.put("php",10);
hashMap.put("java",20); //添加时替换value
System.out.println("hashMap="+hashMap);
}
}
1.执行构造器 new HashMap() ,初始化加载因子 loadFactor =0.75,此时HashMap$Node[ ] table=null
public HashMap() {
this.loadFactor = 0.75F;
}
2.执行 put 方法 ,
3.进一步执行 putVal 方法(原理和 HashSet中的分析一样)
public V put(K key, V value) {
return this.putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
HashMap.Node[] tab; //定义了一些辅助变量,tab是HashMap 的一个数组,类型是Node
int n;
// if 语句判断当前tab 是null或者大小为空的情况,通过调用 resize() 方法,返回一个容量为16的数组,缓存极限为12
if ((tab = this.table) == null || (n = tab.length) == 0) {
n = (tab = this.resize()).length;
}
Object p;
int i;
/** (1)根据 key ,上一步(3)中调用 hash(key) 返回key对应的hash值 得到key应该存放到tab表的索引位置(即算法 i = n - 1 & hash ),
将这个位置的对象,赋值给 p
(2)此时判断 p 是否为 null
2.1 若为null,说明当前位置没有元素,则调用下面的方法,创建一个Node对象 Node(key=“java”,value=PRESENT,next=null)
HashMap.Node<K, V> newNode(int hash, K key, V value, HashMap.Node<K, V> next) {
//此处传入hash 是为了判断下一次添加时是否重复
return new HashMap.Node(hash, key, value, next);
}
然后将该对象放到这个位置;
2.2 若不为空,见下
*/
if ((p = tab[i = n - 1 & hash]) == null) {
tab[i] = this.newNode(hash, key, value, (HashMap.Node)null);
} else {
Object e;
Object k;
/** 在 p 不为空的情况下,
一、如果当前索引位置对应的链表中第一个元素和准备添加的元素hash值相同 ((HashMap.Node)p).hash == hash
并且满足一下条件之一时,都不能加入
1)准备添加的key 和 p 指向的Node节点的 key是同一个对象
2) p 指向的Node节点的 key使用equals 方法和准备加入的 key比较后相同
*/
if (((HashMap.Node)p).hash == hash && ((k = ((HashMap.Node)p).key) == key || key != null && key.equals(k))) {
e = p;
// 二、然后判断 p 是否是一棵红黑树,如果是一棵红黑树,则调用 putTreeVal 进行添加(细节较复杂,此处不详述)
} else if (p instanceof HashMap.TreeNode) {
e = ((HashMap.TreeNode)p).putTreeVal(this, tab, hash, key, value);
} else {
int binCount = 0;
/**三、以上情况都不满足时,需要使用循环依次和当前索引位置对应的链表中的元素逐一比较
(1)循环结束后,如果不和其中的元素相同,则添加到链表的最后
注意: 添加新的元素后,立即判断 此链表是否已经达到8个节点,
是就调用 treeifyBin 方法对当前链表树化,在转化为红黑树时,会进行判断:
tab != null && (n = tab.length) >= 64,满足时,先进行table扩容,
只有不满足才进行树化
(2)循环过程中,发现有相同的元素时,就break跳出
*/
while(true) {
if ((e = ((HashMap.Node)p).next) == null) { //此处是判断整个链表没有和该value 相同时,就添加到最后
((HashMap.Node)p).next = this.newNode(hash, key, value, (HashMap.Node)null);
if (binCount >= 7) { //此处是添加后判断当前链表的的个数,是否到达8个,是就进行树化
this.treeifyBin(tab, hash);
}
break;
}
// 此处是发现链表中有相同的元素,则break 不添加,只是替换该value
if (((HashMap.Node)e).hash == hash && ((k = ((HashMap.Node)e).key) == key || key != null && key.equals(k))) {
break;
}
p = e;
++binCount;
}
}
if (e != null) {
V oldValue = ((HashMap.Node)e).value; //当添加的元素key相同, value被替换为新的value
if (!onlyIfAbsent || oldValue == null) {
((HashMap.Node)e).value = value;
}
this.afterNodeAccess((HashMap.Node)e); //空方法
return oldValue;
}
}
++this.modCount;
if (++this.size > this.threshold) { //此处判断是否要扩容
this.resize();
}
this.afterNodeInsertion(evict);//此方法在HashMap中是一个空方法,它主要是用来给HashMap的子类去实现这个方法,做一些操作
return null;
}
拓展:
HashMap 源码中重要的常量:
5.Map实现类之二:Hashtable
5.1 Hashtable 的基本介绍
1)Hashtable是个古老的 Map 实现类,JDK1.0就提供了。不同于HashMap, Hashtable是线程安全的。
2)Hashtable实现原理和HashMap相同,功能相同。底层都使用哈希表结构,查询 速度快,很多情况下可以互用。
3)与HashMap不同,Hashtable 不允许使用 null 作为 key 和 value ;
4)与HashMap一样,Hashtable 也不能保证其中 Key-Value 对的顺序 ;
5)Hashtable判断两个key相等、两个value相等的标准,与HashMap一致。
HashTable 的底层数组 HashTable$Entry [ ] 初始化大小为 11
临界值为 threshold 为8 =11*0.75F
public Hashtable() {
this(11, 0.75F);
}
5.2 Hashtable 的扩容机制
HashTable 使用 put() 方法添加元素,底层调用的是 addEntry() 方法
public synchronized V put(K key, V value) {
this.addEntry(hash, key, value, index);
return null;
}
addEntry() 方法 执行时,会判断元素个数是否超过阈值 threshold,超过会触发扩容机制 rehash()
private void addEntry(int hash, K key, V value, int index) {
Hashtable.Entry<?, ?>[] tab = this.table;
if (this.count >= this.threshold) {
this.rehash();
tab = this.table;
hash = key.hashCode();
index = (hash & 2147483647) % tab.length;
}
Hashtable.Entry<K, V> e = tab[index];
tab[index] = new Hashtable.Entry(hash, key, value, e);
++this.count;
++this.modCount;
}
扩容机制源码:
int newCapacity = (oldCapacity << 1) + 1;
扩容为原来的 2倍+1 的大小。
protected void rehash() {
int oldCapacity = this.table.length;
Hashtable.Entry<?, ?>[] oldMap = this.table;
int newCapacity = (oldCapacity << 1) + 1;
if (newCapacity - 2147483639 > 0) {
if (oldCapacity == 2147483639) {
return;
}
newCapacity = 2147483639;
}
5.3 Hashtable 和 HashMap 的对比:
6.Map实现类之三:LinkedHashMap
● LinkedHashMap 是 HashMap 的子类
● 在HashMap存储结构的基础上,使用了一对双向链表来记录添加元素的顺序
● 与LinkedHashSet类似,LinkedHashMap 可以维护 Map 的迭代 顺序:迭代顺序与 Key-Value 对的插入顺序一致
7.Map实现类之四:Properties
● Properties 类是 Hashtable 的子类,该对象用于处理属性文件 ;
● 由于属性文件里的 key、value 都是字符串类型,所以 Properties 里的 key 和 value 都是字符串类型 ;
● 存取数据时,建议使用 setProperty(String key,String value) 方法和 getProperty(String key) 方法。
Properties pros = new Properties();
pros.load(new FileInputStream("jdbc.properties"));
String user = pros.getProperty("user");
System.out.println(user);
8.Map实现类之五:TreeMap
● TreeMap存储 Key-Value 对时,需要根据 key-value 对进行排序。 TreeMap 可以保证所有的 Key-Value 对处于有序状态。
● TreeSet底层使用红黑树结构存储数据
● TreeMap 的 Key 的排序:
- 自然排序:TreeMap 的所有的 Key 必须实现 Comparable 接口,而且所有 的 Key 应该是同一个类的对象,否则将会抛出 ClasssCastException
final int compare(Object k1, Object k2) {
return this.comparator == null ? ((Comparable)k1).compareTo(k2) : this.comparator.compare(k1, k2);
}
需要 Key 类实现 Comparable 接口:
public interface Comparable<T> {
int compareTo(T var1);
}
- 定制排序:创建 TreeMap 时,传入一个 Comparator 对象,该对象负责对 TreeMap 中的所有 key 进行排序。此时不需要 Map 的 Key 实现 Comparable 接口
● TreeMap判断两个key相等的标准:两个key通过compareTo()方法或 者compare()方法返回0。
代码示例:
public class TreeMapTest {
public static void main(String[] args) {
TreeMap treeMap = new TreeMap(new Comparator() {
@Override
public int compare(Object o, Object t1) {
//按照传入的 k(String) 的大小排序
// return ((String)o).compareTo((String)t1);
//照传入的 k(String) 长度的大小排序
return ((String)o).length()-((String)t1).length();
}
});
treeMap.put("Jack","杰克");
treeMap.put("Mary","玛丽");
treeMap.put("Tom","汤姆");
treeMap.put("Alice","爱丽丝");
System.out.println("treeMap="+treeMap);
}
}
输出结果为:
这里的Jack 和Mary,在指定的 Compare 方法中,判断是相等的 key ,因此会被替换。
源码分析:
1)构造器,将传入的实现了Comparator接口的匿名内部类,传给了TreeMap 的comparator
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
2)第一次使用 put 方法添加元素时,即 t == null 将 k-v 键值对封装到 Entry对象赋给 root;
3)此后添加时,会遍历所有的 key ,给当前的 key找到合适的位置,动态绑定到 comparator。
遍历过程中,按照Compare 方法, 如果 判断 key 相等,则使用新的 value 替换原来的。
public V put(K key, V value) {
//第一次添加元素时,将 k-v 键值对封装到 Entry对象赋给 root
TreeMap.Entry<K, V> t = this.root;
if (t == null) {
this.compare(key, key);
this.root = new TreeMap.Entry(key, value, (TreeMap.Entry)null);
this.size = 1;
++this.modCount;
return null;
} else {
Comparator<? super K> cpr = this.comparator;
int cmp;
TreeMap.Entry parent;
if (cpr != null) {
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0) {
t = t.left;
} else {
if (cmp <= 0) {
return t.setValue(value); //判断 key 相等时,则使用新的 value 替换
}
t = t.right;
}
} while(t != null);
} else {
if (key == null) {
throw new NullPointerException();
}
Comparable k = (Comparable)key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0) {
t = t.left;
} else {
if (cmp <= 0) {
return t.setValue(value);
}
t = t.right;
}
} while(t != null);
}
TreeMap.Entry<K, V> e = new TreeMap.Entry(key, value, parent);
if (cmp < 0) {
parent.left = e;
} else {
parent.right = e;
}
this.fixAfterInsertion(e);
++this.size;
++this.modCount;
return null;
}
}
★三、总结:如何选择集合实现类 ★
在实际开发中,主要取决于业务操作特点,根据集合实现类的特性选择,分析如下:
1)先判断存储的类型:一组对象 [单列] 或一组键值对 [双列]
2)一组对象 [单列] :Collection 接口
允许重复: List
增删多:LinkedList (底层维护了一个双向链表)
改查多:ArrayList (底层维护了 Object 类型的可变数组)
不允许重复时:
无序: HashSet (底层是 HashMap ,维护了一个哈希表,即:数组+链表+红黑树)
排序: TreeSet
插入和取出顺序一致: LInkedHashSet ,维护数组+双向链表
3)一组键值对 [双列] :Map
键无序:HashMap
键排序:TreeMMap
键插入和读取顺序一致: LinkedHashMap
读取文件: Properties
四、 Collections工具类
● Collections 是一个操作 Set、List 和 Map 等集合的工具类
● Collections 中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作, 还提供了对集合对象设置不可变、对集合对象实现同步控制等方法
● 排序操作:(均为static方法)
reverse(List):反转 List 中元素的顺序
shuffle(List):对 List 集合元素进行随机排序
sort(List):根据元素的自然顺序对指定 List 集合元素按升序排序
sort(List,Comparator):根据指定的 Comparator 产生的顺序对 List 集合元素进行排序
swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换
Collections常用方法
● 查找、替换
Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
Object max(Collection,Comparator):根据 Comparator 指定的顺序,返回 给定集合中的最大元素 Object min(Collection)
Object min(Collection,Comparator) int frequency(Collection,Object):返回指定集合中指定元素的出现次数
void copy(List dest,List src):将src中的内容复制到dest中
boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值
● Collections 类中提供了多个 synchronizedXxx() 方法,该方法可使将指定集 合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
相关面试题:
1.负载因子值的大小,对HashMap有什么影响?
● 负载因子的大小决定了HashMap的数据密度。
● 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长, 造成查询或插入时的比较次数增多,性能会下降。
● 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
● 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此 时平均检索长度接近于常数。