==和equals()、hashcode()+hashmap、hashset相关
== 和 equals() 的区别
·==
对于基本类型和引用类型的作用效果是不同的:
对于基本数据类型来说,==
比较的是值。
对于引用数据类型来说,==
比较的是对象的内存地址。
因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。
·equals()
不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等(所有整型包装类型对象之间值的比较都用equals())。equals()
方法存在于Object
类中,而Object
类是所有类的直接或间接父类,因此所有的类都有equals()
方法。
equals() 方法存在两种使用情况:
1.类没有重写 equals()方法 :通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 Object类equals()方法。
2.类重写了 equals()方法 :一般我们都重写equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。
String
中的 equals
方法是被重写过的,因为 Object
的 equals
方法是比较对象的内存地址,而 String
的 equals
方法比较的是对象的值。
为什么重写 equals() 时必须重写 hashCode() 方法?
按两种情况:
第一种,不会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类:这种情况下,equals() 用来比较该类的两个对象是否相等。而hashCode() 则根本没有任何作用。
第二种,我们会在HashSet, Hashtable, HashMap等等这些本质是散列表的数据结构中,用到该类:
由图可知:相同对象必然导致相同哈希值;不同哈希值必然是由不同对象导致
hashCode方法实际上必须要完成的一件事情就是,为该equals方法认定为相同的对象返回相同的哈希值。
Object类中的equals方法区分两个对象的做法是比较地址值,即使用“==”。而我们如若根据业务需求改写了equals方法的实现,那么也应当同时改写hashCode方法的实现。否则hashCode方法依然返回的是依据Object类中的依据地址值得到的integer哈希值。
带入String类的例子:
public class StringDemo { public static void main(String[] args) { String str1 = "www.jpc.com"; String str2 = new String("www.jpc.com"); System.out.println(str1.equals(str2)); //结果为true HashMap<String,Integer> map=new HashMap<>(); map.put(str1, 111); map.put(str2, 222); map.get(str1); // 222 } }
//String类重写equals() public boolean equals(Object anObject) { if (this == anObject) {//比较地址 return true; } if (anObject instanceof String) { String anotherString = (String)anObject; int n = value.length; if (n == anotherString.value.length) {//比较长度 char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i])//比较每个字符 return false; i++; } return true; } } return false; }
String对象在调用equals方法比较另一个对象时,会有两种情况返回true:(1)地址相同(2)长度和每个字符都相等
如果不重写hashCode(),会发生:对于两个字符串对象,使用他们各自的地址值映射为哈希值。被因为它们的地址值不同,String类中的equals方法认定为相等的两个对象拥有两个不同的哈希值。
所以有啥问题呢?对开发到底有啥影响呢?为什么要保证“equals方法认定为相同的两个对象拥有相同的哈希值”这个hashCode()重写原则?
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; // table未初始化或者长度为0,进行扩容 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); // 桶中已经存在元素(处理hash冲突) else { Node<K,V> e; K k; // 判断table[i]中的元素是否与插入的key一样,若相同那就直接使用插入的值p替换掉旧的值e。 if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k)))) e = p; // 判断插入的是否是红黑树节点 else if (p instanceof TreeNode) // 放入树中 e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); // 不是红黑树节点则说明为链表结点 else { // 在链表最末插入结点 for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); // 结点数量达到阈值(默认为 8 ),执行 treeifyBin 方法 if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } // 判断链表中结点的key值与插入的元素的key值是否相等 if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; // 用于遍历桶中的链表,与前面的e = p.next组合,可以遍历链表 p = e; } } // 表示在桶中找到key值、hash值与插入元素相等的结点 if (e != null) { // 记录e的value V oldValue = e.value; // onlyIfAbsent为false或者旧值为null if (!onlyIfAbsent || oldValue == null) //用新值替换旧值 e.value = value; afterNodeAccess(e); return oldValue; } } }
putVal方法中,pink代码处,为了证明两个对象是同一对象,我们要求(二者哈希值相等)且(二者地址值相等或调用equals认定相等),如果不重写hashCode方法,则str1和str2可以满足equals相等,但是哈希值是不同的,则查不到str1已存在,p节点始终为null,此时就会创建新的节点,就相当于执行了map.put("str1","111"),map.put("str2","222"),而不是用map.put("str1","222")替换map.put("str1","111")。这样map.get(str1)得到的还是111,HashMap就乱套了。
//String类中重写hashCode() public int hashCode() { int h = hash; //默认0 if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
在类中equals()和hashCode()重写思路:
equals()方法重写的思路:先判断二者的地址是否一样,若一样,则为同一对象,必然相同;再判断是否是相同类或其子类,如果不是那直接判false;再判断长度或具体的属性值是否相同;
@Override
public boolean equals(Object obj) {
if (this == obj) return true; // 同一引用
if (obj == null || getClass() != obj.getClass()) return false; // 空或者类型不同
Person person = (Person) obj; // 强制转换
return age == person.age && Objects.equals(name, person.name); // 比较字段
}
hashcode()方法重写的思路:按照equals( )中比较两个对象是否一致的条件用到的属性来重写hashCode()。
@Override
public int hashCode() {
int result = 17; // 任意一个非零常数
result = 31 * result + name.hashCode(); // 字符串类型调用自身hashCode方法
result = 31 * result + age; // 基本类型直接使用值
return result;
}
//任何数n*31都可以被jvm优化为(n<<5)-n,移位和减法的操作效率比乘法的操作效率高很多
HashSet 如何检查重复?
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的hashcode
值作比较,如果没有相符的hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashcode
值的对象,这时会调用equals()
方法来检查hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。
在 JDK1.8 中,HashSet
的add()
方法只是简单的调用了HashMap
的put()
方法,并且判断了一下返回值以确保是否有重复元素。直接看一下HashSet
中的源码:
// Returns: true if this set did not already contain the specified element // 返回值:当 set 中没有包含 add 的元素时返回真 public boolean add(E e) { return map.put(e, PRESENT)==null; }
而在HashMap
的putVal()
方法中也能看到如下说明:
// Returns : previous value, or null if none // 返回值:如果插入位置没有元素返回null,否则返回上一个元素 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {...}
也就是说,在 JDK1.8 中,实际上无论HashSet
中是否已经存在了某元素,HashSet
都会直接插入,只是会在add()
方法的返回值处告诉我们插入前是否存在相同元素。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)