前面,已经系统的对List进行了学习。接下来,先学习Map,然后再学习Set;因为Set的实现类都是基于Map来实现的(如:HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的)。
一、Map架构
如上图:
(1)、 Map 是映射接口,Map中存储的内容是键值对(key-value)。
(2)、 AbstractMap 是继承于Map的抽象类,它实现了Map中的大部分API。其它Map的实现类可以通过继承AbstractMap来减少重复编码。
(3) 、SortedMap 是继承于Map的接口。SortedMap中的内容是排序的键值对,排序的方法是通过比较器(Comparator)。
(4) 、NavigableMap 是继承于SortedMap的接口。相比于SortedMap,NavigableMap有一系列的导航方法;如"获取大于/等于某对象的键值对"、“获取小于/等于某对象的键值对”等等。
(5) 、TreeMap 继承于AbstractMap,且实现了NavigableMap接口;因此,TreeMap中的内容是“有序的键值对”!
(6) 、HashMap 继承于AbstractMap,但没实现NavigableMap接口;因此,HashMap的内容是“键值对,但不保证次序”!
(7)、 Hashtable 虽然不是继承于AbstractMap,但它继承于Dictionary(Dictionary也是键值对的接口),而且也实现Map接口;因此,Hashtable的内容也是“键值对,也不保证次序”。但和HashMap相比,Hashtable是线程安全的,而且它支持通过Enumeration去遍历。
(8) 、WeakHashMap 继承于AbstractMap。它和HashMap的键类型不同,WeakHashMap的键是“弱键”。
二、Map
1、定义:
java.util public Interface Map<K,V>
2、Map接口特点:
- Map提供了一种映射关系,其中的元素是以键值对(key-value)的形式存储的,能够实现根据key快速查找value。
- Map中的键值对以Entry类型的对象实例形式存在。
- 键(key值)不可重复,value值可以重复,一个value值可以和很多key值形成对应关系,每个建最多只能映射到一个值。
- Map支持泛型,形式如:Map<K,V>。
- Map 映射顺序:有些实现类,可以明确保证其顺序,如 TreeMap;另一些映射实现则不保证顺序,如 HashMap 类。
3、常用方法
abstract void clear()
abstract boolean containsKey(Object key)
abstract boolean containsValue(Object value)
abstract Set<Entry<K, V>> entrySet()
abstract boolean equals(Object object)
abstract V get(Object key)
abstract int hashCode()
abstract boolean isEmpty()
abstract Set<K> keySet()
abstract V put(K key, V value)
abstract void putAll(Map<? extends K, ? extends V> map)
abstract V remove(Object key)
abstract int size()
abstract Collection<V> values()
4、Map.Entry:Map是java中的接口,Map.Entry是Map的一个内部接口。
(1)interface Entry<K,V> { }
(2)Map.Entry的常用方法
abstract boolean equals(Object object)
abstract K getKey()
abstract V getValue()
abstract int hashCode()
abstract V setValue(V object)
三、HashMap类
1、简介 :
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable
- HashMap 继承于AbstractMap,实现了Map、Cloneable、java.io.Serializable接口。
- HashMap是Map的一个重要实现类,也是最常用的,基于哈希表实现。
- HashMap中的Entry对象是无序排列的。
- Key值和value值都可以为null,但是一个HashMap只能有一个key值为null的映射(key值不可重复)。
- HashMap是线程不安全的。
2、构造函数
// 默认构造函数。
HashMap()
// 指定“容量大小”的构造函数
HashMap(int capacity)
// 指定“容量大小”和“加载因子”的构造函数
HashMap(int capacity, float loadFactor)
// 包含“子Map”的构造函数
HashMap(Map<? extends K, ? extends V> map)
容量是哈希表中桶的数量,初始容量只是哈希表在创建时的容量。加载因子是哈希表在其容量自动增加之前可以达到多满的一种尺度。当哈希表中的条目数超出了加载因子与当前容量的乘积时,则要对该哈希表进行 rehash 操作(即重建内部数据结构),从而哈希表将具有大约两倍的桶数。通常,默认加载因子是 0.75, 在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少 rehash 操作次数。如果初始容量大于最大条目数除以加载因子,则不会发生 rehash 操作。
3、HashMap的常用方法
void clear()//清空HashMap。它是通过将所有的元素设为null来实现的。
Object clone()//克隆一个HashMap对象并返回
boolean containsKey(Object key) //判断HashMap是否包含key。
boolean containsValue(Object value)//判断HashMap是否包含“值为value”的元素。
Set<Entry<K, V>> entrySet() //返回“HashMap中所有Entry的集合”.
V get(Object key) //获取key对应的value
boolean isEmpty()
Set<K> keySet()
V put(K key, V value) //对外提供接口,让HashMap对象可以通过put()将“key-value”添加到HashMap中。
void putAll(Map<? extends K, ? extends V> map)
V remove(Object key)//删除“键为key”元素
int size()
Collection<V> values()//返回此地图中包含的值的Collection视图。
4、HashMap 和 Map的关系
从图中可以看出:
(1) HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。
(2) HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
size是HashMap的大小,它是HashMap保存的键值对的数量。
threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值="容量*加载因子",当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
loadFactor就是加载因子。
modCount是用来实现fail-fast机制的。
5、HashMap的原理
(1) HashMap就是一个散列表,它是通过“拉链法”解决哈希冲突的。影响HashMap性能的有两个参数:初始容量(initialCapacity) 和加载因子(loadFactor)。
(2) 哈希表(HashTable)又叫做散列表,是根据关键码值(即键值对)而直接访问的数据结构,是一种数据结构能够快速地查找所需的对象。也就是说,它通过把关键码映射到表中一个位置来访问记录,以加快查找速度。这个映射函数就叫做散列(哈希)函数,存放记录的数组叫做散列表。散列码(hash code)是由对象的实例产生的一个整数。具有不同的数据域的对象将产生不同的散列码。
(3) 数组与链表。数组的特点就是查找容易,插入删除困难;而链表的特点就是查找困难,但是插入删除容易。既然两者各有优缺点,那么我们就将两者的有点结合起来,让它查找容易,插入删除也会快起来。哈希表就是讲两者结合起来的产物。
(4) 两个不同的关键字,由于散列函数值相同,因而被映射到同一表位置上。该现象称为冲突(Collision)或碰撞。
HashMap中采用的“拉链法”就是一种冲突解决的方式(hash函数的设计才是冲突避免,但不是一种完全的冲突解决方法),如下图所示为“拉链法”结构。
但是HashMap中的节点是Map.Entry类型的,而不是简单的value,如右上边是一个Node<K,V>[] table数组(在jdk6中是Entry<K,V>数组),Node是Map.Entry的实现类。
即key值不同的两个或多个Map.Entry<K,V>可能会插在同一个桶下面,但是当查找到某个特定的hash值的时候,下面挂了很多个<K,V>映射,怎么确定哪个是我要找的那个<K,V>呢?这就是HashMap底层结构的一个亮点,在它的Entry中不仅仅只是插入value的,他是插入整个Entry 的,里面包含key和value的,所以能识别同一个hash值下的不同Map.Entry。
// 判断两个Entry是否相等
// 若两个Entry的“key”和“value”都相等,则返回true。
// 否则,返回false
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
6、看看put() 和 remove()
(1)put() 的作用是对外提供接口,让HashMap对象可以通过put()将“key-value”添加到HashMap中。
若要添加到HashMap中的键值对对应的key已经存在HashMap中,则找到该键值对;然后新的value取代旧的value,并退出!
若要添加到HashMap中的键值对对应的key不在HashMap中,则将其添加到该哈希值对应的链表中,并调用addEntry()
public V put(K key, V value) {
// 若“key为null”,则将该键值对添加到table[0]中。
if (key == null)
return putForNullKey(value);
// 若“key不为null”,则计算该key的哈希值,然后将其添加到该哈希值对应的链表中。
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
// 若“该key”对应的键值对已经存在,则用新的value取代旧的value。然后退出!
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
// 若“该key”对应的键值对不存在,则将“key-value”添加到table中
modCount++;
addEntry(hash, key, value, i);
return null;
}
下面看看addEntry():addEntry() 的作用是新增Entry。将“key-value”插入指定位置,bucketIndex是位置索引。
void addEntry(int hash, K key, V value, int bucketIndex) {
// 保存“bucketIndex”位置的值到“e”中
Entry<K,V> e = table[bucketIndex];
// 设置“bucketIndex”位置的元素为“新Entry”,
// 设置“e”为“新Entry的下一个节点”
table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
// 若HashMap的实际大小 不小于 “阈值”,则调整HashMap的大小
if (size++ >= threshold)
resize(2 * table.length);
}
说到addEntry(),就不得不说另一个函数createEntry()。它们的作用都是将key、value添加到HashMap中。
(1) addEntry()一般用在 新增Entry可能导致“HashMap的实际容量”超过“阈值”的情况下。
例如,我们新建一个HashMap,然后不断通过put()向HashMap中添加元素;put()是通过addEntry()新增Entry的。
在这种情况下,我们不知道何时“HashMap的实际容量”会超过“阈值”;
因此,需要调用addEntry()。
(2) createEntry() 一般用在 新增Entry不会导致“HashMap的实际容量”超过“阈值”的情况下。
例如,我们调用HashMap“带有Map”的构造函数,它绘将Map的全部元素添加到HashMap中;
但在添加之前,我们已经计算好“HashMap的容量和阈值”。也就是,可以确定“即使将Map中的全部元素添加到HashMap中,都不会超过HashMap的阈值”。 此时,调用createEntry()即可。
(2) remove(): remove() 的作用是删除“键为key”元素
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
// 删除“键为key”的元素
final Entry<K,V> removeEntryForKey(Object key) {
// 获取哈希值。若key为null,则哈希值为0;否则调用hash()进行计算
int hash = (key == null) ? 0 : hash(key.hashCode());
int i = indexFor(hash, table.length);
Entry<K,V> prev = table[i];
Entry<K,V> e = prev;
// 删除链表中“键为key”的元素
// 本质是“删除单向链表中的节点”
while (e != null) {
Entry<K,V> next = e.next;
Object k;
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
modCount++;
size--;
if (prev == e)
table[i] = next;
else
prev.next = next;
e.recordRemoval(this);
return e;
}
prev = e;
e = next;
}
return e;
}
7、遍历HashMap
(1)先拿到key的集合,通过key拿到值:
for (String key: map.keySet()) {
System.out.print(key+"="+map.get(key)+" ");
}
(2)先拿到值的集合,通过迭代器迭代
Collection<Integer> c = map.values();
Iterator<Integer> t = c.iterator();
while(t.hasNext()) {
System.out.println(t.next());
}
8、操作示例
package MapTest;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
public class HashMapTest {
public static void main(String[] args) {
HashMap<String, Integer> map= new HashMap<>();
map.put("A", 1);
map.put("B", 2);
map.put("C", 3);
map.put("D", 4);
//直接输出内容:{A=1, B=2, C=3, D=4}
System.out.println(map);
//通过Key找Value:key为A-->value =1
System.out.println("key为A-->value ="+map.get("A"));
//找不到返回null:key为A-->value =null
System.out.println("key为E-->value ="+map.get("E"));
//删除:{B=2, C=3, D=4}
map.remove("A");
System.out.println(map);
//不存在,返回null
Object o = map.remove("E");
System.out.println(o);
//遍历:B=2 C=3 D=4 先拿到key的集合,通过key拿到值
for (String key: map.keySet()) {
System.out.print(key+"="+map.get(key)+" ");
}
System.out.println("------------");
//先拿到值的集合,通过迭代器迭代
Collection<Integer> c = map.values();
Iterator<Integer> t = c.iterator();
while(t.hasNext()) {
System.out.println(t.next());
}
}
}
四、HashTable
1、简介:Hashtable类大致相当于HashMap ,除了它是同步的,不允许null。
- Hashtable 继承于Dictionary,实现了Map、Cloneable、java.io.Serializable接口。
- Hashtable 的函数都是同步的,这意味着它是线程安全的。它的key、value都不可以为null。此外,Hashtable中的映射不是有序。
- Hashtable 一个实例有两个影响其性能的参数: 初始容量和负载因子 。通常,默认加载因子是 0.75。
2、需注意的点:
- Hashtable的默认容量为11,默认负载因子为0.75.(HashMap默认容量为16,默认负载因子也是0.75)
- Hashtable的容量可以为任意整数,最小值为1,而HashMap的容量始终为2的n次方。
- 为避免扩容带来的性能问题,建议指定合理容量。
- 跟HashMap一样,Hashtable内部也有一个静态类叫Entry,其实是个键值对对象,保存了键和值的引用。
- HashMap和Hashtable存储的是键值对对象,而不是单独的键或值。
- Hashtable每次扩容,容量都为原来的2倍加1,而HashMap为原来的2倍。
3、Hashtable存取删数据:put()方法、get()方法、remove()方法
(1)put方法:加了synchronized关键字
public synchronized V put(K key, V value) {//向哈希表中添加键值对
// Make sure the value is not null
if (value == null) {//确保值不能为空
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry tab[] = table;
int hash = hash(key);//根据键生成hash值---->若key为null,此方法会抛异常
int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash值找到其存储位置
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {/遍历链表
if ((e.hash == hash) && e.key.equals(key)) {//若键相同,则新值覆盖旧值
V old = e.value;
e.value = value;
return old;
}
}
modCount++;
if (count >= threshold) {//当前容量超过阈值。需要扩容
// Rehash the table if the threshold is exceeded
rehash();//重新构建桶数组,并对数组中所有键值对重哈希,耗时!
tab = table;
hash = hash(key);
index = (hash & 0x7FFFFFFF) % tab.length;//这里是取摸运算
}
// Creates the new entry.
Entry<K,V> e = tab[index];
//将新结点插到链表首部
tab[index] = new Entry<>(hash, key, value, e);//生成一个新结点
count++;
return null;
}
1、Hasbtable并不允许值和键为空(null),若为空,会抛空指针。
2、HashMap计算索引的方式是h&(length-1),而Hashtable用的是模运算,效率上是低于HashMap的。
3、另外Hashtable计算索引时将hash值先与上0x7FFFFFFF,这是为了保证hash值始终为正数。
4、特别需要注意的是这个方法包括下面要讲的若干方法都加了synchronized关键字,也就意味着这个Hashtable是个线程安全的类,这也是它和HashMap最大的不同点。
(2)get()取数据:获取key对应的value,没有的话返回null
public synchronized V get(Object key) {//根据键取出对应索引
Entry tab[] = table;
int hash = hash(key);//先根据key计算hash值
int index = (hash & 0x7FFFFFFF) % tab.length;//再根据hash值找到索引
for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {//遍历entry链
if ((e.hash == hash) && e.key.equals(key)) {//若找到该键
return e.value;//返回对应的值
}
}
return null;//否则返回null
}
(3)、remove():删除Hashtable中键为key的元素
public synchronized V remove(Object key) {
Entry tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
// 找到“key对应的Entry(链表)”
// 然后在链表中找出要删除的节点,并删除该节点。
for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
modCount++;
if (prev != null) {
prev.next = e.next;
} else {
tab[index] = e.next;
}
count--;
V oldValue = e.value;
e.value = null;
return oldValue;
}
}
return null;
}
五、TreeMap
1、简介
public class TreeMap<K,V> extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, Serializable
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
- TreeMap 继承于AbstractMap,所以它是一个Map,即一个key-value集合。
- TreeMap 实现了NavigableMap接口,意味着它支持一系列的导航方法。比如返回有序的key集合。
- TreeMap 实现了Cloneable接口,意味着它能被克隆。
- TreeMap 实现了java.io.Serializable接口,意味着它支持序列化。
- TreeMap是非同步的。 它的iterator 方法返回的迭代器是fail-fastl的。
- TreeMap的基本操作 containsKey、get、put 和 remove 的时间复杂度是 log(n) 。
TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
2、构造函数
// 默认构造函数。使用该构造函数,TreeMap中的元素按照自然排序进行排列。
TreeMap()
// 创建的TreeMap包含Map
TreeMap(Map<? extends K, ? extends V> copyFrom)
// 指定Tree的比较器
TreeMap(Comparator<? super K> comparator)
// 创建的TreeSet包含copyFrom
TreeMap(SortedMap<K, ? extends V> copyFrom)
3、TreeMap的常用方法,详细看API
Entry<K, V> ceilingEntry(K key)//返回与大于或等于给定键的最小键相关联的键值映射,如果没有此键,则 null 。
K ceilingKey(K key) 返回大于/等于key的最小的键值对所对应的KEY,没有的话返回null
void clear()
Object clone()
Comparator<? super K> comparator()//返回用于订购此地图中的键的比较器,或null如果此地图使用其键的natural ordering 。
boolean containsKey(Object key)
NavigableSet<K> descendingKeySet()//返回此地图中包含的键的相反顺序NavigableSet 。
NavigableMap<K, V> descendingMap()//返回此映射中包含的映射的反向排序视图。
Set<Entry<K, V>> entrySet() //返回此地图中包含的映射的Set视图。
Entry<K, V> firstEntry()//返回与该地图中的最小键相关联的键值映射
K firstKey() //返回此地图中当前的第一个(最低)键
V get(Object key)
boolean isEmpty()
Set<K> keySet() //返回此地图中包含的键的Set视图。
Entry<K, V> lastEntry()
K lastKey()
Entry<K, V> lowerEntry(K key)
K lowerKey(K key)
V put(K key, V value)
V remove(Object key)
int size()
4、TreeMap本质(首先需要去了解下红黑树)
1)、TreeMap的本质是R-B Tree(红黑树),它包含几个重要的成员变量: root, size, comparator。
2)、root 是红黑数的根节点。它是Entry类型,Entry是红黑数的节点,它包含了红黑数的6个基本组成成分:key(键)、value(值)、left(左孩子)、right(右孩子)、parent(父节点)、color(颜色)。Entry节点根据key进行排序,Entry节点包含的内容为value。
3)、红黑数排序时,根据Entry中的key进行排序;Entry中的key比较大小是根据比较器comparator来进行判断的。
4)、size是红黑数中节点的个数。
5)TreeMap是通过红黑树实现的,TreeMap存储的是key-value键值对,TreeMap的排序是基于对key的排序。
6)TreeMap提供了操作“key”、“key-value”、“value”等方法,也提供了对TreeMap这颗树进行整体操作的方法,如获取子树、反向树。
5、TreeMap的Entry相关函数
1、firstEntry()和getFirstEntry()的代码:可以看出 firstEntry() 和 getFirstEntry() 都是用于获取第一个节点。
public Map.Entry<K,V> firstEntry() {
return exportEntry(getFirstEntry());
}
final Entry<K,V> getFirstEntry() {
Entry<K,V> p = root;
if (p != null)
while (p.left != null)
p = p.left;
return p;
}
firstEntry() 是对外接口; getFirstEntry() 是内部接口。而且,firstEntry() 是通过 getFirstEntry() 来实现的。这么做的目的是:防止用户修改返回的Entry。getFirstEntry()返回的Entry是可以被修改的,但是经过firstEntry()返回的Entry不能被修改,只可以读取Entry的key值和value值。 getFirstEntry()返回的是Entry节点,而Entry是红黑树的节点,我们可以调用Entry的getKey()、getValue()来获取key和value值,以及调用setValue()来修改value的值。
总结:
(1)、 firstEntry()是对外接口,而getFirstEntry()是内部接口。
(2)、 对firstEntry()返回的Entry对象只能进行getKey()、getValue()等读取操作;而对getFirstEntry()返回的对象除了可以进行读取操作之后,还可以通过setValue()修改值。
6、相关操作和遍历
package MapTest;
import java.util.TreeMap;
public class TreeMapTest {
public static void main(String[] args) {
TreeMap<String,Integer> map = new TreeMap<>();
//添加
map.put("A", 30);
map.put("B", 28);
map.put("C", 45);
map.put("D", 33);
map.put("E", 15);
map.put("F", 27);
//打印集合
System.out.println(map);
System.out.println("------------------------------");
//删除集合
map.remove("F");
//遍历集合
for (String key : map.keySet()) {
System.out.println(key+"->"+map.get(key));
}
System.out.println("------------------------------");
//获得key的反序
System.out.println(map.descendingKeySet());
System.out.println("------------------------------");
//获得集合的反序
System.out.println(map.descendingMap());
System.out.println("------------------------------");
//获得第一个集合
System.out.println(map.firstKey());
}
}
Map集合遍历方式都差不多,TreeMap其他遍历方式参考HashMap