集合作为java中非常重要的一部分,在开发中经常被用到,而通过java源码实现可以更好理解底层实现。
下面看java中的HashMap的源代码:
首先,HashMap底层是通过数组和hash表实现的,底层的元素都是entry形式,通过计算每个entry的hash值来决定把entry放到哪个位置,如果出现hash冲突,则用链表解决。
首先看entry类的代码:
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
final int hash;
/**
* entry包含的成员变量:key,value,next指向下一个节点,(在链表中使用,解决hash冲突),hash值
*/
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
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;
}
public final int hashCode() {
return (key==null ? 0 : key.hashCode()) ^
(value==null ? 0 : value.hashCode());
}
public final String toString() {
return getKey() + "=" + getValue();
}
//这个方法实际上没有实现,但是在linkedhashmap中实现了,而且有重要作用
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
}
//这个方法是用来加入元素时调用的
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);//如果key为空,则进入putForNullKey方法,下面会分析
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);//根据key的hash值找到在数组中的索引
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历这个位置的链表,如果有相同元素则覆盖
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
//否则将这个元素加入到最前面<pre name="code" class="java">
modCount++; addEntry(hash, key, value, i); return null; } 下面进入putForNullKey方法
这个方法处理key为空的情况,根据代码看table[0]存放key为空的entry,新加入的entry会覆盖value,但是链表长度为1吗,只是说把空值放在这个位置,键值不为空也有可能放在这里啊!
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);//key为0的话,hash也为0
return null;
}
下面看一个有意思的计算hash值的函数:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
static int indexFor(int h, int length) {
return h & (length-1);
}
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
即通过 hash()函数计算出hash值,再通过indexFor计算出索引,那么indexFor中,为什么是h&(length-1)呢?
下面有一个解释,是摘抄别人的:
这个我们要重点说下,我们一般对哈希表的散列很自然地会想到用hash值对length取模(即除法散列法),Hashtable中也是这样实现的,这种方法基本能保证元素在哈希表中散列的比较均匀,但取模会用到除法运算,效率很低,HashMap中则通过h&(length-1)的方法来代替取模,同样实现了均匀的散列,但效率要高很多,这也是HashMap对Hashtable的一个改进。
接下来,我们分析下为什么哈希表的容量一定要是2的整数次幂。首先,length为2的整数次幂的话,h&(length-1)就相当于对length取模,这样便保证了散列的均匀,同时也提升了效率;其次,length为2的整数次幂的话,为偶数,这样length-1为奇数,奇数的最后一位是1,这样便保证了h&(length-1)的最后一位可能为0,也可能为1(这取决于h的值),即与后的结果可能为偶数,也可能为奇数,这样便可以保证散列的均匀性,而如果length为奇数的话,很明显length-1为偶数,它的最后一位是0,这样h&(length-1)的最后一位肯定为0,即只能为偶数,这样任何hash值都只会被散列到数组的偶数下标位置上,这便浪费了近一半的空间,因此,length取2的整数次幂,是为了使不同hash值发生碰撞的概率较小,这样就能使元素在哈希表中均匀地散列。
对了,遗忘一个加载因子的参数:
其中loadFactor加载因子是表示Hsah表中元素的填满的程度.
若:加载因子越大,填满的元素越多,好处是,空间利用率高了,但:冲突的机会加大了.链表长度会越来越长,查找效率降低。
反之,加载因子越小,填满的元素越少,好处是:冲突的机会减小了,但:空间浪费多了.表中的数据将过于稀疏(很多空间还没用,就开始扩容了)
冲突的机会越大,则查找的成本越高.
因此,必须在 "冲突的机会"与"空间利用率"之间寻找一种平衡与折衷. 这种平衡与折衷本质上是数据结构中有名的"时-空"矛盾的平衡与折衷.
如果机器内存足够,并且想要提高查询速度的话可以将加载因子设置小一点;相反如果机器内存紧张,并且对查询速度没有什么要求的话可以将加载因子设置大一点。不过一般我们都不用去设置它,让它取默认值0.75就好了。
接下来我们看get()方法:
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry<K,V> e = table[indexFor(hash, table.length)];
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
private V getForNullKey() {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null)
return e.value;
}
return null;
}
上面的代码就很好理解了.
最后看一下resize()方法:
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);//赋值原来元素到新数组中,消耗性能
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
补充:
for (Entry<K,V> e = table[i]; e != null; e = e.next) {//遍历这个位置的链表,如果有相同元素则覆盖
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
我们注意到,如果hashMap 放入元素的时候,如果key值相同,那么会覆盖value值,又因为hashSet底层实际上是用hashMap实现的,hashSet是怎么保证放入的元素不会重复呢?
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
所以hashSet加入的过程同样会调用上面判断的方法判断,
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
我们主要看这个判断是否为同一个值的条件:
</pre>e.hash==hash:个人感觉很奇怪,因为上面已经是根据hash算出来的index,如果index相同,那么hash也应该相同。。。。好像不对,<p></p><p></p><pre code_snippet_id="1661232" snippet_file_name="blog_20160426_16_8743048" name="code" class="java">static int indexFor(int h, int length) {
return h & (length-1);
}
这是那么indexFor函数,如果返回的index相同,不一定hash相同,而hash是对象的代表(本来hashCode代表对象,而那个hash值又是根据hashCode算出来的,所以hash值间接代表对象(
(k=e.key)==key这里为什么用==,而不用equals?我想了一下,不一定每个对象都有equals方法,比如int类型的
key.equals(k) 这里不用多说,用equals方法判断
通过上面我们可以看出,当使用集合的增加对象时,hashCode()和equal()方法会自动调用,那么我们可以根据情况自己重写hashCode 和equals 来判断能否加进去。
那什么时候可以加进去呢?通过上面的判断条件也可以看出:
1.如果对象的hashCode不同,直接加进去。
2.如果对象的hashCode相同,再比较equals方法(如果没有equals,比较==),如果equals(或==)返回true,加不进去,否则,直接加进去。
那么equals 和 == 的区别呢?
其中一个,null 没有equals方法,所以
String a=null;
String b="b";
a.equals(b)会抛异常,而a==b可以比较
下面摘抄一段它们的异同:
java中的数据类型,可分为两类:
1.基本数据类型,也称原始数据类型。byte,short,char,int,long,float,double,boolean
他们之间的比较,应用双等号(==),比较的是他们的值。
2.复合数据类型(类)
当他们用(==)进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。 JAVA当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地 址,但在一些类库当中这个方法被覆盖掉了,如String,Integer,Date在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。
对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同。