最近在阅读代码时 遇到一段代码中使用了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方法的逻辑正确性。
Object类中hashCode实现原理
查阅资料如下:
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覆写hashCode和equals方法原理
@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;
}
}
HashMap中hashCode和equals使用
以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;
}
参考链接: