《Effective Java》第二章 对于所有对象都通用的方法

接下来继续讲第二章,第8-12条。

第8条:覆盖equals时请遵守通用约定

equals 时Object类的一个非final方法,一般是表示类的实例对象是否相同,也就是对象的地址是否相等。

但是某些时候却要重写Object.equals方法。即类需要有“逻辑相等”,也就是值类,这都需要重写equals方法。这样这个类的实例可以用做Map的key中。有一种值类就不需要重写equals,就是单例模式的类,至始至终也就一个对象。

在覆盖equals时需要遵守的几个约定:自反性、对称性、传递性、一致性、非null。


第9条:覆盖equals时总要覆盖hashode

每个重写类equals方法的类都必须要重写hashCode方法,不然在改类将不能和基于散列的集合【HashMap、HashSet】一起正常使用。对象的根据equals方法判断时相等的,其hashcode肯定也是相等的。即判断2个对象是否相等,先比较hashcode是否相等,若相等则在比较equals是否相等。

看下面的一个例子。

package com.example.demo2;

import java.util.Objects;

public class Student {

    private int age;
    private String name;
    private int grade;

    public Student(int age, String name, int grade) {
        this.age = age;
        this.name = name;
        this.grade = grade;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;
        Student student = (Student) o;
        return age == student.age &&
                grade == student.grade &&
                Objects.equals(name, student.name);
    }

 
}

当把这个类和HashMap一起使用时

Map stu = new HashMap<>();
        stu.put(new Student(12,"jack",99),"Jack");

    期望的时stu.get(new Student(12,"jack",99)) 返回的时Jack,但是实际上返回的时null,主要的原因就是没有重写hashCode,从而导致2个对象实例有不同的hashcode值。一个好的哈希函数就是为每个不同的对象产出不同的散列码。

一般一个理想的散列函数设计方案:

1、把某个非零的常数值,比如说17,保存在一个名为result的int类型的变量中

2、对于对象只能够的每个关键字f,完成下面的步骤

a、为该域计算int类型的散列码C

b、按下面的计算公式,result = 31 * result + c

3、返回result

 public int hashCode() {

        int result = 17;
        result = 31 * result + age;
        result = 31 * result + Integer.valueOf(name);
        result  = 31 * result + grade;
        return result;
    }

但是现在的IDE已经非常的强大了,可以利用快捷键自动重写equals和hashcode方法了,但是其中的原理我们还是需要掌握的。


第10条:始终要覆盖toString

java.lang.Object也提供了toString方法的一个实现,但是它格式可读性不好,它包含类的名称,一个一个“@”符号,接着是haahcode的无符号的十六进制表示法。虽然遵守toString的约定不如遵守equals、hashcode的约定这么重要,但是提供好的toString方法可以让类的使用更加的舒适。


第11条:谨慎地覆盖clone

按照书中的话来讲,能不重写clone就不要去重写,因为它带来的问题太多了。我们暂且不讨论这里面的陷阱有多少,只从对Java基础知识的掌握程度来说明什么是clone,以及什么是“深拷贝”和“浅拷贝”。首先观察以下代码,并思考对象在内存中的分配以及引用的变化:

public class Student {
    private String name;
    private int age;
    
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}
public class Main {  
    public static void main(String[] args) throws Exception{  
        Student stu = new Student("kevin", 23);  
        Student stu2 = stu;  
        stu2.setAge(0);  
        System.out.println(stu.getAge());  
    }  
} 
这是一段很简单的代码,Student对象实例stu、stu2在内存中的分配及引用分别如下图所示:


所以代码中出现修改stu2实例的age字段时,stu中的age字段也被修改了,原因很简单因为它们的引用指向的都是同一个对象实例。

那如果我们想在实例化一个name=”kevin”,age=23的Student实例怎么办呢?当然可以再写一段Student stu2 = new Student(“kevin”, 23);如果再重新构造一个对象实例很复杂,能不能直接复制呢?显然,使Student实现Cloneable接口并重写clone方法即可,注意此时的重写clone方法在里面仅有一句代码即是即调用父类的clone方法,而不是自定义实现:

[java]  view plain  copy
  1. public class Student implements Cloneable{  
  2.     private String name;  
  3.     private int age;  
  4.       
  5.     public Student(String name, int age) {  
  6.         this.name = name;  
  7.         this.age = age;  
  8.     }  
  9.       
  10.     public String getName() {  
  11.         return name;  
  12.     }  
  13.       
  14.     public void setName(String name) {  
  15.         this.name = name;  
  16.     }  
  17.       
  18.     public int getAge() {  
  19.         return age;  
  20.     }  
  21.       
  22.     public void setAge(int age) {  
  23.         this.age = age;  
  24.     }  
  25.       
  26.     @Override  
  27.     protected Student clone()   
  28.                 throws CloneNotSupportedException {  
  29.         return (Student)super.clone();  
  30.     }  
  31. }  
  32. public class Main {  
  33.     public static void main(String[] args) throws Exception{  
  34.         Student stu = new Student("kevin"23);  
  35.         Student stu2 = stu.clone();  
  36.         stu2.setAge(0);  
  37.         System.out.println(stu.getAge());  
  38.     }  
  39. }  
调用clone方法产生的对象实例并不是之前的实例,而是在堆上重新实例化了一个各个参数类型值都相同的实例,所以此时修改stu2的age字段并不会影响到stu,看起来clone就是一个构造器的作用 -- 创建实例。



