ITEEDU

Java Gossip: Object 类别

在Java中,所有的对象都隐含的扩充了Object类别,Object类别是Java程序中所有类别的父类别,当您定义一个类别时:

public class Foo { 
	// 实作 
} 

这个程序代码相当于:

public class Foo extends Object { 
	// 实作 
}

Object类别定义了几个方法,包括"protected"的clone()、finalize()两个方法,以及几个"public"方法,像是equals()、toString()、getClass()、hashCode()、notify()、notifyAll()等等的方法,这些方法您都可以加以重新定义(除了 getClass()、notify()、notifyAll()、wait()等方法之外,它们被宣告为 "final",无法被子类别扩充,所以无法重新定义),以符合您所建立的类别需求,我会在以后的适当主题中介绍这些方法的使用。

由于Object类别是Java中所有类别的父类别,所以它可以参考至任何的对象而不会发生任何错误,这是很有用,以后您会看到一些 Java程序中,有些对象可以加入一些衍生类别对象,并可透过方法呼叫会直接传回Object对象,这些对象可以经由型态(接口)转换而指定给衍生类别型 态参考。

下面这个程序中,您制作一个简单的 集合(Collection)类别,并将一些自订类别对象加入其中,这个程序示范了Object的一个应用:

Foo1.java
public class Foo1 {
	private String name;
	public Foo1(String name) {
		this.name = name;
	}
	public void showName() {
		System.out.println("foo1 name: " + name);
	}
	// 重新定义toString()
	public String toString() {
		return "foo1 name: " + name;
	}
}
Foo2.java
public class Foo2 {
	private String name;
	public Foo2(String name) {
		this.name = name;
	}
	public void showName() {
		System.out.println("foo2 name: " + name);
	}
	// 重新定义toString()
	public String toString() {
		return "foo2 name: " + name;
	}
}
SimpleCollection.java
public class SimpleCollection {
	private Object[] objArr;
	private int index = 0;
	public SimpleCollection() {
		objArr = new Object[10]; // 预设10个对象空间
	}
	public SimpleCollection(int capacity) {
		objArr = new Object[capacity];
	}
	public void add(Object o) {
		objArr[index] = o;
		index++;
	}
	public int getLength() {
		return index;
	}
	public Object get(int i) {
		return objArr[i];
	}
}
Test.java
public class Test {
	public static void main(String[] args) {
		SimpleCollection objs = new SimpleCollection();
		objs.add(new Foo1("f1 number 1"));
		objs.add(new Foo2("f2 number 1"));
		Foo1 f1 = (Foo1) objs.get(0);
		f1.showName();
		Foo2 f2 = (Foo2) objs.get(1);
		f2.showName();
		System.out.println();
		System.out.println("f1.toString(): " +
		f1.toString());
		System.out.println("f2.toString(): " +
		f2.toString());
	}
}

执行结果:

foo1 name: f1 number 1 
foo2 name: f2 number 1 

f1.toString(): foo1 name: f1 number 1 
f2.toString(): foo2 name: f2 number 1?

在程序中,SimpleCollection对象可以加入任何型态的对象至其中,而传回对象时,您只要透过型态(接口)转换,就可以操作型态(接口)上的方法。

Object的toString()方法预设会传回以下的字符串:

getClass().getName() + '@' +Integer.toHexString(hashCode());

getClass()方法是Object中定义的方法,它会传回对象于执行时期的Class实例,而hashCode()传回该对象的hash code,toString()方法用来传回对象的描述,通常是个文字性的描述,Object的toString()方法预设在某些场合是有用的,例如物 件的自我检视时,但在这边,您将之重新定义为文字模式下使用者看得懂的文字描述。

上面这个程序范例虽然简单,但您以后一定会常常看到类似的应用,例如窗口程序容器、Vector类别等等。

Object预设的equals()本身是比较对象的内存参考,如果您要有必要比较两个对象的内含数据是否相同(例如当对象被储存至Set时)您必须实作equals()与hashCode()。

一个比较常被采用的方法是根据对象中真正包括的的属性值来作比较,来看看下面的一个例子:

public class Cat {
	...
	public boolean equals(Object other) {
		if (this == other)
		return true;
		if (!(other instanceof Cat))
		return false;
		final Cat cat = (Cat) other;
		if (!getName().equals(cat.getName()))
		return false;
		if (!getBirthday().equals(cat.getBirthday()))
		return false;
		return true;
	}
	public int hashCode() {
		int result;
		result = getName().hashCode();
		result = 29 * result + getBirthday().hashCode();
		return result;
	}
}

这是一个根据商务键值(Business key)实作equals()与hashCode()的例子,当然留下的问题就是您如何在实作时利用相关的商务键值来组合,这要根据您实际的需求来决定,API中对于equals()的合约是必须具备反身性(Reflexive)、对称性(Symmetric)、传递性(Transitive)、一致性(Consistent)。

  • 反身性(Reflexive):x.equals(x)的结果要是true。
  • 对称性(Symmetric):x.equals(y)与y.equals(x)的结果必须相同。
  • 传递性(Transitive):x.equals(y)、y.equals(z)的结果都是true,则x.equals(z)的结果也必须是true。
  • 一致性(Consistent):同一个执行期间,对x.equals(y)的多次呼叫,结果必须相同。

可以参考API文件中Object类别的hashCode()之建议:

  • 在同一个应用程序执行期间,对同一对象呼叫 hashCode()方法,必须回传相同的整数结果。
  • 如果两个对象使用equals(Object)测试结果为相等, 则这两个对象呼叫hashCode()时,必须获得相同的整数结果。
  • 如果两个对象使用equals(Object)测试结果为不相等, 则这两个对象呼叫hashCode()时,可以获得不同的整数结果。

两个不同的对象,可以传回相同的hashCode()结果,这是合法甚至适当的,只是对象会被丢到同一个杂凑桶中。

至于clone()方法,它是有关于如何复制对象本身,您可以在当中定义您的复制方法,不过对象的复制要深入的话必须考虑很多细节,您可以从 这篇文章 开始稍微了解一下如何定义clone()方法。