关于程序员:Object所有类的基类

33次阅读

共计 6247 个字符,预计需要花费 16 分钟才能阅读完成。

Java 是一门典型的面向对象语言,提供 extends 关键字使子类继承父类。

public class Student extends Person {...}

然而创立 Person 类时不必应用 extends 继承 Object类。

public class Person extends Object {...}

因为,创立的类没有明确指明继承关系时,会在编译时主动继承 Object 类。能够应用 Object 类型的变量援用任何类型的实例:

Object sample = new Student(1001, "小咖", 20, "男");

也能够将任意的 Object 实例转换为须要的类型:

Person p = (Person) sample;

下面两个例子在编译器中是不会报错的。因而,Object 类是 Person 类的父类,每个类都是由 Object 扩大而来的。所以,相熟 Object 类中提供的服务是非常重要的。

Java 中的根本类型不是对象,但也提供了相应的包装类型,如 int 根本类型对应 Integer 包装类型。但应用根本类型创立的数组是扩大自 Object 类,实际上是援用。

Object sample = new int[10];

其实,所有的数组类型都是扩大了 Object 类。

下表为 Object 类的通用办法。

办法 形容 异样
final native Class<?> getClass() 返回对象的运行时类
native int hashCode() 返回对象的散列码
boolean equals(Object obj) 与其它对象是否相等
native Object clone() 克隆并返回对象的正本 CloneNotSupportedException
String toString() 返回对象的字符串示意
final native void notify() 唤醒正在期待对象监听器上的一个线程
final native void notifyAll() 唤醒正在期待对象监听器上的所有线程
final native void wait() 导致以后线程期待,直到另一个线程调用此对象的 notify()notifyAll() InterruptedException
final native void wait(long timeout) 导致以后线程期待,直到另一个线程调用此对象的 notify()notifyAll(),或者指定工夫已到 InterruptedException
final void wait(long timeout, int nanos) 导致以后线程期待,直到另一个线程调用此对象的 notify()notifyAll(),或者指定工夫已到 InterruptedException
void finalize() 当 GC 确定不再有对该对象的援用时,由对象的 GC 调用此办法 Throwable

equals()

Object 类提供了 equals() 办法用于检测对象是否相等。其实现相等性要满足五个条件:

  1. 自反性。对于任何的非空援用都需满足 x.equals(x) == true
  2. 对称性。对于任何援用都满足 x.equals(y) == y.equals(x)
  3. 传递性。对于任何援用都满足 (x.equals(y) && y.equals(z)) == x.equals(z)
  4. 一致性。对于不产生任何变动的援用都满足 x.equals(y) == x.equals(y),屡次调用 equals() 办法后果不变。
  5. null 的比拟。x.equals(null) 返回 false。对任何不是 null 的对象调用 equals() 办法与 null 比拟的后果都为 false

援用类型最好应用 equals() 办法比拟;而根本类型应用 == 比拟。

其中,子类中定义 equals() 办法时,需先比拟超类的 equals()。如果检测失败,对象就不可能相等。如果超类中的域都相等,就须要比拟子类中的实例域。

上面能够从两个截然不同的状况看一下这个问题:

  • 如果子类可能领有本人的相等概念,则对称性需要将强制采纳 getClass 进行检测。
  • 如果由超类决定相等的概念,那么就能够应用 instanceof 进行检测,这样能够在不同子类的对象之间进行雷同的比拟。

对象中应用 equals() 办法比拟须要实现的步骤如下:

  1. 查看是否为同一个援用,如果是间接返回 true
  2. 检测传入的值是否为 null,如果是间接返回 false
  3. 检测是否属于同一个类型。如果不是间接返回 false
  4. Object 对象类型转为要比拟的类型。
  5. 比拟每个要害域是否相等。
public boolean equals(Object o) {if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;
    Person person = (Person) o; 
    return Objects.equals(name, person.name) 
            && Objects.equals(sex, person.sex)
            && (age != person.age);
}

如果子类从新定义 equals,就要在其中蕴含 super.equals(other)

