//类ReflectTest2
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Properties;
public class ReflectTest2 {
/**
* @param args
*/
public static void main(String[] args) throws Exception{
Collection collections = new HashSet();
ReflectPoint pt1 = new ReflectPoint(3,3);
ReflectPoint pt2 = new ReflectPoint(5,5);
ReflectPoint pt3 = new ReflectPoint(3,3);
collections.add(pt1);
collections.add(pt2);
collections.add(pt3);
collections.add(pt1);
System.out.println("修改移除前集合内容:"+collections);
System.out.println("修改移除前pt1的hashcode: "+pt1.hashCode());
System.out.println("修改移除前pt1的内容:"+pt1);
pt1.y = 7;
System.out.println("-----------------------我是分割线---------------------------");
System.out.println("修改移除后pt1的内容:"+pt1);
System.out.println("修改移除后pt1的hashcode:"+pt1.hashCode());
System.out.println("remove的返回值:"+collections.remove(pt1));;
System.out.println("修改移除后集合内容:"+collections);
System.out.println("修改移除后pt1的hashcode:"+pt1.hashCode());
System.out.println("修改移除后pt1的内容:"+pt1);
}
}
//类ReflectPoint
import java.util.Date;
public class ReflectPoint {
private Date birthday = new Date();
private int x;
public int y;
public String str1 = "ball";
public String str2 = "basketball";
public String str3 = "itcast";
public ReflectPoint(int x, int y) {
super();
this.x = x;
this.y = y;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + x;
result = prime * result + y;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final ReflectPoint other = (ReflectPoint) obj;
if (x != other.x)
return false;
if (y != other.y)
return false;
return true;
}
@Override
public String toString(){
return "x=" + x + " y=" + y;
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return y;
}
public void setY(int y) {
this.y = y;
}
public Date getBirthday() {
return birthday;
}
public void setBirthday(Date birthday) {
this.birthday = birthday;
}
}
输出结果为:
修改移除前集合内容:[x=3 y=3, x=5 y=5]
修改移除前pt1的hashcode: 1057
修改移除前pt1的内容:x=3 y=3
-----------------------我是分割线---------------------------
修改移除后pt1的内容:x=3 y=7
修改移除后pt1的hashcode:1061
remove的返回值:false
修改移除后集合内容:[x=3 y=7, x=5 y=5]
修改移除后pt1的hashcode:1061
修改移除后pt1的内容:x=3 y=7
可以看出,对HashSet中的对象pt1中与计算hashCode有关的属性进行修改后,再移除pt1会导致移除失败。从而造成内存泄露。
我们为ReflectPoint对象添加一个public属性 z,在重写hashCode方法时,使z不相关。这样在主程序中发现,无论怎样修改z的大小,都不会影响对象的移除。代码省略。
直觉上讲,修改对象属性内容,是不应该影响对象的移除的,那么到底是怎么回事呢?经过上部的实验,认为问题是因为该属性与hashCode方法关联。
那么下面,我们来查看Hashset类的remove方法的源代码,来试图发现问题:(jdk1.6)
//
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
我们发现,HashSet是借助HashMap来实现的,只用到了map的key属性。代码中的PRESENT即是map的value属性存放的内容。
那么我们继续查看HashMap的remove方法的代码:
//
/**
* Removes the mapping for the specified key from this map if present.
*
* @param key key whose mapping is to be removed from the map
* @return the previous value associated with <tt>key</tt>, or
* <tt>null</tt> if there was no mapping for <tt>key</tt>.
* (A <tt>null</tt> return can also indicate that the map
* previously associated <tt>null</tt> with <tt>key</tt>.)
*/
public V remove(Object key) {
Entry<K,V> e = removeEntryForKey(key);
return (e == null ? null : e.value);
}
/**
* Removes and returns the entry associated with the specified key
* in the HashMap. Returns null if the HashMap contains no mapping
* for this key.
*/
final Entry<K,V> removeEntryForKey(Object key) {
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;
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;
}
可以看出remove方法是根据removeEntryForKey方法用key返回一个Entry e。然后根据e是否为空决定是返回null还是返回e.value.
下面看removeEntryForKey方法,参数是key。第一行 int hash=(key==null)?0:hash(key.hashCode());可以看出在HashMap中对象的hash码是通过对对象的hashCode进行hash得到。这样我们就发现问题了,因为我们修改了与hashcode相关的对象的属性,那么对象的hashcode同样也进行了改变,那么在这里获得的hash值也改变了。而在HashMap是用拉链法来实现的,通过key值来寻找元素的过程是:通过计算元素的hash值来找到一个链表,该条链表上的所有元素的hash值都是相等的,遍历该链表,如果有就返回true,如果没有就返回false。所以hashcode的改变导致了hash值的改变,程序会找到另一条链表,遍历后肯定无法发现要找的对象。
那么HashMap的remove方法就找不到相应的对象,所以就返回为null,并且移除失败了。然后HashSet的remove方法就会返回false。
到这里可能还是比较迷惑,那么我们看下HashMap的实现机制。下面是转载大神egg 的一篇文章中的部分内容。
一、HashMap的内部存储结构
Java中数据存储方式最底层的两种结构,一种是数组,另一种就是链表,数组的特点:连续空间,寻址迅速,但是在删除或者添加元素的时候需要有较大幅度的移动,所以查询速度快,增删较慢。而链表正好相反,由于空间不连续,寻址困难,增删元素只需修改指针,所以查询慢、增删快。有没有一种数据结构来综合一下数组和链表,以便发挥他们各自的优势?答案是肯定的!就是:哈希表。哈希表具有较快(常量级)的查询速度,及相对较快的增删速度,所以很适合在海量数据的环境中使用。一般实现哈希表的方法采用“拉链法”,我们可以理解为“链表的数组”,如下图:
从上图中,我们可以发现哈希表是由数组+链表组成的,一个长度为16的数组中,每个元素存储的是一个链表的头结点。那么这些元素是按照什么样的规则存储到数组中呢。一般情况是通过hash(key)%len获得,也就是元素的key的哈希值对数组长度取模得到。比如上述哈希表中,12%16=12,28%16=12,108%16=12,140%16=12。所以12、28、108以及140都存储在数组下标为12的位置。它的内部其实是用一个Entity数组来实现的,属性有key、value、next。接下来我会从初始化阶段详细的讲解HashMap的内部结构。
最后我们知道了,HashSet中的元素,不能修改其与hashCode方法相关的属性。如修改后就可能会移除失败,导致内存泄露。
参考文献: