大家先看完有关hashcode的介绍再看看我的例子程序,麻烦大家了。这问题困扰小弟多天了。有许多人学了很长时间的Java,但一直不明白hashCode方法的作用,我来解释一下吧。首
大家先看完有关hashcode的介绍再看看我的例子程序,麻烦大家了。这问题困扰小弟多天了。
有许多人学了很长时间的Java,但一直不明白hashCode方法的作用,我来解释一下吧。
首先,想要明白hashCode的作用,你必须要先知道Java中的集合。
总的来说,Java中的集合(Collection)有两类,一类是List,再有一类是Set。你知道它们的区别吗?前者集合内的元素是有序的,元素可以重复;后者元素无序,但元素不可重复。那么这里就有一个比较严重的问题了:要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。
于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。
这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址。所以这里存在一个冲突解决的问题。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。
所以,Java对于eqauls方法和hashCode方法是这样规定的:
1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同
上面说的对象相同指的是用eqauls方法比较。
你当然可以不按要求去做了,但你会发现,相同的对象可以出现在Set集合中。同时,增加新元素的效率会大大下降。
程序:
import java.util.*;
public class TestHashCode1{
public static void main(String[] args) {
Name n1=new Name("f1","n1");
Name n2=new Name("f1","n1");
String s1=new String("w");
String s2=new String("w");
System.out.println(n1.hashCode());
System.out.println(n2.hashCode());//结果n1和n2的hashcode并不相同
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());//s1和s2的hashcode相同
System.out.println(n1.equals(n2));//true
Collection c = new HashSet();
c.add("hello");
c.add(new Name("f1","l1"));
c.add(new Integer(100));
c.remove("hello");
c.remove(new Integer(100));
System.out.println
(c.remove(new Name("f1","l1")));//false
System.out.println(c);
}
}
class Name {
private String firstName,lastName;
public Name(String firstName, String lastName) {
this.firstName = firstName; this.lastName = lastName;
}
public String toString() { return firstName + " " + lastName; }
public boolean equals(Object obj) {
if (obj instanceof Name) {
Name name = (Name) obj;
return (firstName.equals(name.firstName))
&& (lastName.equals(name.lastName));
}
return super.equals(obj);
}
//public int hashCode() {
// return firstName.hashCode();
//}
}
这程序中,开始我并没有重写Name的hashCode方法,结果如注释所示,大家可以看到n1和n2这两个对象相同(equals)但它们的hashcode不同。所以并没有删除New Name("f1","l1")。
这里就跟上面说的矛盾了
1、如果两个对象相同,那么它们的hashCode值一定要相同;
当我重写hashCode方法后,返回firstName.hashCode()强行使n1和n2的hashcode的返回值相同后,就成功删除了
New Name("f1","l1")这种方法使我不能接受!
发现一个问题,如果不重写Name的toString方法,直接打印n1的话@后面的就是n1的hashcode值的十六进制!
麻烦高手解答小弟疑惑!
不慎感激!
18 个解决方案
你自己查得很明确了。
两个不同的对象,hashcode不同。
String a = new String("abc");
String b = new String("abc");
两个不同的对象,但是值相同。
hashcode用于判断对象的地址是否相同,而不是值。
Name n1=new Name("f1","n1");
Name n2=new Name("f1","n1");
n1和n2不是两个相同的对象,只是他们的值相同。地址是不同的,所以equal相同,hashcode是不同的。
可能我没说清楚我的问题,那s1和s2也是不同的对象为什么它们的hashcode的值相同呢?
equals()相等的两个对象,hashcode()一定相等;
反过来:hashcode()不等,一定能推出equals()也不等;
hashcode()相等,equals()可能相等,也可能不等。
如果你打印一个对象,不重些toString()方法,它就会调用默认你的toString()打印这个些的类名加这个这个类名的十六十六进制。
理解存在偏差。
“要想保证元素不重复,可两个元素是否重复应该依据什么来判断呢?这就是 Object.equals方法了。但是,如果每增加一个元素就检查一次,那么当元素很多时,后添加到集合中的元素比较的次数就非常多了。也就是说,如果集合中现在已经有1000个元素,那么第1001个元素加入集合时,它就要调用1000次equals方法。这显然会大大降低效率。”
Java实现者怎么可能笨到这种地步。内部怎么实现那可是一门深奥的学问呢,设计很多高级的数据结构。否则还要用它提供的Collection干什么!自己也写一个呗。
“于是,Java采用了哈希表的原理。哈希(Hash)实际上是个人名,由于他提出一哈希算法的概念,所以就以他的名字命名了。哈希算法也称为散列算法,是将数据依特定算法直接指定到一个地址上。如果详细讲解哈希算法,那需要更多的文章篇幅,我在这里就不介绍了。初学者可以这样理解,hashCode方法实际上返回的就是对象存储的物理地址(实际可能并不是)。”
这个理解确实荒唐,按这种说法Hash在不同的电脑上对同一个字符串难道还会返回不同的值,移植性从何谈起。
赞,楼主真细心!
我举例子的时候,因为偷懒,不恰当的使用了String类。
String这个类在java中是一个很特殊的类。因为在一般情况下,大部分代码都在做字符串的操作。而同时保存很多字符串又需要消耗大量内存。实际中,我们的程序中会出现很多重复的字符串资源。
针对这样的情况,java的虚拟机实现中,对String这个类做了特殊处理。在内存中又一个部分被称作“字符串”池。程序中的字符串被保存在这个池中,值相同的字符串在池中只保留一份。这样大量的减少了字符串存储的所需空间。
也就是说:
String s1 = new String("a ");
String s2 = new String("a ");
这样的代码,实际上在内存中,只有在字符串池中,保存一个"a "这样的字符串。s1和s2是这个字符串的两个引用。
因为同一个字符串被多个引用,如果一个引用修改这个值,会导致所有引用的值都改变。这样就使错误的。为了避免这样的问题出现。java的实现中,字符串池中的字符串具有不变性。也就是说,一点字符串创立,那么他在jvm中永远不会改变。
比如:
String s1 = new String("a ");//a空格
String s2 = new String("a ");
s1=s1.trim();
这个操作实际上是生成了一个新的字符串保存s1.trim()方法的结果"a",然后将s1的引用更新为这个新字符串。
这是这个特性,也导致类s1+s2的时候,实际上是产生了一个新的字符串,保存着s1+s2的值。也就是为什么做java的程序员会被告诫用StringBuffer做字符串连接。
后头我们看你的问题。
String s1 = new String("a ");
String s2 = new String("a ");
中,s1和s2的值相同,所以equalse方法为true,因为s1和s2都是通过new获得的,s1与s2是两个类似c中指针的空间,这两个空间由new分配。所以s1与s2并不相等(==为false).
最后我们说hashCode。这两个字符串的值,实际上在同一空间,即字符串池中的"a ",hashCode代表内存地址的位置,因为在寻找这个值得时候,可以用hashCode定位,做快速的内存操作,所以如果hashCode是s1和s2两个指针的地址,那么根据他定位出来的是两个指针,不能做值得比较。而HashCode实际上保存的时最终的那个池中的"a "的地址,所以s1和s2的hashCode相同。
总结下:
s1.equals(s2);//true
s1==s2;//false
因为字符串池的原因s1.hashCode()==s2.hashCode()。
有不完整或有疑义的地方,各位补充、探讨。
感谢楼主!
感谢楼上的耐心解答!这问题越来越有意思了!希望更多的朋友进来讨论!
大家来看看n1和n2 它们equals为true但它们的hashcode不相等,然后c.remove(new Name("f1","l1"))时,它与n1是equals的,但remove方法会先比较它们的hashcode值,显然它们的值并不相等,所以返回fluse。
当我重写hashCode方法后,返回firstName.hashCode()强行使n1和n2的hashcode的返回值相同后,c.remove(new Name("f1","l1"))返回true了。
这个东西关键是弄懂内存,java中定义的类,当你new 对象的时候系统自动为其分配内存,上面的n1和n2 new了两次,所以占用两块内存,其hashcode必然不同;
java中的String类比较特殊,他和普通类的存储方式不同,他存储时自动进行优化,上面的s1和s2由于内容相同,所以占用一块内存,故其hashcode相同.
如果想看对象原始的,也就是 Object 所实现的 hashCode 的话,可以使用:
System.identityHashCode 这个方法,无论该类是否重写过 hashCode 方法都能调用。
支持7楼的回答,String是一个引用类型。
在创建对象的时候,是创建的一个引用。
而s1,s2他们的值都是一样的。所以2个对象引用的同一个文中说到的物理路径。因而他们的hashCode是相同。
1、如果两个对象相同,那么它们的hashCode值一定要相同;
2、如果两个对象的hashCode相同,它们并不一定相同
这个应该是你应该去遵守的,而不是写出了一段代码出来发现和你的预期不一致的时候,你想去推翻的。
只有你遵守了这个约定的时候,Collection和Set等数据结构才可能给你正确的结果。
很多人都把String当成一个特例,其实不是的,大家有兴趣可以去看看String类的源代码就明白了。
Object类中,hashcode方法返回的就是内存地址,所以自定义一个类,如果没重写hashcode方法,则返回的和Object对象一样,是内存地址。
对于String类来说,因为业务的需要,通常情况下大家认为字符串值相等则是相同的对象,所以在String中重写了hashcode方法,返回的是和字符串值相关的一个整型值。
对象往SET集中加入值的时候的判断,我想楼主可以突略了一个最大的问题,理解上也是有校大偏差。就是SET集的实现都是基于哈希桶的算法的。
关于哈希桶简单的原理可以描述为:比如一个HashSet开了10000这么大空间的内存区,HashSet会在使用时把这个内存区分成10个小区(假设,实际上区数是2的N次方), 要往这个HashSet里加对象的时候,首先会判断加入的对象的hashcode值,我们都知道这个值是整型的,取到了这个值后除以区数,也就是除以10,得到作数是几就加入到十个区里的编号为几的区里面。当然要从Set集里取对象也是一样,首先判断要取对象的hashcode值,然后除以10,余数是几,就到编号为几的分区里去找相应的对象。