Java - Obejct 类 (待续)

2018-09-18 06:37:14来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

关于Object类中的方法,根据其所涉及的知识点,分为如下4个部分:

  • 基础
    • clone : protected Object clone() throws CloneNotSupportedException
    • equals : public boolean equals?(Object obj)
    • hashCode : public int hashCode()
    • toString : public String toString()
  • 反射
    • getClass : public final Class<?> getClass()
  • 线程
    • wait
      • public final void wait() throws InterruptedException
      • public final void wait?(long timeout) throws InterruptedException
      • public final void wait?(long timeout,int nanos) throws InterruptedException
    • notify : public final void notify()
    • notifyAll : public final void notifyAll()
  • JVM
    • finalize : protected void finalize() throws Throwable

 2018-09-15

今天写:equals、hashCode、toString、clone、getClass,后面的方法等学到相关知识再作补充。

 一. equals

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

equals用来判断两个对象是否”相等“,而对“相等”这个词的定义不同,得出的结果也不同:

  • 1. Object类中的equals使用 this == obj 来定义相等,即:两个引用是否指向同一个对象;
  • 2. 有些时候,我们并不需要定义如此严格的相等,比如:定义一个Point类,只要两个点的横纵坐标分别相等,我们就认为它们相等;

要实现上面的需求,我们就需要重写equals方法,如下:

public class Point {
    int x;
    int y;
    
    Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof Point) {
            Point p = (Point)obj;
            return (this.x == p.x && this.y == p.y);
        }
        return false;
    }
}

测试:

public static void main(String[] args) {
    Point p1 = new Point(1, 1);
    Point p2 = new Point(1, 1);
    System.out.println("p1.equals(p2) : " + p1.equals(p2));
    p2.x = 2;
    System.out.println("p1.equals(p2) : " + p1.equals(p2));
}
/* 输出
p1.equals(p2) : true
p1.equals(p2) : false
*/

另记:String类也重写了equals方法,实现了:只要两个字符串长度相等及字符串中对应位置的字符相等,则两字符串相等。可以参考String类中的equals方法。

注意:在重写equals方法时,方法定义要写为:public boolean equals(Object obj) {....} ,参数类型是:Object obj

 二. hashCode

public native int hashCode();

hashCode,官方解释:返回对象的一个哈希码,为基于哈希表的数据结构提供便利,如:HashMap等。

Object类中的hashCode方法为本地方法,在重写hashCode方法时,需遵循以下规则:

  • 1. 在对象equals比较中使用的信息未被修改的前提下,在Java程序运行期间,同一个对象多次调用(不同时间)应该始终返回同一个整数。在程序不同的运行期间,返回的整数不需要保持一致;
  • 2. 如果两个对象调用 equals(Object obj) 时相等,那么这两个对象在调用 hashCode 时应该返回同一个整数值;
  • 3. 并不要求为调用 equals(Object obj) 时不相等的两个对象,返回两个不同的哈希码。只不过为不相等的对象返回不同的哈希码可以提高哈希表的性能;

关于第2条规则,我们继续Point类这个例子。首先,在未重写hashCode方法的情况下,我们测试两个对象的hashCode()输出值:

public static void main(String[] args) {
    Point p1 = new Point(9483, 89382);
    Point p2 = new Point(9483, 89382);
    System.out.println("p1.hashCode() : " + p1.hashCode());
    System.out.println("p2.hashCode() : " + p2.hashCode());
}
/* 输出:
p1.hashCode() : 166239592
p2.hashCode() : 991505714
*/

可以看到,在我们定义的equals方法下相等的两个对象,得到的hashCode是不同的,如此不一致会造成什么后果呢?我们知道 HashMap 在存储<Key, Value>时,如果Key1等于Key2,那么存储的键值对为:<Key1, Value2>,即:只会存储一个Key,使用的是最新的Value。而 HashMap 中在判断 Key1是否等于Key2时,使用的就是它们的hashCode。在未重写hashCode方法的情况下,看如下测试:

public static void main(String[] args) {
    Point p1 = new Point(9483, 89382);
    Point p2 = new Point(9483, 89382);
    
    HashMap<Point, Integer> map = new HashMap<Point, Integer>();
    map.put(p1, p1.hashCode());
    map.put(p2, p2.hashCode());
    for (Map.Entry<Point, Integer> m : map.entrySet()) {
        System.out.println(m);
    }
}
/* 输出
Point@9e89d68=166239592
Point@3b192d32=991505714
*/

根据我们对Point类相等的定义,p1与p2相等,而在 HashMap 中却存入了两个键值对,显然不符合我们的意图。(equals与hashCode的不一致,会造成使用时产生歧义,从而导致意想不到的错误。所以,我们在重写equals方法后,也要重写hashCode方法,使其意义一致)现在我们来重写hashCode方法,再进行如上测试:

@Override
public int hashCode() {
    return (x & y) | (x ^ y);
}
/* 输出
Point@17d2f=97583
*/

根据我们对hashCode方法的定义,对象的hashCode只与(x, y)相关,所以 p1.hashCode() == p2.hashCode() 为 true。这样一来,HashMap 中只会存入一个键值对,符合我们的预期。

 三. toString

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

