重写toString()、equals()、hashcode()、compareTo()、compare()

接下来将从两方面分析

  • 为什么要重写这几个方法?
  • 怎么重写这几个方法?

toString()

public class Student {
    String name;
    int age;
    String address;
    public Student() {
    }
    public Student(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    }
}
public class testForStudent {
    public static void main(String[] args) {
        Student st1 = new Student("嘉文四世",20,"德玛西亚");
        Student st2 = new Student("卡萨丁",50,"虚空");
        Student st3 = new Student("卡莎",18,"虚空");
        Student st4 = new Student("提莫",6,"扭曲丛林");
        Student st5 = new Student("寒冰",18,"弗雷尔卓德");
        Student st6 = new Student("寒冰",18,"弗雷尔卓德");

        System.out.println(st1);
    }
}

输出是:

indiv.zcg.day912.Student@1b6d3586

如果不重新toString()方法,toString方法默认输出的是对象的地址!像String、Interger这种类其实都已经将toString重写了

查阅代码可知

public String toString() {
    return this;
}

如果不重写toString,即Object类中的

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

这就解释了为什么上面我们的输出是一个类全限类名@一个16进制数

那么如何重写?

public class Student {
    String name;
    int age;
    String address;
    public Student() {
    }
    public Student(String name, int age, String address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }
    @Override
    public String toString(){
        return "姓名:" + this.name + " " + "年龄:" + 
        		this.age + " " +"地址:" + this.address;
    }
}

输出结果是:

姓名:嘉文四世 年龄:20 地址:德玛西亚

判断是否重写了toString()方法,直接输出toString就好了,如果输出的是地址那么就是没有重写。


equals()

默认的equals()方法是

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

如果不重写,它比较的是两个对象的地址值

System.out.println(st5.equals(st6));
System.out.println(st3.equals(st6));

false
false

因此,必须重写equals()方法,它才能起作用

public boolean strEquals(String s1,String s2){
        if(s1.equals(s2)) return true;
        else return false;
    }
    @Override
    public boolean equals(Object obj){
        if(obj instanceof Student){
            Student st = (Student)obj;
            return strEquals(st.name,this.name) && st.age == this.age && strEquals(st.address,this.address);
        }
        return false;
    }

true
false

分析:由于重写了equals方法,这次比较的是名字、年龄、地址都相等才能返回true。上面的代码涉及到了子类对象专有的属性和方法父类对象无法访问的问题,就进行了多态的向下转型,将obj强制转型为一个Student,进而达到调用strEquals方法和属性的作用。

有个坑就是:将obj强制转换必须用student来接,不然会有异常。相当于没转类型。


hashcode

public native int hashCode();

hashcode底层的代码就这一行,它再往底层应该是用C++实现的一个哈希算法,至于哈希算法的具体细节感兴趣自己看吧,其实哈希算法是很重要的一个研究领域,一个好的哈希算法是很难设计的。

但是只要知道它的功能是:它会根据对象的地址,返回一个哈希值!Hash(address) = ****。

下面来试一下

System.out.println(st3.hashCode());
System.out.println(st5.hashCode());
System.out.println(st6.hashCode());

输出是:

460141958
1163157884
1956725890

这三个值是根据st3、st5、st6的内存地址经过哈希运算得来的。由于Java语言本身的限制,好像获取一个变量的地址挺难的…我是没查到如何获取。但是只要知道,hashcode采用的哈希函数的散列范围足够大,对于不同的输入,输出不会相同(这就是我上面说的哈希函数的好坏衡量标准,如果出现哈希碰撞那它就不是一个好的哈希函数)

那么不重写hashcode会有什么问题呢?问题很大!我上面埋了一个伏笔,st5和st6是两个完全相同的对象,可是他俩的hashcode却不一样。但是java中规定:如果两个对象的equals值相等,那么他们的hashcode必须相等。但是hashcode相等,他们的equals可以不相等!为什么要这么规定呢,举个反例。

HashSet这个容器的特点是,装入的对象不能重复。

HashSet<Student> hh = new HashSet<>();
hh.add(st2);
hh.add(st5);
hh.add(st6);
Iterator it = hh.iterator();
while (it.hasNext()){
    System.out.println(it.next());
}

输出

姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:卡萨丁 年龄:50 地址:虚空

结果分析

发现了没有!两个一样的对象居然都装入了HashSet!那么这个容器就失效了!这是绝对不允许的,其实如果用HashMap也会失效。这是因为HashSet的原理是:先获取对象的hashcode值,然后放入哈希表指定位置,如果冲突,再通过equals方法判断是否已经存在,如果存在就替换到原来的对象,不存在则加入到该位置的链表中(这个看不懂就再研究一下HashSet)。