上面我们仅仅是说明了什么是clone,接下来我们接着来讲解什么是“深拷贝”和“浅拷贝”。

  在上面的例子Student类中,我们新增一个引用型变量Test类:

[java]  view plain  copy
  1. public class Student implements Cloneable{  
  2.     private String name;  
  3.     private int age;  
  4.     private Test test;  
  5.     public Student(String name, int age) {  
  6.         this.name = name;  
  7.         this.age = age;  
  8.     }  
  9.       
  10.     public String getName() {  
  11.         return name;  
  12.     }  
  13.       
  14.     public void setName(String name) {  
  15.         this.name = name;  
  16.     }  
  17.       
  18.     public int getAge() {  
  19.         return age;  
  20.     }  
  21.       
  22.     public void setAge(int age) {  
  23.         this.age = age;  
  24.     }  
  25.     public String getTest() {  
  26.         return test;  
  27.     }  
  28.       
  29.     public void setTest(Test test) {  
  30.         this.test= test;  
  31.     }  
  32.     @Override  
  33.     protected Student clone()   
  34.                   throws CloneNotSupportedException {  
  35.         return (Student)super.clone();  
  36.     }  
  37. }  

[java]  view plain  copy
  1. public class Main {  
  2.     public static void main(String[] args) throws Exception{  
  3.         Student stu = new Student("kevin"23);  
  4.         Student stu2 = stu.clone();  
  5.         stu2.setAge(0);  
  6.         System.out.println(stu.getAge());  
  7.     }  
  8. }  
实际上测试这段代码可知,clone出来的stu2确实和stu是两个对象实例, 但它们的成员变量实际上确是指向的同一个引用 (通过比较hashCode可知),这也就是所谓的“浅拷贝”。对应的“深拷贝”则是所有的成员变量都会真正的做一份拷贝。怎么做到“深拷贝”,则是要求将类中的所有引用型变量都要clone。
[java]  view plain  copy
  1. /** 
  2.  * 深拷贝 
  3.  *  
  4.  */  
  5. public class Student implements Cloneable{  
  6.     private String name;  
  7.     private int age;  
  8.     private Test test;  
  9.   
  10.     public Student(String name, int age) {  
  11.         this.name = name;  
  12.         this.age = age;  
  13.     }  
  14.   
  15.     public String getName() {  
  16.         return name;  
  17.     }  
  18.   
  19.     public void setName(String name) {  
  20.         this.name = name;  
  21.     }  
  22.   
  23.     public int getAge() {  
  24.         return age;  
  25.     }  
  26.   
  27.     public void setAge(int age) {  
  28.         this.age = age;  
  29.     }  
  30.   
  31.     public Test getTest() {  
  32.         return test;  
  33.     }  
  34.   
  35.     public void setTest(Test test) {  
  36.         this.test = test;  
  37.     }  
  38.   
  39.     @Override  
  40.     protected Object clone()   
  41.                       throws CloneNotSupportedException {  
  42.         Student stu = (Student)super.clone();  
  43.         stu.test = test.clone();    //Test类也要继承Cloneable  
  44.         return stu;  
  45.     }  
  46. }  

书中是不建议自定义重写clone方法的,如果非要重写书中总结为一句话:clone方法就是一个构造器,你必须确保它不会伤害到原始的对象,并确保正确地创建被克隆对象中的约束条件。

  再说一个与本条目无关的点,查看Cloneable接口实际上可以发现里面什么方法都没有,clone方法却来自Object类,继承了Cloneable接口为什么就能重写clone方法了呢?原因在于clone方法在Object类中的修饰符是protected,而Cloneable接口和Object处于同一个包下,熟悉修饰符的都知道protected的权限限定在同一个包下或者其子类。Cloneable和Object同属于一个包,Cloneable自然能继承clone方法,继承了Cloneable接口的成为了它的子类同样也就继承了clone方法。



第12条:考虑实现Comparable接口

此接口强行对实现它的每个类的对象进行整体排序。这种排序被称为类的自然排序,类的 compareTo 方法被称为它的自然比较方法

实现此接口的对象列表(和数组)可以通过 Collections.sort(和 Arrays.sort)进行自动排序。实现此接口的对象可以用作有序映射中的键或有序集合中的元素,无需指定比较器。


package com.example.demo2;

import java.util.*;

public class Student  implements Comparable{

    private int age;
    private String name;

    private int grade;

    @Override
    public int compareTo(Student o) {

        return this.grade - o.grade;
    }

    public Student(int age, String name, int grade) {
        this.age = age;
        this.name = name;
        this.grade = grade;
    }


    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name='" + name + '\'' +
                ", grade=" + grade +
                '}';
    }

    public static void main(String[] args) {


        Student s1 = new Student(12,"kk",200);
        Student s2 = new Student(15,"ff",100);
        Student s3 = new Student(17,"zz",390);
        List result = new ArrayList<>();
        Collections.sort(result);

       for(Student s : result) {
           System.out.println(s.toString());
       }

    }




}

总结:我们可以使用Comparable接口中的compareTo方法使原本无法比较的对象通过某种自身条件来排序.

下面的方法也可以实现按grade字段排序

Collections.sort(result, new Comparator() {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.grade - o2.grade;
    }
});

你可能感兴趣的:(Java)