浅谈Java中的equals与==陷阱

Java当中最常用的一个类便是String,对于C++程序员来说这个string也是非常熟悉的。到了Java这一块,咦,仅仅是变了一个大小写一样,还不是一样的用。不过用着用着就发现了一个天大的问题。这也是我实际写Java的时候发生的问题,程序莫名其妙死了,就是因为使用了Java的==

std::string a = "abc";
std::string b = "abc";
if (a == b){
    std::cout<<"a is equal to b"<<std::endl;
} else {
    std::cout<<"a is not equal to b"<<std::endl;
}
String a = "abc";
String b = "abc";
if (a == b){
    System.out.println("a is equal to b");
} else {
    System.out.println("a is not equal to b);
}

对于一个C++程序员来说上面的代码输出什么估计想都不用想都知道是输出a is equal to b,但是如果是一个Java程序员他肯定会说肯定输入a is not equal to b。C++程序员在想:Java程序员这么弱智的嘛,这个a和b明显相等的啊!Java程序员在想:C++程序员这么智障的吗,这个明显不相等的啊!先不管这两个程序员骂来骂去的了,先看看为什么会发生这种情况。

这时候一般就会有一个大神跳出来说,你们这群蠢蛋,Java当中==比较的是地址,equals()方法才是比较的内容。C++程序员其实是在做这样的事

String a = "abc";
String b = "abc";
if (a.equals(b)){
    System.out.println("a is equal to b");
} else {
    System.out.println("a is not equal to b);
}

C++程序员一测验,果然输出了a is equal to b。哇这个人果然是大神,晓得了晓得了,又学到了一个知识点,==比较地址,equals比较内容。不过C++程序员转念一想1 + 1 == 2难道在Java中都不对吗?于是反问大神,大神说:你是沙雕吗?这个肯定是true啊。那我加个限制吧,对于非基础类型而言,==是比较地址, equals()方法是比较内容。

但是C++程序员都是比较喜欢test瞎测验的。这一侧不得了了。

StringBulider a = new StringBulider("abc");
StringBulider b = new StringBulider("abc");

if (a.equals(b)){
    System.out.println("a is equal to b");
} else {
    System.out.println("a is not equal to b");
}

if (a == b){
    System.out.println("a is equal to b");
} else {
    System.out.println("a is not equal to b");
}

C++程序员发现这两个输出的都是a is not equal to b。鬼鬼,这还得了啊。问了一下大神,大神也是哑口无言了。说起什么,你这个类不算,肯定是你的类出了问题。

那么问题到底出在了哪儿呢?

首先我们要清楚为什么为什么每个类都会有一个equals方法。比如如下写了一个类

class Student{
    private int id;
    private String name;

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

    // 这里省略了setter
    public int getId(){
        return this.id;
    }

    public String getName(){
        return this.name;
    }
}

我们发现这个类中没有写equals方法,但是我们仍然是可以调用的。原因就在于Java中的每个类都是默认继承Object的,也就是说equals是从Object中继承过来的方法。那就好办了,直接看看Object中equals()方法是怎么写的不就行了。

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

我靠!这个equals和==不就是一样的嘛。有个屁的区别啊。这大神不是在耍我完的吗?那为什么上面的String类中==和equals呈现的结果不同呢?那是因为String中是Override(覆盖)了equals方法的。

以上面的这个Student举例,判断两个人是不是相等,肯定不能用name(光是我听到的名字叫张旭的就至少五个,据说有一次考试按姓名分班,全校的张旭都坐一排,到了张旭,有来了两个张旭,感觉张旭这个名字比张伟还强,至少张伟这个一个都不认识)。所以我们应该用id来重载一下equals()方法。

class Student{
    private int id;
    private String name;

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

    public int getId(){
        return this.id;
    }

    public String getName(){
        return this.name;
    }

    @Override
    public boolean equals(Object obj){
        if (obj instanceof Student){
            Student s = (Student)obj;
            return s.getId() == obj.id;
        }
        return false;
    }
}

哈,现在已经都完事了吧。这样这个类也算是完美了。

至于重写equals方法是有五个规定的,不过那五个规定估计傻子都不会违反的,这里就叭说了。

重写了这个equals难道这就完美了吗?不尽然。

class Person{
    protected int id;
    protected String name;

    public Person(int id, String name){
        this.id = id;
        this.name = name;
    }

    public int getId(){
        return this.id;
    }

    public String getName(){
        return this.name;
    }
}

class Student extends Person{
    @Override
    public boolean equals(Object obj){
        if (obj instanceof Student){
            Student s = (Student)obj;
            return s.getId() == this.id;
        }
        return false;
    }
}

你说此时,难道作为人的小明和作为学生的小明不是一个人吗?所以说这个equals还是有不足,没有考虑到可能有的父类。或者说这个equals可以直接在父类中重载,而不用在子类中重载。

把这一点也考虑到了,哇哈哈,这个equals已经完美了,不是么?

嗯。。还真的不是,你以为你又能了,其实不然。尤其是在HashMap的使用上。

Student a = new Student(123, "sher");
Student b = new Student(123, "sher");
System.out.println(a.equals(b)); // true

Map<Student, Integer> scoreMap = new HashMap<Student, Integer>();
scoreMap.put(a, 99);
System.out.println(scoreMap.get(a)); // 99
System.out.println(scoreMap.get(b)); // null

你看,又出问题了。即使你a和b是完全一样的。但是放入HashMap中之后你却只能通过a来获取成绩了。这是因为HashMap使用的是哈希表。HashMap在判断你是否相等的时候是这样子的.

if (a.equals(b) && a.hashCode() == b.hashCode()){
    ....
}

前面虽然是相等的,但是后面是不相等的。那么后面那玩意是啥啊?其实他也是所有类都从Object继承的方法。当我们重写equals而不重载hashCode的话,就会在这些使用哈希表的集合中出现问题。所以api文档规定我们:

  • 如果两个对象调用equals()是true,那么他们的hashCode必须是相等的。(hashCode返回一个int)
  • 如果两个对象调用equals()是false,我们建议他们的hashCode不要相等。

所以我们还要Override一下hashCode方法。

class Person{
    protected int id;
    protected String name;

    public Person(int id, String name){
        this.id = id;
        this.name = name;
    }

    public int getId(){
        return this.id;
    }

    public String getName(){
        return this.name;
    }
}

class Student extends Person{
    @Override
    public boolean equals(Object obj){
        if (obj instanceof Person){
            Person p = (Person)obj;
            return p.getId() == this.id;
        }
        if (obj instanceof Student){
            Student s = (Student)obj;
            return s.id == obj.id;
        }
        return false;
    }

    @Override
    public int hashCode(){
        // int类型的hashCode系统已经替我们写好了
        return Integer.hashCode(this.id);// 其实就写return id;好像也没什么问题,毕竟是整型变量。
    }
}

至此,是不是就没有问题了呢?其实,可能还有问题,不过还有什么问题,我也是不晓得的了,等学到了更多的知识,有了丰富的经验之后看这一小段代码,还是会有很多的问题。不过,这个问题,现在就叭说了吧。


一枚小菜鸡