先看下set的类图:
Set集合和Map集合的对应关系如下。
-
Set集合和Map集合的对应关系如下。
■ Set <-> Map
■ EnumSet <-> EnumMap
■ SortedSet <-> SortedMap
■ TreeSet <-> TreeMap
■ NavigableSet <-> NavigableMap
■ HashSet <-> HashMap
■ LinkedHashSet <-> LinkedHashMap
Map集合的key具有一个特征:所有key不能重复,key之间没有顺序。也就是说,如果将Map集合的所有key集中起来,那这些key就组成了一个Set集合。所以,发现Map集合提供了如下方法来返回所有key组成的Set集合。
class SimpleEntry<K , V>
implements Map.Entry<K , V>, java.io.Serializable
{
private final K key;
private V value;
//定义如下2个构造器
public SimpleEntry(K key, V value)
{
this.key =key;
this.value=value;
}
public SimpleEntry(Map.Entry<? extends K
, ? extends V> entry)
{
this.key =entry.getKey();
this.value=entry.getValue();
}
//获取key
public K getKey()
{
return key;
}
//获取value
public V getValue()
{
return value;
}
//改变该key-value对的value值
public V setValue(V value)
{
V oldValue=this.value;
this.value=value;
return oldValue;
}
//根据key比较2个SimpleEntry是否相等
public boolean equals(Object o)
{
if (o == this)
{
return true;
}
if (o.getClass() == SimpleEntry.class)
{
SimpleEntry se=(SimpleEntry)o;
return se.getKey().equals(getKey());
}
return false;
}
//根据key计算hashCode
public int hashCode()
{
return key == null ? 0 : key.hashCode();
}
public String toString()
{
return key+"="+value;
}
}
//继承HashSet实现一个Map
public class Set2Map<K , V>
extends HashSet<SimpleEntry<K , V>>
{
//实现清空所有key-value对的方法
public void clear()
{
super.clear();
}
//判断是否包含某个key
public boolean containsKey(Object key)
{
return super.contains(
new SimpleEntry<K , V>(key ,null));
}
//判断是否包含某个value
boolean containsValue(Object value)
{
for (SimpleEntry<K , V> se : this)
{
if (se.getValue().equals(value))
{
return true;
}
}
return false;
}
//根据指定key取出对应的value
public V get(Object key)
{
for (SimpleEntry<K , V> se : this)
{
if (se.getKey().equals(key))
{
return se.getValue();
}
}
return null;
}
//将指定key-value对放入集合中
public V put(K key, V value)
{
add(new SimpleEntry<K , V>(key ,value));
return value;
}
//将另一个Map的key-value对放入该Map中
public void putAll(Map<? extends K,? extends V> m)
{
for (K key : m.keySet())
{
add(new SimpleEntry<K , V>(key , m.get(key)));
}
}
//根据指定key删除指定key-value对
public V removeEntry(Object key)
{
for (Iterator<SimpleEntry<K , V>> it=this.iterator()
; it.hasNext() ; )
{
SimpleEntry<K , V> en=(SimpleEntry<K , V>)it.next();
if (en.getKey().equals(key))
{
V v=en.getValue();
it.remove();
return v;
}
}
return null;
}
//获取该Map中包含多少个key-value对
public int size()
{
return super.size();
}
}
HashSet和HashMap之间有很多相似之处。对于HashSet而言,系统采用Hash算法决定集合元素的存储位置,这样可以保证快速存、取集合元素;对于HashMap而言,系统将value当成key的附属,系统根据Hash算法来决定key的存储位置,这样可以保证快速存、取集合key,而value总是紧随key存储。
HashMap类的put(K key , V value)方法的源代码如下:
public V put(K key, V value)
{
//如果key为null,调用putForNullKey方法进行处理
if (key == null)
return putForNullKey(value);
//根据key的keyCode计算Hash值
int hash=hash(key.hashCode());
//搜索指定hash值在对应table中的索引
int i=indexFor(hash, table.length);
//如果i索引处的Entry不为null,通过循环不断遍历e元素的下一个元素
for (Entry<K,V> e=tablei]; e != null; e=e.next)
{
Object k;
//找到指定key与需要放入的key相等(hash值相同,通过equals比较放回true)
if (e.hash == hash && ((k=e.key) == key || key.equals(k)))
{
V oldValue=e.value;
e.value=value;
e.recordAccess(this);
return oldValue;
}
}
//如果i索引处的Entry为null,表明此处还没有Entry
modCount++;
//将key、value添加到i索引处
addEntry(hash, key, value, i);
return null;
}
从上面put方法的源代码可以看出,当程序试图将一个key-value对放入HashMap中时,首先根据该key的hashCode()返回值决定该Entry的存储位置:如果两个Entry的key的hashCode()返回值相同,那它们的存储位置相同;如果这两个Entry的key通过equals比较返回true,新添加Entry的value将覆盖集合中原有Entry的value,但key不会覆盖;如果这两个Entry的key通过equals比较返回false,新添加的Entry将与集合中原有Entry形成Entry链,而且新添加的Entry位于Entry链的头部—具体说明请看addEntry()方法的说明。
当向HashMap中添加key-value对,由其key的hashCode()返回值决定该key-value对(就是Entry对象)的存储位置。当两个Entry对象的key的hashCode()返回值相同时,将由key通过equals()比较值决定是采用覆盖行为(返回true),还是产生Entry链(返回false)。
void addEntry(int hash, K key, V value, int bucketIndex)
{
//获取指定bucketIndex索引处的Entry
Entry<K,V> e=tablebucketIndex]; //①
//将新创建的Entry放入bucketIndex索引处,并让新的Entry指向原来的Entry
tablebucketIndex]=new Entry<K,V>(hash, key, value, e);
//如果Map中的key-value对的数量超过了极限
if (size++>= threshold)
//把table对象的长度扩充到2倍
resize(2 * table.length); //②
}
系统总是将新添加的Entry对象放入table数组的bucketIndex索引处。如果bucketIndex索引处已经有了一个Entry对象,新添加的Entry对象指向原有的Entry对象(产生一个Entry链);如果bucketIndex索引处没有Entry对象,也就是上面程序①行代码的e变量是null,即新放入的Entry对象指向null,就没有产生Entry链。
提示:根据上面代码可以看出,在同一个bucket存储Entry链的情况下,新放入的Entry总是位于bucket中,而最早放入该bucket中的Entry则位于这个Entry链的最末端。
上面程序中还有以下两个变量。
■ size:该变量保存了该HashMap中所包含的key-value对的数量。
■ threshold:该变量包含了HashMap能容纳的key-value对的极限,它的值等于HashMap的容量乘以负载因子(load factor)。
从上面程序中②行代码可以看出,当size++>= threshold时,HashMap会自动调用resize方法扩充HashMap的容量。每扩充一次,HashMap的容量就增大一倍。
看一下什么是负载因子:
上面程序中使用的table其实就是一个普通数组,每个数组都有一个固定的长度,这个数组的长度就是HashMap的容量。HashMap包含如下几个构造器。
■ HashMap():构建一个初始容量为16,负载因子为0.75的HashMap。
■ HashMap(int initialCapacity):构建一个初始容量为initialCapacity,负载因子为0.75的HashMap。
■ HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个HashMap。
//以指定初始化容量、负载因子创建HashMap
public HashMap(int initialCapacity, float loadFactor)
{
//初始容量不能为负数
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
//如果初始容量大于最大容量,让初始容量等于最大容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity=MAXIMUM_CAPACITY;
//负载因子必须是大于0的数值
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
//计算出大于initialCapacity的最小的2的n次方值
int capacity=1;
while (capacity < initialCapacity)
capacity <<= 1;
this.loadFactor=loadFactor;
//设置容量极限等于容量乘以负载因子
threshold=(int)(capacity * loadFactor);
//初始化table数组
table=new Entrycapacity]; //①
init();
}
上面代码中粗体字代码包含了一个简洁的代码实现:找出大于initialCapacity的、最小的2的n 次方值,并将其作为HashMap的实际容量(由 capacity变量保存)。例如,给定initialCapacity为10,那么该HashMap的实际容量就是16。
程序①行代码处可以看出,table的实质就是一个数组,一个长度为capacity的数组。
从上面代码可以看出,创建HashMap时指定的initialCapacity并不等于HashMap的实际容量。通常来说,HashMap的实际容量总比 initialCapacity 大一些,除非指定的initialCapacity参数值恰好是2的n次方。当然,掌握了HashMap容量分配的知识之后,应该在创建HashMap时将initialCapacity参数值指定为2的n次方,这样可以减少系统的计算开销。
对于HashMap及其子类而言,它们采用Hash算法来决定集合中元素的存储位置。当系统开始初始化HashMap时,系统会创建一个长度为capacity的Entry数组。这个数组里可以存储元素的位置被称为“桶(bucket)”,每个bucket 都有其指定索引,系统可以根据其索引快速访问该bucket里存储的元素。
public V get(Object key)
{
//如果key是null,调用getForNullKey取出对应的value
if (key == null)
return getForNullKey();
//根据该key的hashCode值计算它的hash码
int hash=hash(key.hashCode());
//直接取出table数组中指定索引处的值
for (Entry<K,V> e=tableindexFor(hash, table.length)];
e != null;
//搜索该Entry链的下一个Entry
e=e.next) //①
{
Object k;
//如果该Entry的key与被搜索key相同
if (e.hash == hash && ((k=e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
如果HashMap的每个bucket里只有一个Entry,HashMap可以根据索引快速地取出该bucket里的Entry。在发生“Hash冲突”的情况下,单个bucket里存储的不是一个Entry,而是一个Entry链,系统只能按顺序遍历每个Entry,直到找到想搜索的Entry为止。如果恰好要搜索的Entry位于该Entry链的最末端(该Entry最早放入该bucket中),那系统必须循环到最后才能找到该元素。
归纳起来简单地说:
- HashMap在底层将key-value对当成一个整体进行处理,这个整体就是一个Entry对象。
- HashMap底层采用一个Entry[]数组来保存所有的key-value对,当需要存储一个Entry对象时,会根据Hash算法来决定其存储位置;当需要取出一个Entry时,也会根据Hash算法找到其存储位置,直接取出该Entry。由此可见,HashMap之所以能快速存、取它所包含的Entry,完全类似于现实生活中的:不同的东西要放在不同的位置,需要时才能快速找到它。
- 当创建HashMap时,有一个默认的负载因子(load factor),其默认值为0.75。这是时间和空间成本上的一种折衷:增大负载因子可以减少Hash表(就是那个Entry数组)所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的的操作(HashMap的get()与put()方法都要用到查询);减小负载因子会提高数据查询的性能,但会增加Hash表所占用的内存空间
- HashMap时根据实际需要适当地调整load factor的值。如果程序比较关心空间开销,内存比较紧张,可以适当地增加负载因子;如果程序比较关心时间开销,内存比较宽裕,则可以适当减少负载因子。通常情况下,程序员无需改变负载因子的值。
- 如果开始就知道HashMap会保存多个key-value对,可以在创建时就使用较大的初始化容量,如果HashMap中Entry的数量一直不会超过极限容量(capacity * load factor),HashMap就无需调用resize()方法重新分配table数组,从而保证较好的性能。当然,开始就将初始容量设置太高可能会浪费空间(系统需要创建一个长度为capacity的Entry数组),因此创建HashMap时初始化容量设置也需要小心对待。
HashSet:
HashSet的绝大部分方法都是通过调用HashMap的方法来实现的,因此 HashSet和HashMap两个集合在实现本质上是相同的。
注意
由于HashSet的add()方法添加集合元素时实际上转变为调用HashMap的put()方法来添加 key-value对,当新放入 HashMap的Entry 中key 与集合中原有Entry的key 相同(hashCode()返回值相等,通过equals比较也返回true)时,新添加的Entry的value将覆盖原来Entry的value,但key不会有任何改变。因此,如果向HashSet中添加一个已经存在的元素,新添加的集合元素(底层由HashMap的key保存)不会覆盖已有的集合元素。
hashset出始化源码
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
//使用HashMap的key保存HashSet中的所有元素
private transient HashMap<E,Object> map;
//定义一个虚拟的Object对象作为HashMap的value
private static final Object PRESENT=new Object();
...
//初始化HashSet,底层会初始化一个HashMap
public HashSet()
{
map=new HashMap<E,Object>();
}
//以指定的initialCapacity、loadFactor创建HashSet
//其实就是以相应的参数创建HashMap
public HashSet(int initialCapacity, float loadFactor)
{
map=new HashMap<E,Object>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity)
{
map=new HashMap<E,Object>(initialCapacity);
}
HashSet(int initialCapacity, float loadFactor, boolean dummy)
{
map=new LinkedHashMap<E,Object>(initialCapacity, loadFactor);
}
//调用map的keySet来返回所有的key
public Iterator<E> iterator()
{
return map.keySet().iterator();
}
//调用HashMap的size()方法返回Entry的数量,得到该Set里元素的个数
public int size()
{
return map.size();
}
//调用HashMap的isEmpty()判断该HashSet是否为空
//当HashMap为空时,对应的HashSet也为空
public boolean isEmpty()
{
return map.isEmpty();
}
//调用HashMap的containsKey判断是否包含指定key
//HashSet的所有元素就是通过HashMap的key来保存的
public boolean contains(Object o)
{
return map.containsKey(o);
}
//将指定元素放入HashSet中,也就是将该元素作为key放入HashMap
public boolean add(E e)
{
return map.put(e, PRESENT) == null;
}
//调用HashMap的remove方法删除指定Entry,也就删除了HashSet中对应的元素
public boolean remove(Object o)
{
return map.remove(o)==PRESENT;
}
//调用Map的clear方法清空所有Entry,也就清空了HashSet中所有元素
public void clear()
{
map.clear();
}
...
}
HashSet判断两个对象相等的标准除了要求通过equals()方法比较返回true之外,还要求两个对象的hashCode()返回值相等。而上面程序没有重写Name类的hashCode()方法,两个Name对象的hashCode()返回值并不相同,因此HashSet会把它们当成2个对象处理,程序返回false。
由此可见,当试图把某个类的对象当成 HashMap的key,或者试图将这个类的对象放入HashSet中保存时,重写该类的equals(Object obj)方法和hashCode()方法很重要,而且这两个方法的返回值必须保持一致。当该类的两个hashCode()返回值相同时,它们通过equals()方法比较也应该返回true。通常来说,所有参与计算 hashCode()返回值的关键属性,都应该用于作为equals()比较的标准。