最近在阅读代码时 遇到一段代码中使用了HashMap,其中key值为JavaBean对象,由于需要对其进行修改,为了避免书写大量的get和set方法,将该对象使用@Data注解修饰,结果却出现了奇怪的现象。
首先将对象作为HashMap的key值是没有问题的,验证如下:
创建一个JavaBean对象
public class User { private Integer userId; private String userName; public User(Integer userId, String userName) { this.userId = userId; this.userName = userName; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } @Override public String toString() { return "User{" + "userId=" + userId + ", userName='" + userName + '\'' + '}'; } }测试如下:
public static void m1() { List<User> userList = Arrays.asList( new User(1, "zhangsan"), new User(2, "lisi"), new User(3, "wanger")); Map<User, String> map = new HashMap<>(); for (User user: userList) { map.put(user, user.getUserName()); } System.out.println(map); map.forEach((k, v)->{ System.out.println(k.hashCode() + " " + k + " " + map.get(k)); }); }最终结果如下所示:可知使用javaBean对象作为HashMap的key值是可以实现正常存取的。
{User{userId=1, userName='zhangsan'}=zhangsan, User{userId=3, userName='wanger'}=wanger, User{userId=2, userName='lisi'}=lisi} 1627674070 User{userId=1, userName='zhangsan'} zhangsan 1625635731 User{userId=3, userName='wanger'} wanger 1360875712 User{userId=2, userName='lisi'} lisi
变换如下,在遍历HashMap的过程中改变对象的属性,如下所示:
public static void m2() { List<User> userList = Arrays.asList( new User(1, "zhangsan"), new User(2, "lisi"), new User(3, "wanger")); Map<User, String> map = new HashMap<>(); for (User user: userList) { map.put(user, user.getUserName()); } System.out.println(map); map.forEach((k, v)->{ System.out.println(k.hashCode()); k.setUserId(k.getUserId()+100); System.out.println(k.hashCode() + " " + k + " " + map.get(k)); }); }结果如下所示,可知尽管改变了对象的属性 但是对象的hashCode值是没有改变的。
{User{userId=3, userName='wanger'}=wanger, User{userId=1, userName='zhangsan'}=zhangsan, User{userId=2, userName='lisi'}=lisi} 1072408673 1072408673 User{userId=103, userName='wanger'} wanger 1791741888 1791741888 User{userId=101, userName='zhangsan'} zhangsan 1595428806 1595428806 User{userId=102, userName='lisi'} lisi
再次变换如下所示:将JavaBean对象使用@Data注解修饰
@Data public class User2 { private Integer userId; private String userName; public User2(Integer userId, String userName) { this.userId = userId; this.userName = userName; } }在遍历过程中改变对象的属性 可以发现其对象的hashCode值发生变化 先前的key值已经获取不到之前的value值了。
public static void m3() { List<User2> userList = Arrays.asList( new User2(1, "zhangsan"), new User2(2, "lisi"), new User2(3, "wanger")); Map<User2, String> map = new HashMap<>(); for (User2 user: userList) { map.put(user, user.getUserName()); } System.out.println(map); map.forEach((k, v)->{ System.out.println(k.hashCode() + " " + k + " " + map.get(k)); k.setUserId(k.getUserId()+100); System.out.println(k.hashCode() + " " + k + " " + map.get(k)); }); }输出如下:
{User2(userId=2, userName=lisi)=lisi, User2(userId=3, userName=wanger)=wanger, User2(userId=1, userName=zhangsan)=zhangsan} 3325602 User2(userId=2, userName=lisi) lisi 3331502 User2(userId=102, userName=lisi) null -795133894 User2(userId=3, userName=wanger) wanger -795127994 User2(userId=103, userName=wanger) null -1432601016 User2(userId=1, userName=zhangsan) zhangsan -1432595116 User2(userId=101, userName=zhangsan) null由以上可知 在使用map的过程中要注意不要改变key的hashCode 否则会产生一些莫名其妙的错误。
普通的JavaBean对象 改变其属性时不会改变其hashCode值 而使用@Data注解时hashCode值却会改变,这是为啥?
查看代码可以发现,普通的JavaBean对象hashCode和equals方法继承了Object类的方法,而Object类hashCode方法为本地方法,由具体的JVM来负责实现,也就是说一旦对象已经创建 其hashCode值已经确定 不会发生改变。
public native int hashCode(); public boolean equals(Object obj) { return (this == obj); }当对象使用@Data注解时,其hashCode和equals方法其实已经被覆写,可以将User2.class反编译,可以发现如下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.chen.normal.hashmap; public class User2 { private Integer userId; private String userName; public User2(Integer userId, String userName) { this.userId = userId; this.userName = userName; } public Integer getUserId() { return this.userId; } public String getUserName() { return this.userName; } public void setUserId(Integer userId) { this.userId = userId; } public void setUserName(String userName) { this.userName = userName; } public boolean equals(Object o) { if (o == this) { return true; } else if (!(o instanceof User2)) { return false; } else { User2 other = (User2)o; if (!other.canEqual(this)) { return false; } else { Object this$userId = this.getUserId(); Object other$userId = other.getUserId(); if (this$userId == null) { if (other$userId != null) { return false; } } else if (!this$userId.equals(other$userId)) { return false; } Object this$userName = this.getUserName(); Object other$userName = other.getUserName(); if (this$userName == null) { if (other$userName != null) { return false; } } else if (!this$userName.equals(other$userName)) { return false; } return true; } } } protected boolean canEqual(Object other) { return other instanceof User2; } public int hashCode() { int PRIME = true; int result = 1; Object $userId = this.getUserId(); int result = result * 59 + ($userId == null ? 43 : $userId.hashCode()); Object $userName = this.getUserName(); result = result * 59 + ($userName == null ? 43 : $userName.hashCode()); return result; } public String toString() { return "User2(userId=" + this.getUserId() + ", userName=" + this.getUserName() + ")"; } }可知 hashCode和equals方法已经被覆写,与具体的属性值有关,因此当对象的属性值被改变时,其hashCode值也会发生变化,在map中会出现找不到value的情况。
因此在使用普通对象作为HashMap key时要特别注意hashCode和equals方法的逻辑正确性。
查阅资料如下:
static inline intptr_t get_next_hash(Thread * Self, oop obj) { intptr_t value = 0 ; if (hashCode == 0) { // This form uses an unguarded global Park-Miller RNG, // so it's possible for two threads to race and generate the same RNG. // On MP system we'll have lots of RW access to a global, so the // mechanism induces lots of coherency traffic. value = os::random() ; } else if (hashCode == 1) { // This variation has the property of being stable (idempotent) // between STW operations. This can be useful in some of the 1-0 // synchronization schemes. intptr_t addrBits = intptr_t(obj) >> 3 ; value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ; } else if (hashCode == 2) { value = 1 ; // for sensitivity testing } else if (hashCode == 3) { value = ++GVars.hcSequence ; } else if (hashCode == 4) { value = intptr_t(obj) ; } else { // Marsaglia's xor-shift scheme with thread-specific state // This is probably the best overall implementation -- we'll // likely make this the default in future releases. unsigned t = Self->_hashStateX ; t ^= (t << 11) ; Self->_hashStateX = Self->_hashStateY ; Self->_hashStateY = Self->_hashStateZ ; Self->_hashStateZ = Self->_hashStateW ; unsigned v = Self->_hashStateW ; v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ; Self->_hashStateW = v ; value = v ; } value &= markOopDesc::hash_mask; if (value == 0) value = 0xBAD ; assert (value != markOopDesc::no_hash, "invariant") ; TEVENT (hashCode: GENERATE) ; return value; }@Data相当于@Getter @Setter @RequiredArgsConstructor @ToString @EqualsAndHashCode这5个注解的合集
@EqualsAndHashCode注解定义了hashCode和equals方法覆写逻辑 @Target({ElementType.TYPE}) @Retention(RetentionPolicy.SOURCE) public @interface EqualsAndHashCode { String[] exclude() default {}; String[] of() default {}; boolean callSuper() default false; boolean doNotUseGetters() default false; EqualsAndHashCode.AnyAnnotation[] onParam() default {}; boolean onlyExplicitlyIncluded() default false; /** @deprecated */ @Deprecated @Retention(RetentionPolicy.SOURCE) @Target({}) public @interface AnyAnnotation { } @Target({ElementType.FIELD}) @Retention(RetentionPolicy.SOURCE) public @interface Exclude { } @Target({ElementType.FIELD, ElementType.METHOD}) @Retention(RetentionPolicy.SOURCE) public @interface Include { String replaces() default ""; } }1. 此注解会生成equals(Object other) 和 hashCode()方法。 2. 它默认使用非静态,非瞬态的属性 3. 可通过参数exclude排除一些属性 4. 可通过参数of指定仅使用哪些属性 5. 它默认仅使用该类中定义的属性且不调用父类的方法 6. 可通过callSuper=true解决上一点问题。让其生成的方法中调用父类的方法。
因此可以通过避免使用@Data和@EqualsAndHashCode来避免覆写hashCode和equals方法
@Getter @Setter @ToString public class User3 { private Integer userId; private String userName; public User3(Integer userId, String userName) { this.userId = userId; this.userName = userName; } }以get方法为例:
public V get(Object key) { Node<K,V> e; return (e = getNode(hash(key), key)) == null ? null : e.value; }hash(key)逻辑
根据key的hashCode来计算hash表的下标索引
(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); first = tab[(n - 1) & hash]当定位到某一个具体的hash表索引时,再根据equals方法来判断是否相等。
do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); } final Node<K,V> getNode(int hash, Object key) { Node<K,V>[] tab; Node<K,V> first, e; int n; K k; if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) { if (first.hash == hash && // always check first node ((k = first.key) == key || (key != null && key.equals(k)))) return first; if ((e = first.next) != null) { if (first instanceof TreeNode) return ((TreeNode<K,V>)first).getTreeNode(hash, key); do { if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) return e; } while ((e = e.next) != null); } } return null; }参考链接:
https://projectlombok.org/features/EqualsAndHashCode
