Java 中Object类与equals方法

2019-06-15 00:00:00 object java 方法


Object类中的equals()

总说Object类

首先介绍一下Object类。
Class {@code Object} is the root of the class hierarchy.
Every class has {@code Object} as a superclass. All objects,
including arrays, implement the methods of this class.

正如 JDK 文档所说,Object类是类继承层次结构的根,所有的对象(包括数组)都实现Object类的方法。


类Object的常见方法

equals(Object obj)

源码如下:

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

说明:equals方法在Object类中比较简单。如上,如果参数obj所指的类和此方法本身的类相等,返回true。反之返回false。其实,从语义上分析,X.equals(Y)方法如果返回true,意味着 X 和 Y是等价的。equals方法反映了一种等值关系。
另外需要注意

Note that it is generally necessary to override the {@code hashCode}
method whenever this method is overridden, so as to maintain the
general contract for the {@code hashCode} method, which states
that equal objects must have equal hash codes.
当你在子类中重写equals方法时,同时重写hashCode()方法是非常有必要的。(想想这句话,到底是为什么?思考之后看看下面的介绍)下面我们来看看hashCode方法。

hashCode()

源码:

public native int hashCode();

它是native方法,本地实现。此方法返回object对象的哈希值。
先看 JDK 中的这段话。

As much as is reasonably practical, the hashCode method defined by

  • class {@code Object} does return distinct integers for distinct
  • objects. (This is typically implemented by converting the internal
  • address of the object into an integer, but this implementation
  • technique is not required by the
  • Java™ programming language.)

这段话是说,hashCode方法由于非常实用,所以被定义在Object类中。它为不同的对象返回不同的整数值。它的内部实现机理,通过底层的native方法将内存地址映射为一个整数值。

重写hashCode方法实例

public class Student {

    private int age;

    public Student(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return age;
    }
}

首先我们定义一个Student类,里面有一个age属性。并重写 hashCode ()。
然后来测试一下:


public class Main {

    public static void main(String[] args) {
        Student student = new Student(21);
        System.out.println("student.hasCode is -->>>" +
        student.hashCode());
    }
}

输出结果:student.hasCode is -->>>21
由于我们new Student()构造方法中传入21,并重写了hashCode方法,让其return age,所以上述例子不难理解。但是问题来了,我们重写了hashCode方法,将其返回age属性值。那么如果我想返回原来的hashCode值,怎么办呢?是不是无法实现呢?
别急,看下面:

public class Main {

    public static void main(String[] args) {
        Student student = new Student(21);
        System.out.println("student.hasCode is -->>>" +
        student.hashCode());
        int originalHashCode = System.identityHashCode(student);
        System.out.println("Original hash code of student is -->>>" +
                originalHashCode);
    }
}
 输出结果为:`student.hasCode is -->>>21 
 Original hash code of student is -->>>460141958`

注意:如果你运行的hashCode值与我这里的不一样,但你并没有错。这是因为哈希值是任意的,它取决于你的系统。

hashSet hashMap 与hashCode的关系?

先看一个例子:

package com.company;

/**
 * Created by xiaofengzheng on 2016/12/14 0014.
 */
public class Student {

    private int age;

    public Student(int age) {
        this.age = age;
    }
}

这个一个普通的JavaBean。Student类,里面有个age字段和一个构造方法。

package com.company;

import java.util.HashSet;

public class Main {

    public static void main(String[] args) {
        Student s1 = new Student(16);
        Student s2 = new Student(17);
        Student s3 = new Student(18);
        Student s4 = new Student(19);
        Student s5 = new Student(20);

        HashSet<Student> students = new HashSet<>();

        students.add(s1);
        students.add(s2);
        students.add(s3);
        students.add(s4);
        students.add(s5);

        System.out.println("HashSet Size--->>>" + students.size());
        System.out.println("students.contains( new Student(16))--->>>" +
                students.contains(new Student(16)));
        System.out.println("students.remove( new Student(19)--->>>" +
                students.remove( new Student(19)));
        System.out.println("Now HashSet Size--->>>" +
                students.size());

    }
}

