Effective Java学习笔记-Chapter3-关于Object的通用方法

第 10 条:覆盖 equals 时请遵守通用约定
默认情况:
类的每个实例本质上都是唯一的。也就是说,相同的指针(地址)指向的对象才相等。
默认实现如下:

public boolean equals(Object obj) {
    return (this == obj);
}

需要覆盖的情况:
如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖 equals
注:
结合工作的实际情况,从数据库里查询得到的实体bean,如ID或几个关键属性相等,即可认为两个对象相等。此时要特别警惕父类的equals方法,如果父类进行了覆盖,除非对其逻辑与改动影响完全理解,否则不要轻易覆盖!

equals 方法实现了等价关系,其属性如下:
自反性( reflexive):对于任何非 null x 的引用值x.equals(x)必须返回 true(我就是我)
对称性( symmetric):对于任何非 null 的引用值 x 和 y ,当且仅当 y.equals(x)返回 true 时,x.equals(y)必须返回 true 。(x=y → y=x)
传递性( transitive):对于任何非 null 的引用值 x 、y 和 z ,如果 x.equals(y )返回true ,并且 y.equals(z)也返回 true ,那么 x.equals(z)也必须返回 true (x=y,y=z → x=z)
一致性( consistent ):对于任何非 null 的 引用值 x 和 y ,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y)就会一致地返回 true,或者一致地返回 false(今天成立,明天也成立)
对于任何非 null 的引用值 x, x.equals (null)必须返回 false(与空值比较一定不成立)
总结:equals方法的覆盖要符合最基本的数理逻辑!

结合所有这些要求,得出了以下实现高质量 equals 方法的诀窍 :
1.使用==操作符检查“参数是否为这个对象的引用” 。 如果是,则返回 true 。 这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。
2.使用 instanceof 操作符检查“参数是否为正确的类型” 。 如果不是,则返回 false一般说来,所谓“正确的类型”是指 equals 方法所在的那个类 。
3.把参数转换成正确的类型。因为转换之前进行过且 stanceof 测试,所以确保会成功 。
4.对于该类中的每个“关键”( significant )域,检查参数中的域是否与该对象中对应的域相匹配。如果这些测试全部成功,则返回 true;否则返回 false 。
正确示例:借助于使用Google开源的AutoValue框架,或IDE自动生成equals方法,生成正确的equals代码。

public class Database {
    private String id;   
    private String systemId;
    private String parentId;
    private String nodeType;
    private String name;
    private String ip;
    private Integer port;
    private String dbType;
    private String identify;
    private String username;
    private String password;
    private String rootId;
    private String targetTable; 
    private String needRefresh;

   @Override
   public boolean equals(Object o) {
       // 规则1
      if (this == o) return true; 
      // 规则2
      if (!(o instanceof Database)) return false;
      // 规则3
      Database database = (Database) o;
      // 规则4
      return Objects.equals(getId(), database.getId()) &&
            Objects.equals(getSystemId(), database.getSystemId()) &&
            Objects.equals(getParentId(), database.getParentId()) &&
            Objects.equals(getNodeType(), database.getNodeType()) &&
            Objects.equals(getName(), database.getName()) &&
            Objects.equals(getIp(), database.getIp()) &&
            Objects.equals(getPort(), database.getPort()) &&
            Objects.equals(getDbType(), database.getDbType()) &&
            Objects.equals(getIdentify(), database.getIdentify()) &&
            Objects.equals(getUsername(), database.getUsername()) &&
            Objects.equals(getPassword(), database.getPassword()) &&
            Objects.equals(getRootId(), database.getRootId()) &&
            Objects.equals(getTargetTable(), database.getTargetTable()) &&
            Objects.equals(getNeedRefresh(), database.getNeedRefresh());
   }