hasCode()

hashCode() 返回的是整数值,是无规律的散列码。hashCode() 定义在了 Object 类中,每个类都能够应用 hashCode()办法调用本身散列码,其值为对象的存储地址。

Object sample = new Student(1001, "小咖", 20, "男");
System.out.println(sample); // [I@5b464ce8

如果你在创立的类中笼罩了 equals() 办法,就必须笼罩 hashCode() 办法。这是 hashCode() 的通用约定。上面是笼罩 hashCode() 办法的约定:

  • 程序执行期间,对象的 equals() 办法中比拟的信息不变,同一个对象的 hashCode() 办法的返回值也不变。两个程序的执行期间,hashCode() 办法返回的值能够不统一。
  • 如果两个对象依据 equals(Object) 办法比拟相等,那 hashCode() 办法的返回值也必须相等。
  • 如果两个对象依据 equals(Object) 办法比拟不相等,那 hashCode() 办法的返回值最好不相等。如果相等,会在应用 Map 时造成散列码抵触。

总结就是两个对象相等,其散列码肯定雷同;然而散列码雷同的两个对象并不一定相等。因为计算散列码具备随机性,两个值不同的对象可能计算出雷同的散列码。

现实的散列函数是把汇合中不相等的实例平均地散布到所有可能的 int 之上。但十分艰难,只能实现绝对靠近这种现实的情景。

当计算散列码时,要将每个域都思考进去。能够将每个域都当成 R 进制的某一位,而后组成一个 R 进制的整数。

R 个别取奇数 31,偶数会呈现乘法溢出,信息会失落。因为与 2 相乘相当于向左移一位,最右边的位失落。并且一个数与 31 相乘能够转换成移位和减法:31*x == (x<<5)-x,编译器会主动进行这个优化。

如下是一个如何简略的计算散列码的参考:

  • 根本类型调用 Type.hashCode(value) 来生成。Type 类型为根本类型各自的包装类。
  • 如果是援用类型,并且须要笼罩 equals() 办法,equals() 办法应用哪些域比拟,hashCode() 办法也会递归地调用这些域的散列码并计算。
  • 如果是数组类型,那数组中的每个值都当做独自的域来解决。也能够应用 Arrays.hashCode() 办法计算。

不要试图从散列码计算中排除掉一个对象的要害域来进步性能

上面重写 Student 类的 hashCode() 办法:

@Override
public int hashCode() {int result = super.hashCode();
    result = 31 * result + sid;
    return result;
}

hashCode() 办法返回的散列码也能够是正数,正当地组合实例域的散列码,以便可能让各个不同的对象产生的散列码更加平均。

toString()

默认的 toString() 办法是返回的是 java.lang.Object@511baa65 这种类型是的字符串。

Student sample = new Student(1001, "小咖", 20, "男");
System.out.println(sample); // java.lang.Object@511baa65

下面 System.out.println(sample) 会主动调用 sample.toString() 办法将值输入到控制台。然而,提供好的 toString() 实现能够获取对象状态的必要信息,也易于调试。因而倡议为每个自定义的类笼罩 toString() 办法。

上面重写 Student 类的 toString() 办法:

@Override
public String toString() {return getClass().getName() 
            + "{ sid=" + sid
            + ", name=" + super.getName()
            + ", age=" + super.getAge()
            + ", sex=" + super.getSex()
            + "}";
}

如果父类 Person 也重写类 toString() 办法:

@Override
public String toString() {return getClass().getName() + 
            "{" + '\'' +
            "name='" + name + '\'' +
            ", age=" + age +
            ", sex='" + sex + '\'' +
            '}';
    }

并且子类的 toString() 也能够调用:

@Override
public String toString() {return super.toString() + "{sid =" + sid + "}";
}
// xxx.Student{'name=' 小咖 ', age=20, sex=' 男 '}{sid = 1001}

clone()