运行结果为:

HashSet Size--->>>5
students.contains( new Student(16))--->>>false
students.remove( new Student(19)--->>>false
Now HashSet Size--->>>5

这以为着你不能找到期望的对象。都返回了false。为什么呢?先不急。
下面我们稍微修改下Student类:

package com.company;

public class Student {

    private int age;

    public Student(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return age;
    }

    @Override
    public boolean equals(Object obj) {
        boolean flag = false;
        Student s = (Student) obj;
        if (s.age == age) {
            flag = true;
        }
        return flag;
    }
}

然后再运行下:
你会结果为:

HashSet Size--->>>5
students.contains( new Student(16))--->>>true
students.remove( new Student(19)--->>>true
Now HashSet Size--->>>4

这是为什么呢?
JDK文档中hashCode方法前,有下面一段文字

Returns a hash code value for the object. This method is
supported for the benefit of hash tables such as those provided by
{@link java.util.HashMap}.

所以我觉得,HashTable一定与Object类中的hashCode有关。
下面来探索一下:
在HashSet中

   public boolean contains(Object o) {
        return map.containsKey(o);
    }
  public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

可以看出HashSet内部是用HashMap实现的。
来看HashMap

   public boolean containsKey(Object key) {
        return getNode(hash(key), key) != null;
    }
    public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
    }

发现都有方法hash(key).于是,

  static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

看!正是调用了Object.hasCode.这也解释了我们上面的例子。请读者 细细琢磨一下。

为什么要重写hashCode方法和equals方法呢?

The general contract of {@code hashCode} is:
<li>Whenever it is invoked on the same object more than once during
an execution of a Java application, the {@code hashCode} method
must consistently return the same integer, provided no information
used in {@code equals} comparisons on the object is modified.
This integer need not remain consistent from one execution of an
application to another execution of the same application.
<li>If two objects are equal according to the {@code equals(Object)}
method, then calling the {@code hashCode} method on each of
the two objects must produce the same integer result.
<li>It is <em>not</em> required that if two objects are unequal
according to the {@link java.lang.Object#equals(java.lang.Object)}
method, then calling the {@code hashCode} method on each of the
two objects must produce distinct integer results. However, the
programmer should be aware that producing distinct integer results
for unequal objects may improve the performance of hash tables.
</ul>
<p>

详情请阅读上述文档。简而言之,契约的意思就是 相同的对象返回相同的哈希值,不同的对象也要有不同的哈希值。当然,如果你同时重写hashCode和equals也行,只是违反了规则。
所以,如果你想equals方法去比较两个对象,那么请你同时重写那这个方法。下面给出一个例子。

public class Student {

    private int age;

    public Student(int age) {
        this.age = age;
    }

    @Override
    public int hashCode() {
        return age;
    }

    @Override
    public boolean equals(Object obj) {
        boolean flag = false;
        Student s = (Student) obj;
        if (s.age == age) {
            flag = true;
        }
        return flag;
    }
}
public class Main {

    public static void main(String[] args) {
        Student student = new Student(21);
        Student student2 = new Student(21);
        System.out.println("student.equals(student2)-->>" +
                student.equals(student2));
    }
}

输出结果为:student.equals(student2)-->>true

wait()

当一个线程执行到wait()方法时,它就进入到一个等待集合(the wait set for this object)中,同时失去(释放)了对象的机锁,使得其他线程可以访问。用户可以使用notify,notifyAll或者时间片用完来唤醒等待集合中的线程。

wait,notify,notifyAll

这三个方法都有线程有关,却不是Thread的一部分,而是Object的方法?仅仅是针对线程的功能为什么却作为通用基类的一部分来实现呢?尽管看起来很奇怪,不过也是有道理的。因为这些方法操作的锁也是所有对象的一部分,每个对象有一个锁(lock,monitor)。所以,你可以把wait方法放在任何同步控制方法里,而不用这个类是继承了Thread还是实现了Runnable接口。

toString()

这个方法不再细说,只看源码:

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

细心的你一定会发现,此方法调用了hashCode方法。

好,关于Object类的常见方法暂时介绍这么多,不知不觉两个小时过去了。时光荏苒,岁月静好。只希望我的分享能对你有所帮助,在下没有说清楚的,还烦请在下方留言,一起讨论,一起进步。加油!


String类中equals()

分析完了Object类中的equals方法,下面看String类中equals方法。这也是经常遇到的问题,面试官也很眷顾的一个知识点。
还是先分析下源码,然后举个例子体验下。

  public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

有源码可以看到,

if (this == anObject) {
            return true;
        }

如果用“==”成立,说明二者指向同一个对象,自然他们所有的字符序列也完全一样。直接return true。
然后 if (anObject instanceof String)判断anObject是否为String类型,若不是,返回false。若是,
判断长度是否相等,接着判断每个字符相等。


String类equals 与“==”的区别

由上面源码可以看出,String类里面的equals方法调用了==来判断。可见,“==”是判断两个对象的引用是否指向同一个对象。String类的equals这是判断两个字符串是否每个字符都相等。
下面用例子证明一下:

package com.company;

/**
 * Created by 10648 on 2016/12/14 0014.
 */
public class StringTest {
    public static void main(String[] args) {

        String str1 = new String("abc");
        String str2 = "abc";
        String str3 = new String("abc");
        String str4 = "ab" + "c";
        //使用equals判断
        System.out.println(str1.equals(str2));
        System.out.println(str1.equals(str3));
        System.out.println(str3.equals(str2));
        //等号判断,判断引用变量str1,2,3,4 是否指向同一个
        //对象(引用变量(reference)类似指向对象的指针,也可以理解为地址)
        //在Java中我们不会也不该知道该引用变量实际装载的是什么,它只是用来
        // 代表单一的对象。只有Java虚拟机才会知道如何使用引用来取得对象。
        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str3 == str2);
        System.out.println(str4 == str2);
    }
}

结果为:

true
true
true
false
false
false
true

前六个不难理解,下面主要说下最后一个
也就是System.out.println(str4 == str2);为什么返回true?

/** Cache the hash code for the string */
    private int hash; // Default to 0
  public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

Java中String是存在于常亮池中的。也就是说,一个String被定义后它就被缓存到了常量池中,当其他地方使用到同样的字符串时,则直接使用的是缓存,而不会重复创建。
str2 和 str4 都是通过字面值赋值的,故str4使用了缓存在常量池中的str2对象。也就是说,str2 和 str4 是同一个字符串对象。其实,这也是设计模式之享元模式的体现


intern()方法

public class StringTest2 {
    public static void main(String[] args) {
        String s1 = new String("abc");
        String s2 = "abc";
        System.out.println(s1.equals(s2));
        System.out.println(s1 == s2);
        System.out.println(s1.intern() == s2);
    }
}

输出:

true
false
true

intern方法,检查字符串池里是否存在”abc”这么一个字符串,如果存在,就返回池里的字符串;如果不存在,该方法会 把”abc”添加到字符串池中,然后再返回它的引用。


总结

终于快写完了。总结下

  1. Object类及常见方法(equals,toString,hashCode,wait, notify)
  2. String 类重写了Object的equals方法,以及他们的关系
  3. HashSet和HashMap也与HashCode存在关系
  4. string的equals和“==”的区别
  5. 设计模式之享元模式(只是简单提起,后面有时间再补充享元模式)


参考来源

HashCode and Equals method in Java object – A pragmatic concept
JDK文档

转载请注明出处,请尊重个人劳动成果,同人作者,CSDN首发
http://blog.csdn.net/Zheng548/article/details/53644357
作者:小风筝
欢迎留言讨论

相关文章