    // 覆盖equals必须覆盖hashCode
   @Override
   public int hashCode() {
      return Objects.hash(getId(), getSystemId(), getParentId(), getNodeType(), getName(), getIp(), getPort(), getDbType(), getIdentify(), getUsername(), getPassword(), getRootId(), getTargetTable(), getNeedRefresh());
   }

下面是最后的一些告诫 :
1.覆盖equals时总要覆盖hashCode
2.不要企图让equals方法过于智能
3.不要将equals声明中的Object 对象替换为其他的类型(记住用@Override,不要以重载代替重写!)

第 11 条:覆盖 equals 时总要覆盖 hashCode
原因在于以下约定:
1.在应用程序的执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被
修改,那么对同一个对象的多次调用, hashCode 方法都必须始终返回同一个值 。
在一个应用程序与另一个程序的执行过程中,执行 hashCode 方法所返回的值可以
不一致 。
2.如果两个对象根据 equals(Object )方法比较是相等的,那么调用这两个对象中
的 hashCode 方法都必须产生同样的整数结果 。
3.如果两个对象根据 equals(Object)方法比较是不相等的,那么调用这两个对象
中的 hashCode 方法,则不一定要求 hashCode 方法必须产生不同的结果 。 但是程
序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表( hash
table )的性能 。
概括如下:
1.hashCode 方法依赖于关键属性,执行结果同样要求一致性
2.equals成立→hashCode相等,即equals是hashCode的充分不必要条件

在实际工作中,如果不遵守以上约定会造成诸多不便,比如集合HashMap的实现就依赖了Key对象的hash码,不遵守约定的对象无法正常使用集合
可以参照规则10实现hashCode即可,同样可借助AutoValue框架,或IDE自动生成代码,Objects.hash参考实现如下:

public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

Arrays.hashCode实现

public static int hashCode(Object a[]) {
    if (a == null)
        return 0;
    int result = 1;
    for (Object element : a)
        result = 31 * result + (element == null ? 0 : element.hashCode());
    return result;
}

第 12 条:始终要覆盖 toString
提供好的 toString 实现可以便类用起来更加舒适,使用了这个类的系统也更易于调试。
当对象被传递给 println 、 printf 、字符串联操作符(+)以及 assert ,或者被调试器打印出来时, toString 方法会被自动调用!
toString的初始实现方法如下所示:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

在实际应用中, toString 方法应该返回对象中包含的所有值得关注的信息,同样可以借助IDE,选择关键属性生成,如下所示:

@Override
public String toString() {
   return "Database{" +
         "id='" + id + '\'' +
         ", systemId='" + systemId + '\'' +
         ", parentId='" + parentId + '\'' +
         ", name='" + name + '\'' +
         ", ip='" + ip + '\'' +
         ", port=" + port +
         '}';
}

该规则还建议对toString进行详尽的注释,告知其格式是否会改变,但在实际的业务开发中,涉及对象与字符串的互相转换往往依赖第三方的json转换jar包,如fastjson,gson,json-lib,jackson等。

第 13 条:谨慎地覆盖 clone
Cloneable接口决定了 Object中受保护的 clone 方法实现的行为:如果一个类实现了 Cloneable, Object 的 clone方法就返回该对象的逐域拷贝,否则就会抛出 CloneNotSupportedException 异常 。
事实上,实现 Cloneable 接口的类是为了提供一个功能适当的公有的 clone 方法。为了达到这个目的,类及其所有超类都必须遵守一个相当复杂的、不可实施的,并且基本上没有文档说明的协议。 由此得到一种语言之外的机制:它无须调用构造器就可以创建对象。
在Object中的原始定义如下:

protected native Object clone() throws CloneNotSupportedException;

有native修饰,说明其底层并不是由java语言实现,因此使用方法一般有三个步骤,如下,

// 1.实现Cloneable 接口(该接口没有声明的方法)
public class DBField implements Cloneable {
   private String id;
   private String column;
   ...
   
   @Override
   public DBField clone() {
      try {
          // 2.调用super.clone()得到一个复制的对象
         DBField obj = (DBField)super.clone();
         // 3.可以修改相关属性
         obj.setId(UUID.randomUUID().toString());
         return obj;
      } catch (CloneNotSupportedException e) {
         throw new AssertionError();
      }
   }
}

注意事项:
1.不可变的类永远都不应该提供 clone 方法
2.必须确保它不会伤害到原始的对象, 并确保正确地创建被克隆对象中的约束条件(如果对象中包含的域引用了可变的对象,可变对象需递归地调用 clone)
3.对象拷贝的更好的办法是提供一个拷贝构造器(copy constructory)或拷贝工厂(copy factory)

第 14 条:考虑实现 Comparable 接口
接口的源代码如下所示,一旦类实现了 Comparable 接口,它就可 以跟许多泛型算法以及依赖于该接口的集合实现进行协作(主要是用于排序)

public interface Comparable<T> {
    public int compareTo(T o);
}

compareTo的返回值为-1,0,1(小于、等于、大于),满足基本数理逻辑:自反性、对称性和传递性
强烈建议(x.compareTo(y) == 0) == (x.equals (y))

在 Java 8 中,Comparator接口配置了一组比较器构造方法,使得比较器的构造工作变得非常流畅。

// Java8流行的高效写法
public static Comparator<BusinessSystem> comparable1 = Comparator.comparing(BusinessSystem::getId);
// 带优先级的比较
public static Comparator<BusinessSystem> comparable2 = Comparator.comparing(BusinessSystem::getId)
    .thenComparing(BusinessSystem::getName);

@Test
public void comparatorTest() {
    BusinessSystem system = new BusinessSystem();
    system.setId("1234");
    system.setName("ABC");
    BusinessSystem copy = system.clone();
    System.out.println(comparable1.compare(system, copy));  // 0
    copy.setName("BBB");
    System.out.println(comparable2.compare(system, copy));  // -1
}

Tip:在实现有序集合时,可以考虑用Comparator初始化TreeSet或利用Arrays.sort()方法高效排序

总结:
每当实现一个对排序敏感的类时,都应该让这个类实现 Comparable 接口,以便其实例可以轻松地被分类 、搜索,以及用在基于比较的集合中。每当在 compareTo 方法的实现中比较域值时,都要避免使用< 和>操作符,而应该在装箱基本类型的类中使用静态的compare方法,或者在Comparator 接口中使用比较器构造方法 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值