而上面的t5和t6对象的哈希code是不相等的,为了适应java的这些容器,他俩实际上应该是相等的,所以就必须重写hashcode()

如何重写呢?

@Override
public int hashCode() {
    int result = name.hashCode();
    result = 17 * result + Integer.valueOf(age).hashCode();
    result = 17 * result + address.hashCode();
    return result;
}

姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:卡萨丁 年龄:50 地址:虚空

上面这个写法写完了之后,确实可以保证元素唯一了,因为HashSet底层调用hashcode的时候,值不相同的对象的hashcode就不一样了(但是好像也未必严谨,因为可能会冲突,但是就先不要考虑那么多啦,再好的哈希函数都有可能冲突)

IDEA编译器自动生成的hashcode方法是直接返回

return Objects.hash(name,age,address)

以上就是三种方法为什么要重写,以及重写的方法。

由于上面既然引出了Objects工具类,那就强调几个方法吧。

Objects工具类中常用的静态方法

  • Objects.hash(obj1,obj2…) // 这个是java支持的多参数传入写法
public static int hash(Object... values) {
    return Arrays.hashCode(values); // 多参数传入的values是一个数组
} // 求多个对象组合的哈希值
  • Objects.hashCode(obj1) // 获取一个对象的hashcode
  • Objects.equals(obj1,obj2)

compareTo()

其实这个不应该和上面混在一起,但是既然都要重写,那归纳到一起吧。

String类的CompareTo方法重写会返回两个字符串对应的asc码差值。

compareTo()方法是在Comparable接口中的一个方法,是一个比较器,必须被重写。

List<Student> students = new ArrayList<>();
students.add(st1);
students.add(st2);
students.add(st5);
students.add(st6);
Iterator ite = students.iterator();
while(ite.hasNext()){
    System.out.println(ite.next());
}
Collections.sort(students);
Iterator ite2 = students.iterator();
while(ite2.hasNext()){
    System.out.println(ite2.next());
}

姓名:嘉文四世 年龄:20 地址:德玛西亚
姓名:卡萨丁 年龄:50 地址:虚空
姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:寒冰 年龄:18 地址:弗雷尔卓德


姓名:嘉文四世 年龄:20 地址:德玛西亚
姓名:卡萨丁 年龄:50 地址:虚空
姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:寒冰 年龄:18 地址:弗雷尔卓德

结果分析:如果没有重写compareTo方法,比较器不起作用,Collection.sort是基于compareTo实现的。那么如何重写呢?

@Override
public int compareTo(Object o) {
    if(o instanceof Student){
        Student st = (Student) o;//强转
        if(st.age > this.age) return -1;
        if(st.age == this.age) return 0;
        if(st.age < this.age) return 1;
    }
    return 0;
}

重写了compareTo方法后输出的结果是

姓名:嘉文四世 年龄:20 地址:德玛西亚
姓名:卡萨丁 年龄:50 地址:虚空
姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:寒冰 年龄:18 地址:弗雷尔卓德


姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:寒冰 年龄:18 地址:弗雷尔卓德
姓名:嘉文四世 年龄:20 地址:德玛西亚
姓名:卡萨丁 年龄:50 地址:虚空

结果分析:说明compareTo起效果了。

compare方法

compare方法是comparator接口中的方法,它的功能其实和comparable接口中的compareTo方法一模一样!重写方法也一模一样,只不过是使用方法不一样而且传入的参数不一样。
首先说comparable接口,这个单词翻译过来是“具备比较能力的”或“能够比较的”,它应该由某个Java bean来实现,然后这个bean重写compareTo方法,在方法中定义比较的规则,这个方法只有一个参数,传入的参数和当前bean创建的对象this来比较。这个接口也叫自然排序接口,什么意思呢,就是一个类本身是不具备比较的能力的,比如一个学生类,按照年龄排序,这个类本身是不能按照年龄排序的,默认是按照类对象的地址来排序,它可以使得这个类具备排序的能力!所以叫自然排序。
然后是comparator接口,这个单词翻译过来是“比较者”或“比较器”,相对于上面的comparable,它是外部比较器,不是某个对象所特有的。当某个类实现了comparator接口,并重写compare接口,这个类的对象就成为了一个外部比较器!compare方法传入的参数有2个,是两个需要被比较的对象。当某个不具备比较能力的JavaBean想进行比较的时候,就得指定比较的规则,比较器中就定义了这个规则。

总之,两个接口其实功能是一样,只不过使用方法不一样,意义不一样,原则上讲,其实只会一个也行,我也搞不懂为什么非得要设计这两个比较器?很奇怪,通常来讲能用comparable的都可以用comparator代替的。