java_Map<T>接口下重写equals()方法和hashCode()方法(包含了我对equals()方法的一些探究)
package map_and_hash; import java.util.HashMap; import java.util.Map; import java.util.Objects; public class EqualsAndHashCode { public static void main(String[] args) { String key1 = "a"; Map<String, Integer> map = new HashMap<>(); map.put(key1, 123); /*通过new实例化一个不同于key1的新的字符串对象(尽管这个对象里内容是一样的)*/ String key2 = new String("a"); map.get(key2); // 123 System.out.println(key1 == key2); // false /*而,在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。 * 我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。 * 我们再思考一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。 通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。(当然,String类中已经实现了hashCode方法了:返回次字符串的hash码;而任何对象都有hashCode码方法(因为Object根类中就有hashCode()方法) * 因此,正确使用Map必须保证: 1.作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true; 2.作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范: 如果两个对象相等,则两个对象的hashCode()必须相等; 如果两个对象不相等,则两个对象的hashCode()尽量不要相等。 即对应两个实例a和b: 如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode(); 如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。 上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。 而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。 */ System.out.println(key1.equals(key2)); // true ///自定义类型作为key的类型 Map<Person2,Integer> map2=new HashMap<>(); //定义三个key的实例(令第三个实例和第一个实例在内容上相等) Person2 t1=new Person2("Xiao","rr",55), t2=new Person2("Xiao","Hong",66), t3=new Person2("Xiao", "rr", 55); // System.out.println("t1==t3:"+t1.equals(t3)); //Object obj=new Object();obj.equals(5);//说明基本类型也可以传递个Object类(或许是先提升为Integer,然后在执行?) map2.put(t1 ,77); map2.put(t2 ,88); //map2.put(t3,879); System.out.println(map2.get(t1));/*map2.get("a")返回一个Person2类型的对象;然而为了打印出对象里的信息,应当重写toString方法*/ System.out.println(map2.get(t2)); System.out.println(map2.get(t3));//可以验证,在t3实例不放入map2映射中时,用t3也可以取得key为t1实例时所对应的value值(77) /*此外,就算我们插入的实例都时不一样的,我们,key类中的hashCode方法计算的hash值也任然有可能是一样的(作为key的实例相同的时候那肯定是相同的),总之两个key实例的hash * 相同,那么就会产生hash冲突,引发List的隐式创建来存储这些映射值。 * 如果不重写eauals方法,那么使用t3就无法取得有t2映射的value值了; */ //尝试更新某对key-value: map2.put(t3,314); System.out.println("用t3去影响t1所映射的value:" + map2.get(t1));//成功用t3去更新了t1->value; /*使用Map的时候,只要key不相同,它们映射的value就互不干扰。 但是,在HashMap内部,确实可能存在不同的key,映射到相同的hashCode(),即相同的数组索引上 假设情况:"a"和"b"这两个key最终计算出的索引都是 5,那么,在HashMap的数组中,实际存储的不是一个Person实例,而是一个List,它包含两个Entry,一个是"a"的映射,一个是"b"的映射: ┌───┐ 0 │ │ ├───┤ 1 │ │ ├───┤ 2 │ │ ├───┤ 3 │ │ ├───┤ 4 │ │ ├───┤ 5 │ ●─┼───> List<Entry<String, Person>>//这是说,这两个list存储的元素是都是Entry<String,Person>类型的(对于List<>而言,这是泛型的二重嵌套了;Entry<K,V>(记录的类型是包含字段类型情况:K->String和V->Person)其中public static 接口 Map.Entry<K, V> 详细API: https://www.apiref.com/java11-zh/java.base/java/util/Map.Entry.html ├───┤ 6 │ │ ├───┤ 7 │ │ └───┘ 在查找的时候,例如: Person p = map.get("a"); HashMap 内部通过"a"找到的实际上是List<Entry<String, Person>>,它还需要遍历这个List,并找到一个Entry(记录/元素),它的key字段是"a",才能返回对应的Person实例。 我们把不同的key具有相同的hashCode()的情况称之为哈希冲突。 在冲突的时候,一种最简单的解决办法是用List存储hashCode()相同的key-value。显然,如果冲突的概率越大,这个List就越长,Map的get()方法效率就越低,这就是为什么要尽量满足条件:如果两个对象不相等,则两个对象的 hashCode() 尽量不要相等。 hashCode()方法编写得越好,HashMap工作的效率就越高。 小结 要正确使用HashMap,作为key的类必须正确覆写equals()和hashCode()方法; 一个类如果覆写了equals(),就必须覆写hashCode(),并且覆写规则是: 如果equals()返回true,则hashCode()返回值必须相等; 技巧:实现equals()方法可以通过Objects.equals()辅助方法来实现; 如果equals()返回false,则hashCode()返回值尽量不要相等。 技巧:实现hashCode()方法可以通过Objects.hashCode()辅助方法实现。 Note: java.util.Objects public final class Objects extends Object 此类包含static实用程序方法,用于操作对象或在操作前检查某些条件。 这些实用程序包括null或null方法,用于计算对象的哈希代码,返回对象的字符串,比较两个对象,以及检查索引或子范围值是否超出范围。 */ } } class Person2 { String firstName; String lastName; int age; public Person2(String firstName,String lastName,int age){ this.firstName=firstName; this.lastName=lastName; this.age=age; } /* @Override public int hashCode() { int h = 0; h = 31 * h + firstName.hashCode(); //实现equals()方法遇到的问题类似,如果firstName或lastName为null,上述代码工作起来就会抛NullPointerException。 h = 31 * h + lastName.hashCode(); h = 31 * h + age; return h; }*/ /*注意到String类已经正确实现了hashCode()方法,我们在计算Person的hashCode()时,反复使用31*h,这样做的目的是为了尽量把不同的Person实例的hashCode()均匀分布到整个int范围。 和实现equals()方法遇到的问题类似,如果firstName或lastName为null,上述代码工作起来就会抛NullPointerException。 为了解决这个问题,我们在计算hashCode()的时候,经常借助Objects.hash()来计算: public static int hash(Object... values) 为一系列输入值生成哈希码。 生成哈希码,如同所有输入值都放在一个数组中,并通过调用Arrays.hashCode(Object[])对该数组进行哈希处理 。此方法对于在包含多个字段的对象上实现Object.hashCode()非常有用。 例如,如果有三个字段,对象x , y ,和z ,一个可以这样写: @Override public int hashCode() { return Objects.hash(x, y, z); } */ /* 作为key的对象的类必须正确覆写equals()方法。 回顾下:HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引, 否则,相同的key每次取出的value就不一定对。 通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。(注意我们是为充当key的对象的对应类编写hashCode()方法,而key选用String对象时,由于String内部已经实现了正确的hashCode(),所以不需要我们在重写的.而当key的类型是我们自己编写的时候,就务必重写hashCode()乃至equals()这两个方法; HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。*/ @Override//借助.hash()方法类重写hashCode(); 通过调试可以观察到,执行put()函数的时候回调用hashCode函数 public int hashCode() { return Objects.hash(firstName, lastName, age); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person2 person2 = (Person2) o; return age == person2.age && Objects.equals(firstName, person2.firstName) && Objects.equals(lastName, person2.lastName);/*在比较这两个String类型字段大的时候,遗憾的无法看到String中的eauals()是zen‘me怎么运行的;吃了不少苦头; 不过在另一个类中(equal3.java)的调试中,确实成功步入到String的equals()方法的运行;到底在什么情况下可以步入呢?*/ /*发现: * public static boolean equals(Object a, Object b) { return (a == b) || (a != null && a.equals(b));//如果a和b是两个String,那(a==b为false);而或上后面那一部分(a!=null&&a.equals(b))引发object.equals()方法的调用: * public boolean equals(Object obj) { return (this == obj);//函数体就这一个语句;(虽然如此,但考虑到 多态,equals的具体实现未必就是(往往)不是调用根类的那个比较地址的equals()方法;比如传入的obj可能是Object的子类对象String,那这个时候equals就看String类是怎么实现它的了(String中的equals()用到某些宏和属性(coder); } * public boolean equals(Object obj) 指示某个其他对象是否“等于”此对象。 equals方法在非null对象引用上实现等价关系: 自反性 :对于任何非空的参考值x , x.equals(x)应该返回true 。 它是对称的 :对于任何非空引用值x和y , x.equals(y)应该返回true当且仅当y.equals(x)回报true 。 传递性 :对于任何非空引用值x , y和z ,如果x.equals(y)回报true个y.equals(z)回报true ,然后x.equals(z)应该返回true 。 * 它是一致的 :对于任何非空引用值x和y ,多次调用x.equals(y)始终返回true或始终返回false ,前提是未修改对象上的equals比较中使用的信息。 对于任何非空的参考值x , x.equals(null)应该返回false 。 类Object的equals方法实现了对象上最具区别的可能等价关系; 也就是说,对于任何非空引用值x和y ,当且仅当x和y引用同一对象( x == y具有值true )时,此方法返回true 。 请注意,通常需要在重写此方法时覆盖hashCode方法,以便维护hashCode方法的常规协定,该方法声明相等对象必须具有相等的哈希代码。 参数 obj - 要与之比较的参考对象。 结果 true如果此对象与obj参数相同; 否则为false 。 }*/ } @Override public String toString() { return "Person2{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + ", age=" + age + '}'; } } /*编写 equals 和 hashCode - 廖雪峰的官方网站 小白的零基础 Java 教程,从入门到顶级架构师! 我们知道 Map 是一种键 - 值(key-value)映射表,可以通过 key 快速查找对应的 value。 以 HashMap 为例,观察下面的代码: Map<String, Person> map = new HashMap<>(); map.put("a", new Person("Xiao Ming")); map.put("b", new Person("Xiao Hong")); map.put("c", new Person("Xiao Jun")); map.get("a"); map.get("x"); HashMap之所以能根据key直接拿到value,原因是它内部通过空间换时间的方法,用一个大数组存储所有value,并根据 key 直接计算出value应该存储在哪个索引: ┌───┐ 0 │ │ ├───┤ 1 │ ●─┼───> Person("Xiao Ming") ├───┤ 2 │ │ ├───┤ 3 │ │ ├───┤ 4 │ │ ├───┤ 5 │ ●─┼───> Person("Xiao Hong") ├───┤ 6 │ ●─┼───> Person("Xiao Jun") ├───┤ 7 │ │ └───┘ 如果key的值为"a",计算得到的索引总是1,因此返回value为Person("Xiao Ming"),如果key的值为"b",计算得到的索引总是5,因此返回value为Person("Xiao Hong"),这样,就不必遍历整个数组,即可直接读取key对应的value。 当我们使用key存取value的时候,就会引出一个问题: 我们放入Map的key是字符串"a",但是,当我们获取Map的value时,传入的变量不一定就是放入的那个key对象。 换句话讲,两个key应该是内容相同,但不一定是同一个对象。测试代码如下: 因为在Map的内部,对key做比较是通过equals()实现的,这一点和List查找元素需要正确覆写equals()是一样的,即正确使用Map必须保证:作为key的对象必须正确覆写equals()方法。 我们经常使用String作为key,因为String已经正确覆写了equals()方法。但如果我们放入的key是一个自己写的类,就必须保证正确覆写了equals()方法。 我们再思考一下HashMap为什么能通过key直接计算出value存储的索引。相同的key对象(使用equals()判断时返回true)必须要计算出相同的索引,否则,相同的key每次取出的value就不一定对。 通过key计算索引的方式就是调用key对象的hashCode()方法,它返回一个int整数。HashMap正是通过这个方法直接定位key对应的value的索引,继而直接返回value。 因此,正确使用Map必须保证: 1.作为key的对象必须正确覆写equals()方法,相等的两个key实例调用equals()必须返回true; 2.作为key的对象还必须正确覆写hashCode()方法,且hashCode()方法要严格遵循以下规范: 如果两个对象相等,则两个对象的hashCode()必须相等; 如果两个对象不相等,则两个对象的hashCode()尽量不要相等。 即对应两个实例a和b: 如果a和b相等,那么a.equals(b)一定为true,则a.hashCode()必须等于b.hashCode(); 如果a和b不相等,那么a.equals(b)一定为false,则a.hashCode()和b.hashCode()尽量不要相等。 上述第一条规范是正确性,必须保证实现,否则HashMap不能正常工作。 而第二条如果尽量满足,则可以保证查询效率,因为不同的对象,如果返回相同的hashCode(),会造成Map内部存储冲突,使存取的效率下降。 我们还需要正确实现hashCode(),即上述 3 个字段分别相同的实例,hashCode()返回的int必须相同: 编写equals()和hashCode()遵循的原则是: equals()用到的用于比较的每一个字段,都必须在hashCode()中用于计算;equals()中没有使用到的字段,绝不可放在hashCode()中计算。 另外注意,对于放入HashMap的value对象,没有任何要求。 延伸阅读 既然HashMap内部使用了数组,通过计算key的hashCode()直接定位value所在的索引,那么第一个问题来了:hashCode() 返回的int范围高达 ±21 亿,先不考虑负数,HashMap内部使用的数组得有多大? 实际上HashMap初始化时默认的数组大小只有 16,任何key,无论它的hashCode()有多大,都可以简单地通过: int index = key.hashCode() & 0xf; 把索引确定在 0~15,即永远不会超出数组范围,上述算法只是一种最简单的实现。 第二个问题:如果添加超过 16 个key-value到HashMap,数组不够用了怎么办? 添加超过一定数量的key-value时,HashMap会在内部自动扩容,每次扩容一倍,即长度为 16 的数组扩展为长度 32,相应地,需要重新确定hashCode()计算的索引位置。例如,对长度为 32 的数组计算hashCode()对应的索引,计算方式要改为: int index = key.hashCode() & 0x1f; 由于扩容会导致重新分布已有的key-value,所以,频繁扩容对HashMap的性能影响很大。如果我们确定要使用一个容量为10000个key-value的HashMap,更好的方式是创建HashMap时就指定容量: Map<String, Integer> map = new HashMap<>(10000); 虽然指定容量是10000,但HashMap内部的数组长度总是 2n,因此,实际数组长度被初始化为比10000大的16384(214)。 最后一个问题:如果不同的两个key,例如"a"和"b",它们的hashCode()恰好是相同的(这种情况是完全可能的,因为不相等的两个实例,只要求hashCode()尽量不相等),那么,当我们放入: map.put("a", new Person("Xiao Ming")); map.put("b", new Person("Xiao Hong")); 时,由于计算出的数组索引相同,后面放入的"Xiao Hong"会不会把"Xiao Ming"覆盖了?当然不会! */
附上equal3.java代码
package study.equals; import java.util.List; import java.util.Objects; /*自反性/对称性是针对某种"关系"而言,比如像这样的说法:关系R具有自反性*/ /*如何正确编写(重写)equals()方法?equals()方法要求我们必须满足以下条件: 自反性(Reflexive):对于非null的x来说,x.equals(x)必须返回true; 对称性(Symmetric):对于非null的x和y来说,如果x.equals(y)为true,则y.equals(x)也必须为true; 传递性(Transitive):对于非null的x、y和z来说,如果x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)也必须为true; 一致性(Consistent):对于非null的x和y来说,只要x和y状态不变,则x.equals(y)总是一致地返回true或者false; 对null的比较:即x.equals(null)永远返回false。*/ public class equal3 { /*判断相等关系时,有两类: * 对于引用字段比较,我们使用equals(),(当然可以类似的达到像cpp的重载运算符的效果,只是cpp直接用符号,而java是用一个标识符(其实cpp也可以只用标识符,用符号只是形象些,但不必须) * 对于基本类型字段的比较,我们使用== 来做相等性判断*/ /*java.lang.Object @Contract(value = "null -> false", pure = true) public boolean equals(Object obj) 可见,equals()是继承自根类Object的方法,就是说任何类都有equals方法;但是我们往往需要自己重写这个重要方法:*/ public static void main(String[] args) { List<Person1> list = List.of( new Person1("Xiao", "Ming", 18), new Person1("Xiao", "Hong", 25), new Person1("Bob", "Smith", 20) ); boolean exist = list.contains(new Person1("Bob", "Smith", 20)); System.out.println(exist ? "测试成功!" : "测试失败!"); } } class Person1 { String firstName; String lastName; int age; /*constructor*/ public Person1(String firstName, String lastName, int age) { this.firstName = firstName; this.lastName = lastName; this.age = age; } /*override equals*/ /**/ public boolean equals(Object o) { /*注意equals()的参数类型一定要是Object(引用类型的根类),也就是说不能用equals来比较基础类型; 对于基础类型的比较,equals这是多余的,也是不必要的,用"=="足矣; 再次强调下,String类不是基础类型,是正儿八经的对象,尽量用equals来做相等性判断*/ if (o instanceof Person1) { /*如果传入的对象是Person1,那么放心的将对象o强制转为Person1,赋值给Person1*/ Person1 p = (Person1) o; /*object类中的equals已经处理好在引用类型String/Integer/基础类型..的(自反性/对称性/..等要求) * 即,要简化引用类型的比较,我们使用Objects.equals()静态方法: * java.util.Objects @Contract(value = "null, !null -> false; !null, null -> false", pure = true) public static boolean equals(@Nullable Object a,Object b); * 注意静态方法的调用方式(类名.静态方法名(必要的参数,(一般比普通方法要多传入一个参数))*/ return Objects.equals(this.firstName, p.firstName) && Objects.equals(this.lastName, p.lastName) && this.age == p.age; } return false; } } /*equals()遇到null时: public class Person { public String name; public int age; } public boolean equals(Object o) { if (o instanceof Person) { Person p = (Person) o; return this.name.equals(p.name) && this.age == p.age; } return false; } 如果this.name为null,那么equals()方法会报错,因为null为空引用,是一个特殊值,不是某种对象(实例),更加不会提供/支持任何方法(通过null调用方法是荒谬的), 应该先让this.name和null先做判断; 当然,null可以作为参数传入个其他(对象)的方法; 因此,需要继续改写如下: public boolean equals(Object o) { if (o instanceof Person) { Person p = (Person) o; boolean nameEquals = false; //null和null的等与不等: //对象A==null同时对象B==null,那么null作为桥梁,间接表示A和B相等(在equals()的实现中体现为返回true) if (this.name == null && p.name == null) { nameEquals = true; } if (this.name != null) { nameEquals = this.name.equals(p.name); } return nameEquals && this.age == p.age;//出口1 } return false;//出口2; }*/
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了