概述
Object 类是类继承结构的根。它是每个类的顶级父类。所有对象(包括数组)都实现这个类的方法。
Object 类中的方法如下:
equals()
public boolean equals(Object obj) {
return (this == obj);
}
可以看出默认情况下 equals 进行对象比较时只判断了对象是否是其自身,当有特殊的“相等”逻辑时,则需要覆盖 equals 方法。
equals 方法的通用约定:
- 自反性: 对于任何非 null 的引用值 x,x.equals(x) 必须返回 true。
- 对称性: 对于任何非 null 的引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 必须返回 true。
- 传递性: 对于任何非 null 的引用值 x、y、z,如果 x.equals(y) 返回 true,并且 y.equals(z) 返回 true,那么x.equals(z) 也必须返回 ture。
- 一致性: 对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就会一致的返回 ture,或者一致的返回 false。
- 非空性: 对于任何非 null 的引用值 x,x.equals(null) 必须返回 false。
在类继承尤其要注意实现的 equals 方法是否满足约定。
一个不满足约定的例子如下:
有个 People 父类:
public class People {
private float weight;
private String name;
public People(float weight, String name) {
this.weight = weight;
this.name = name;
}
@Override
public boolean equals(Object o) {
if (o != null && o instanceof People) {
People people = (People) o;
return (this.weight == people.weight) && Objects.equals(this.name, people.name);
}
return false;
}
}
有个 Student 子类:
public class Student extends People {
private String address;
public Student(float weight, String name, String address) {
super(weight, name);
this.address = address;
}
@Override
public boolean equals(Object o) {
if (o != null && o instanceof Student) {
Student student = (Student) o;
return super.equals(student) && this.address.equals(student.address);
}
return false;
}
}
执行一下比较:
public static void main(String[] args) {
People people = new People(51.0F, "张三");
Student student = new Student(51.0F, "张三", "北京");
System.out.println(people.equals(student)); // true
System.out.println(student.equals(people)); // false
}
很显然,这并不符合对称性。
事实上:在扩展可实例化的类的同时,是无法既增加新的值组件,同时又保留 equals 的约定,除非愿意放弃面向对象的抽象所带来的优势。
那这个问题该如何解决呢:
方法一:
不要拿 People 和 Student 去比,这依赖于人的主观自觉,虽然可以既利用继承带来的优势,但总归还要取决于人,有一定风险。
方法二:
将继承转为组合。
public class Student {
private String address;
private People people;
public Student(String address, People people) {
this.address = address;
this.people = people;
}
@Override
public boolean equals(Object o) {
if (o != null && o instanceof Student) {
Student student = (Student) o;
return people.equals(student.people) && this.address.equals(student.address);
}
return false;
}
}
这时,再进行比较,结果是满足 equals 约定的。
public static void main(String[] args) {
People people = new People(51.0F, "张三");
Student student = new Student("北京", people);
System.out.println(people.equals(student)); // false
System.out.println(student.equals(people)); // false
}
注意:当 toString 方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
hashcode()
public native int hashCode();
hashCode方法的约定:
(1)在应用程序执行期间,只要对象的 equals 方法的比较操作所用到的信息没有被修改,那么对这同一个对象调用多次,hashCode 方法都必须始终如一的返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
(2)如果两个对象根据 equals(Object) 方法比较是相等的,那么调用这两个对象中任意一个对象的 hashCode 方法都必须产生同样的整数结果。
(3)如果两个对象根据 equals(Object) 方法比较是不相等的,那么调用这两个对象中任意一个对象的 hashCode 方法,则不一定要产生不同的整数结果。(尽管如此,理想的 hashCode 方法对不相等的对象,应当提供不同的 hashCode 值,这样的话再将对象作为 Map 的 key 时就可以提高散列表的性能)
如果覆盖了 equals 方法,但是没有覆盖 hashCode 方法,就会违反上述第 2 条约定。
用上面的 People 类来验证一下:
public static void main(String[] args) {
People people1 = new People(51.0F, "张三");
People people2 = new People(51.0F, "张三");
System.out.println(people1.equals(people2)); // true
System.out.println(people1.hashCode() == people2.hashCode()); // false
}
这会带来什么后果呢?如果把 people1 放到 HashMap 中,随后试图用一个 “相等” 的people2 来找回时,就会出问题。如下:
public static void main(String[] args) {
People people1 = new People(51.0F, "张三");
People people2 = new People(51.0F, "张三");
Map<People, Integer> peopleMap = new HashMap<>();
peopleMap.put(people1, 1);
System.out.println("the number of People:" + peopleMap.get(people2));
}
执行程序,得到如下结果:
the number of People:null
toString()
Object 类的 toString 方法 等价于 getClass().getName() + "@" + Integer.toHexString(hashCode())
,
默认的 toString 方法仅仅返回类名+当前实例 hashCode 值的十六进制串,这非常不便于阅读,且没有包含实例中属性的值。
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
通过重写 toString 方法,可以输出需要的实例信息,例如可以对实例属性值进行格式化显示等等:
@Override
public String toString() {
return "People{" +
"weight=" + weight +
", name='" + name + '\'' +
'}';
}
除了手工编写格式化方式,我们还可以借助于一些第三方类库来实现实例的格式化,例如使用 Apache Commons Lang 工具包中的 ToStringBuilder 类。
getClass()
public final native Class<?> getClass();
一个不可被重写的本地方法,返回运行时的类。
获得运行时类信息后,就可以站着 Class 类上做很多事。比如生成类的实例、获取类中的方法、获取类的注解等。
clone()
protected native Object clone() throws CloneNotSupportedException;
clone 方法将创建和返回该对象的一个拷贝。这个 “拷贝” 的精确含义取决于该对象的类。一般的含义是,对于任何对象 x,表达式 x.clone() != x
将会是 true,表达式 x.clone().getClass() == x.getClass()
将会是 true。并且通常情况下,表达式 x.clone().equals(x)
将会是 true。
定义有点啰嗦,见下例就明白了。
一个标准的 clone
实现需要做到以下两点:
- 调用
super.clone()
方法 - 对于对象中的所有引用类型,均需要实现
Cloneable
接口,并重写clone
方法,然后对每个引用执行clone
方法。
注意,所有的数组都被视为实现接口 Cloneable。
示例代码:
public class Student extends People implements Cloneable {
private String address;
public Student(float weight, String name, String address) {
super(weight, name);
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.address = this.address;
return student;
}
public static void main(String[] args) {
try {
Student student1 = new Student(10.2F, "张三", "北京");
Student student2 = (Student) student1.clone();
System.out.println(student2 != student1); //x.clone() != x 为 true
System.out.println(student2.getClass() == student1.getClass()); // true
System.out.println(student2.equals(student1)); //true
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
使用 clone
方法创建对象的的优点:
- 速度快。
clone
方法最终会调用Object.clone()
方法,这是一个 native 方法,本质是内存块复制,所以在速度上比使用 new 创建对象要快。 - 灵活。可以在运行时动态的获取对象的类型以及状态,从而创建一个对象。
当然,使用 clone
方法创建对象的缺点同样非常明显:
- 实现深拷贝较为困难,需要整个类继承系列的所有类都很好的实现
clone
方法。 - 需要处理
CloneNotSupportedException
异常。Object类中的clone
方法被声明为可能会抛出CloneNotSupportedException
,因此在子类中,需要对这一异常进行处理。
因此,如果想使用 clone 方法的话,需要非常谨慎。《Effective Java》中建议不应该实现 Cloneable 接口,而应该使用拷贝构造器或者拷贝工厂。
finalize()
protected void finalize() throws Throwable { }
根据 java api中的说法:
“当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法”。
“finalize 方法可以采取任何操作,其中包括再次使此对象对其他线程可用;不过,finalize 的主要目的是在不可撤消地丢弃对象之前执行清除操作”。
“Java 编程语言不保证哪个线程将调用某个给定对象的 finalize 方法”。
“对于任何给定对象,Java 虚拟机最多只调用一次 finalize 方法”。
尽管 finalize 在某些时候是有用的,但是在大部分情况下,还是不建议使用,基于以下几点:
(1)不保证会被 jvm 执行,且不知道何时才会执行。这就给程序执行带来了很大不确定性。
(2)不同的 jvm 垃圾回收算法不一致,在一个 jvm 上工作良好,可能在另一个 jvm 上未必有效。
(2)性能。根据《Effective Java》的描述,增加了 finalize 后,对象的创建和销毁时间慢了430 倍。
wait、notify、notifyAll
-
notify()
-
notifyAll()
-
wait(long)
-
wait(long,int)
-
wait()
这几个本地方法都是 public final native
的,不可以被子类重新。这三个方法提供了 java 线程间等待、挂起等协同机制,是 java 多线程的基础。等看到 JUC 包后再来补充~