由@Data注解进一步理解hashCode和equals方法

        最近在阅读代码时 遇到一段代码中使用了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;
}

参考链接:

https://projectlombok.org/features/EqualsAndHashCode

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值