Object 类提供了 clone() 办法用于克隆实例,但因为是 protected 修饰符所润饰的办法,因而不会显示地笼罩 clone()。实现 Cloneable 接口的类能够笼罩 clone() 办法提供克隆。如果不实现,会抛出 CloneNotSupportedException 异样。正确的写法如下所示:

public class Person implements Cloneable {
    private String name;
    private int age;
    private String sex;
    private String[] address;
    public Person(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    
    ... 省略 getter 与 setter...

    @Override
    protected Person clone() throws CloneNotSupportedException {return (Person) super.clone();}
}

Java 反对协变返回类型,也就是笼罩办法的返回类型能够是被笼罩办法的返回类型的子类。并且在 clone() 办法中调用 super.clone() 办法失去性能残缺的克隆对象。

当应用 Person 创建对象并调用 clone() 办法克隆。

Person s1 = new Person("小卡", 22, "男");
s1.setAddress(new String[] {"浙江省", "江苏省", "湖南省"});
Person s2 = sample.clone();
System.out.println(s1.hashCode()); // 873415566
System.out.println(s2.hashCode()); // 818403870
System.out.println(s1 == s2); // false
System.out.println(s1.getAddress() == s2.getAddress()); // true

从下面的代码中得出,调用 clone() 办法取得的对象是个新的对象,然而对象中的援用还是原来的援用,而不是新援用。这次的克隆被称为 浅拷贝

应用 clone() 办法与通过结构器创建对象实际上是一样的,要确保不会挫伤到原始的对象,并确保正确地创立被克隆的对象中的约束条件。这次的克隆被称为 深拷贝

因而,在 Person 类的外部,address 数组也要递归地调用 clone() 办法:

@Override
protected Person clone() throws CloneNotSupportedException {Person result = (Person) super.clone();
    result.address = address.clone();
    return result;
}

记住,Cloneable 与援用可变对象的 final 域的失常用法是不兼容的。因而,实现 clone() 办法禁止给 final 赋新值。

上述的拷贝形式比较复杂。能够在类中提供一个拷贝结构器或拷贝工厂来实现克隆的代替性能。

public Person(Person value) {...}
public static Person newInstance(Person value) {...}

finalize()

当 GC 确定不再有对该对象的援用时,GC 会调用对象的 finalize() 办法来革除回收。

protected void finalize() throws Throwable {}

因而,子类能够通过笼罩此办法解决一些额定的清理工作。然而,finalize() 办法何时被调用取决于 Java VM,而且不保障 finalize() 办法会被及时地执行。因而,不要依赖 finalize() 办法来更新重要的长久状态。

Java VM 会确保一个对象的 finalize() 办法只被调用一次,而且程序中不能间接调用 finalize() 办法。

finalize() 办法通常也不可预测,而且很危险,个别状况下,不必要笼罩 finalize() 办法。

wait 与 notify

Object 对象提供了 wait()notify() 办法,这两个办法的应用是绝对的:

  • wait():线程进入期待状态。
  • notify():唤醒期待该对象的线程。

应用 wait() 办法必须在同步区域外部调用,这个同步区域将对象锁定在调用 wait() 办法的对象上。上面是应用 wait() 办法的规范模式:

synchronized (obj) {while (<condition does not hold>) {obj.wait();
    }
}

这时的 obj 对象所在线程会处于期待状态,须要应用 notify() 办法唤醒 obj 所在线程。

synchronized (obj) {obj.notify();
}

最好应用 notifyAll() 办法唤醒线程。因为总会产生正确的后果,保障会唤醒所有须要被唤醒的线程。尽管也会唤醒其余线程,但不影响程序的正确性,而且 notifyAll() 办法代替 notify() 办法能够防止来自不相干线程的意外或歹意的期待。

留神:必须应用 synchronized,否则会报 IllegalMonitorStateException 异样。

总结

这里对 Object 类做了一个简略的理解,晓得 Object类是所有类的基类,能够援用所有的援用类型,包含数组类型。且 Object 中提供的办法须要在适宜的场景下笼罩失去最佳的后果,最好始终都笼罩 toString() 办法。

更多内容请关注公众号「海人为记」

正文完
 0