java中equals和hashcode方法总结

时间:2022-05-18 22:16:49

  在java中,object是所有类的基类,它是一个具体类,主要的目的是定义一些基础的方法,共其它类调用,非final方法主要有equals、hashCode、toString,clone和finalize方法。

 

当子类要覆盖equals方法时,要遵循通用约定:

无需覆盖equals方法的情况

1、类的每一个实例本质上都是唯一的:主要针对代表实体的,如thread,非值类。

2、不关心类是否提供 逻辑相等 功能。

3、超类已经覆盖了equals方法,从超类继承的方法也使用于子类。

4、类是私有的或者是包级私有的,确定它的equals方法不会被条用。

 

需要覆盖equals方法:值类:有自己独特的“逻辑相等”,而且超类没有覆盖equals方法来实现期待的行为。

在覆盖equals方法时,要遵循的准则:

1、自反性   :对任意引用值X,x.equals(x)的返回值一定为true.
2    对称性:   对于任何引用值x,y,当且仅当y.equals(x)返回值为true时,x.equals(y)的返回值一定为true;
3    传递性:如果x.equals(y)=true, y.equals(z)=true,则x.equals(z)=true
4   一致性:如果参与比较的对象没任何改变,则对象比较的结果也不应该有任何改变
5   非空性:任何非空的引用值X,x.equals(null)的返回值一定为false

 

一般的写法是:

@Override public boolean equals(Object o){
//类型检查
if(!(o instanceof MyClass){
return false;
}
MyClass c = (MyClass)o;
//然后是成员变量的值检查
return c.id=this.id && c.name =this.name;
}


关键准则,在覆盖equals方法时,一定要覆盖hasCode方法。

因为在基于散列的集合中,标示类是基于散列值,一个类要有自己唯一的散列值,如果没有hascode方法,即使是相同的类,生成的散列值也是不同的,违反自反性。

 

一个好的hascode函数:为不相等的对象产生不相等的散列码,这样也比较困难,但是尽量平均的分散到各个散列值。

 

对一般的类而言,可以在根据各个域值在计算本类的哈希值。

大致是这样的:假如则个类有三个域:areaCode,prefix,lineNumber;

@Override public int hasCode(){
int result = 17;
result = 31*result +areaCode;
result = 31*result + prefix;
result = 31*result + lineNumber;
return result;
}

 


 如果一个类是不可变类,而且计算散列码的开销比较大的话,可以缓存散列码。


private volatile int hashCode;

@Override public int hashCode(){
int result = hashCode;
if(result == 0){
      //像上面那样重新计算散列码            }    return result;}



Cloneable接口是为了说明 这样的对象允许克隆,但是object的clone方法是受保护的。

插一个简单使用clone的例子,如果一个对象内部有可变对象实例的话,public API不应该直接返回该对象的引用,以防调用方的code改变该对象的内部状态。这个时候可以返回该对象的clone。

问题来了,什么叫一模一样。
一般来说,有
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
但是这些都不是强制的。
我们需要什么样的clone就搞出什么样的clone好了。
一般而言,我们要的clone应该是这样的。copy和原型的内容一样,但是又是彼此隔离的。即在clone之后,改变其中一个不影响另外一个。


Object的clone以及为什么如此实现

Object的clone的行为是最简单的。以堆上的内存存储解释的话(不计内务内存),对一个对象a的clone就是在堆上分配一个和a在堆上所占存储空间一样大的一块地方,然后把a的堆上内存的内容复制到这个新分配的内存空间上。
看例子。
Java代码class User {
    String name;
    int age;
}

class Account implements Cloneable {
    User user;
    long balance;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}


Java代码//user
 User user = new User(); 
 user.name = "user"; 
 user.age = 20;
 // account. 
 Account account = new Account(); 
 account.user = user; 
 account.balance = 10000; 
 // copy. 
 Account copy = (Account) account.clone(); 
  
 // balance因为是primitive,所以copy和原型是相等且独立的。 
 Assert.assertEquals(copy.balance,account.balance); 
 copy.balance =20000;
// 改变copy不影响原型。 
 Assert.assertTrue(copy.balance != account.balance); 
  
 // user因为是引用类型,所以copy和原型的引用是同一的。 
 Assert.assertTrue(copy.user == account.user); 
 copy.user.name = "newName"; 
 // 改变的是同一个东西。 
 Assert.assertEquals("newName", account.user.name); 

java中equals和hashcode方法总结


恩,默认实现是帮了我们一些忙,但是不是全部。
primitive的确做到了相等且隔离。
引用类型仅仅是复制了一下引用,copy和原型引用的东西是一样的。
这个就是所谓的浅copy了。
要实现深copy,即复制原型中对象的内存copy,而不仅仅是一个引用。只有自己动手了。
等等,是不是所有的引用类型都需要深copy呢?
不是!
我们之所以要深copy,是因为默认的实现提供的浅copy不是隔离的,换言之,改变copy的东西,会影响到原型的内部。比如例子中,改变copy的user的name,影响了原型。
如果我们要copy的类是不可变的呢,如String,没有方法可以改变它的内部状态呢。
Java代码class User implements Cloneable { String name; int age; @Override public Object clone() throws CloneNotSupportedException { return super.clone(); } }  class User implements Cloneable { 
        String name; 
        int age; 
     
        @Override 
        public Object clone() throws CloneNotSupportedException { 
            return super.clone(); 
        } 
    } 
Java代码// user.  
User user = new User();  
user.name = "user";  
user.age = 20;  
   
 // copy 
 User copy = (User) user.clone(); 
  
// age因为是primitive,所以copy和原型是相等且独立的。 
 Assert.assertEquals(copy.age, user.age); 
 copy.age = 30; 
 // 改变copy不影响原型。 
 Assert.assertTrue(copy.age != user.age); 
  
 // name因为是引用类型,所以copy和原型的引用是同一的。 
 Assert.assertTrue(copy.name == user.name); 
 // String为不可变类。没有办法可以通过对copy.name的字符串的操作改变这个字符串。 
 // 改变引用新的对象不会影响原型。 
 copy.name = "newname"; 
 Assert.assertEquals("newname", copy.name); 
 Assert.assertEquals("user", user.name); 
 可见,在考虑clone时,primitive和不可变对象类型是可以同等对待的。

java为什么如此实现clone呢?
也许有以下考虑。
1 效率和简单性,简单的copy一个对象在堆上的的内存比遍历一个对象网然后内存深copy明显效率高并且简单。
2 不给别的类强加意义。如果A实现了Cloneable,同时有一个引用指向B,如果直接复制内存进行深copy的话,意味着B在意义上也是支持Clone的,但是这个是在使用B的A中做的,B甚至都不知道。破坏了B原有的接口。
3 有可能破坏语义。如果A实现了Cloneable,同时有一个引用指向B,该B实现为单例模式,如果直接复制内存进行深copy的话,破坏了B的单例模式。
4 方便且更灵活,如果A引用一个不可变对象,则内存deep copy是一种浪费。Shadow copy给了程序员更好的灵活性。

如何clone
clone三部曲。
1 声明实现Cloneable接口。
2 调用super.clone拿到一个对象,如果父类的clone实现没有问题的话,在该对象的内存存储中,所有父类定义的field都已经clone好了,该类中的primitive和不可变类型引用也克隆好了,可变类型引用都是浅copy。
3 把浅copy的引用指向原型对象新的克隆体。
给个例子。
Java代码class User implements Cloneable {
            String name;
            int age;

            @Override
            public User clone() throws CloneNotSupportedException {
                    return (User) super.clone();
            }
    }

    class Account implements Cloneable {
            User user;
            long balance;

            @Override
            public Account clone() throws CloneNotSupportedException {
                    Account account = null;

                    account = (Account) super.clone();
                    if (user != null) {
                            account.user = user.clone();
                    }

                    return account;
            }
    }


java中equals和hashcode方法总结


对clone的态度

clone嘛,我觉得是个好东西,毕竟系统默认实现已经帮我们做了很多事情了。
但是它也是有缺点的。
1 手工维护clone的调用链。这个问题不大,程序员有责任做好。
2 如果class的field是个final的可变类,就不行了。三部曲的第三步没有办法做了。

考虑一个类对clone的态度,有如下几种。
1 公开支持:好吧,按照clone三部曲实现吧。前提是父类支持(公开或者默默)。
2 默默支持:不实现Cloneable接口,但是在类里面有正确的protected的clone实现,这样,该类不支持clone,但是它的子类如果想支持的话也不妨碍。
3 不支持:好吧,为了明确该目的,提供一个抛CloneNotSupportedException 异常的protected的clone实现。
4 看情况支持:该类内部可以保存其他类的实例,如果其他类支持则该类支持,如果其他类不支持,该类没有办法,只有不支持。


其他的选择

可以用原型构造函数,或者静态copy方法来手工制作一个对象的copy。
好处是即使class的field为final,也不会影响该方法的使用。不好的地方是所有的primitive赋值都得自己维护。


和Serializable的比较

使用Serializable同样可以做到对象的clone。但是:
Cloneable本身就是为clone设计的,虽然有一些缺点,但是如果它可以clone的话无疑用它来做clone比较合适。如果不行的话用原型构造函数,或者静态copy方法也可以。

Serializable制作clone的话,添加了太多其它的东西,增加了复杂性。
1 所有的相关的类都得支持Serializable。这个相比支持Cloneable只会工作量更大
2 Serializable添加了更多的意义,除了提供一个方法用Serializable制作Clone,该类等于也添加了其它的public API,如果一个类实现了Serializable,等于它的2进制形式就已经是其API的一部分了,不便于该类以后内部的改动。
3 当类用Serializable来实现clone时,用户如果保存了一个老版本的对象2进制,该类升级,用户用新版本的类反系列化该对象,再调用该对象用Serializable实现的clone。这里为了一个clone的方法又引入了类版本兼容性的问题。不划算。


性能

不可否认,JVM越来越快了。
但是系统默认的native实现还是挺快的。
clone一个有100个元素的int数组,用系统默认的clone比静态copy方法快2倍左右。