源码中直接返回:对象类型名@对象hashCode的十六进制,举个例子:

public static void main(String[] args) {
    Point p1 = new Point(9483, 89382);
    System.out.println(p1.toString());
}
/* 输出
Point@17d2f
*/

很多情况下,我们都要重写toString()方法,就比如Point类,我们想知道的是点的横纵坐标(x, y),而不是 Point@17d2f 这串不知所云的字符。

@Override
public String toString() {
    return "(" + x + ", " + y + ")";
}
/* 输出
(9483, 89382)
*/

四. clone

protected native Object clone() throws CloneNotSupportedException;

 从方法定义入手:

  • 1. protected,表明只有本类及其子类可以使用该方法来克隆对象,子类可以重写该方法;
  • 2. native,表明Object类中clone是本地方法;
  • 3. CloneNotSupportedException,如果调用clone方法的对象的类没有 implememnts Cloneable,就会抛出这个异常;

现在,我们对之前的Point类进行部分修改,为了节省空间,我只贴出修改部分的代码:

首先,定义Data类,用来记录一个点所包含的相关信息;

public class Data {
    int weight;
    String name;
    
    Data(int weight, String name) {
        this.weight = weight;
        this.name = name;
    }
}

然后,Point类实现Cloneable接口,并且Point类中包含一个Data类型字段,如下:

public class Point implements Cloneable {
    int x;
    int y;
    Data data;

    Point(int x, int y, Data data) {
        this.x = x;
        this.y = y;
        this.data = data;
    }
... }

测试:

public static void main(String[] args) throws Exception {
    Data data = new Data(20, "A");
    Point p1 = new Point(1, 2, data);
    Point p2 = (Point)p1.clone();
    
    System.out.println("p1 == p2 : " + (p1 == p2));
    System.out.println("p1.(x, y) = " + p1.toString() + ", p2.(x, y) = " + p2.toString());
    System.out.println("p1.data == p2.data : " + (p1.data == p2.data));
}
/* 输出
p1 == p2 : false
p1.(x, y) = (1, 2), p2.(x, y) = (1, 2)
p1.data == p2.data : true
*/

对于测试的输出,我们可以发现:

  • 1. p1 == p2 为 false,说明 p1.clone() 重新生成了一个对象;
  • 2. p1.(x, y) 等于 p2.(x, y),说明 p1.clone() 会把原对象的基础数据类型字段的值拷贝给生成的新对象的对应字段;
  • 3. p1.data == p2.data 为 true,说明引用类型字段,新对象的字段与原对象的字段引用同一个对象;

对于第3条,即Object类的clone方法是浅拷贝,理解如图:

 

在一些并发编程情景下,我们常常需要操作 不可变对象 来保证并发安全性。不可变对象,顾名思义就是你创建的对象不会改变,你可以理解为:

  • 1. 对象本身就是不可变的,如:字段都是final修饰等;
  • 2. 对象本身可变,但是我们确保在使用的时候不会去改变它,即人为的不可变;

(更详细的内容,可以参考《Java并发编程实战》)

现在,假如我要在并发环境下使用p1.clone()出来的对象p2,并要求p2是不可变的。而事实上,其他线程可以通过 p1.data 来改变 p2.data 的状态,以破坏p2的不可变性。

要想使p2不可变,我们就需要对Point类进行深拷贝,即:对Piont类中的Data类型字段也创建一个新的对象,使得 p1.data != p2.data,如下:

public class Data {
    ...
    // 自定义的clone(),并非重写Object类中的clone()
    public Data clone() {
        return new Data(weight, name);
    }
}
public class Point implements Cloneable {
    ...
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Point p = (Point)super.clone();
        p.data = data.clone(); // 这里的data.clone()与Object类中的clone()无关
        return p;
    }
    ...
}
/* 重复上面的测试,输出:
p1 == p2 : false
p1.(x, y) = (1, 2), p2.(x, y) = (1, 2)
p1.data == p2.data : false
*/

思考:如果一个类中一直嵌套着包含引用类型字段,那么我们该怎么才能做到深拷贝呢?很明显,对于类中每一个引用类型对象都做深拷贝。(递归处理)

 五. getClass

public final native Class<?> getClass();

getClass方法,返回对象的类对象,在反射中经常使用,例如:

Data类中有个私有方法 printInfo(),该方法在Point类中无法正常调用,但是我们可以通过反射机制来调用该方法。

public class Data {
    ...
    private void printInfo() {
        System.out.println("weight = " + weight);
        System.out.println("name : " + name);
    }
}
// 在Point类中
public static void main(String[] args) throws Exception {
    Data data = new Data(20, "A");
    Class<?> clz = data.getClass();
    Method m = clz.getDeclaredMethod("printInfo");
    m.setAccessible(true); // 抑制Java的访问控制检查
    m.invoke(data);
}
/* 输出
weight = 20
name : A
*/

这里只是简单提一下,更多关于反射的知识,会在后期总结。

 

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:Tomcat服务器使用(一)

下一篇:SpringAOP面向切面编程