首先,我们都知道Java所有的类都继承自超类【Object】,也就是说所有的类都是Object的子类,这是一个隐式的,我们并不需要特别的指出。
我们定义一个类A。
class A {
public A() {
super();
}
}
代码并不难懂,就是写了类A的构造函数。但是这里需要注意的是super关键词,调用的是父类的构造函数,我们可以跟进一下,就会跳到Object类去了。也就是说类A虽然没有明确的写出继承Object,但是Java还是让其继承了Object类。
Object类是有很多好处的,在本文中,我会慢慢讲述。
我们首先看看Object类主要的方法
protected Object clone()创建并返回此对象的一个副本。
boolean equals(Object obj)指示其他某个对象是否与此对象“相等”。
protected void finalize()当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。
Class<?> getClass()返回此 Object 的运行时类。
int hashCode()返回该对象的哈希码值。
String toString()返回该对象的字符串表示。
当然还有一些wait()、waitAll()等方法,与线程有关,就不提出来了。
1、我们先看看clone()方法:
首先clone()方法,Object的子类是不可直接用的,所以我们需要重写。请注意重写与重载的区别。重写是重写父类的方法,重载是写多个方法名相同,参数类型或者参数个数或者返回类型不同的方法。
clone()方法就是用于深拷贝的。
我们首先来看看浅拷贝与深拷贝的区别。
深拷贝:复制出两个内容相同,但是引用地址完全不同的两个对象。
浅拷贝:创建了一个与一开始对象引用地址完全相同的两个对象。
浅拷贝典型的例子:
public class Main {
public static void main(String[] args) {
A a = new A("Aiden");
A b = a;
System.out.println(a);
System.out.println(b);
System.out.println(a.name.hashCode());
System.out.println(b.name.hashCode());
}
}
class A {
String name;
public A(String name) {
this.name = name;
}
}
我们看看输出结果:
test.A@2a139a55
test.A@2a139a55
63256261
63256261
可以看出对象a和对象b输出的内容是一样的。@前面表示的是对象映射的类,@后面表示的是对象的哈希地址。而name的哈希码也是相同的。
在更多的时候,我们需要深拷贝,即拷贝出两个内容相同、但引用地址不同的对象。我们就可以重写clone()方法去实现了。
下面是简单的实现。
public class Main {
public static void main(String[] args) {
A a = new A("Aiden");
A b = a.clone();
System.out.println(a);
System.out.println(b);
System.out.println(a.name.hashCode());
System.out.println(b.name.hashCode());
}
}
class A {
String name;
public A(String name) {
this.name = name;
}
@Override
protected A clone() {
String name = new String(this.name);
return new A(name);
}
}
我们来看看输出的结果:
test.A@2a139a55
test.A@15db9742
63256261
63256261
我们看看,两个对象的引用确实不同了吧。可是两个对象的内容name初始的哈希码怎么还是一样的呢?好神奇。这是因为String是常量,所有的内容存储于常量池中,每次的赋值都会去常量池中找,如果找到了就将内容匹配的地址赋值给name,如果找不到,就新建一块内存,用于存储想要赋值的内容。
2、boolean equals(Object obj)方法
这个方法是用于比较两个对象是否相同的。**相同的定义包括了它们所属的类相同以及哈希码相同。**我们可以跟进Object类中的equals()方法。
public boolean equals(Object obj) {
return (this == obj);
}
如果不重写equals()方法的话,equals就是 == 符号的作用。
A a = new A("Aiden");
A b = new A("Aiden");
System.out.println(a.equals(b));
b = a;
System.out.println(a.equals(b));
输出结果:
false
true
但是,我们经常看到String对象如果要比较内容的话,使用的是equals()方法,也就是说String类的实现肯定是重写了equals()方法的。我们可以看看源码:
public boolean equals(Object anObject) {
if (this == anObject) { // 如果两个对象的引用相同,则返回true
return true;
}
if (anObject instanceof String) { // 如果anObject是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;
}
源码还是很简单的,注释也写了,就不多说了。
3、protected void finalize()
该方法是用于回收”垃圾“的。当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。详情可以参考博文:点击参考
4、int hashCode()方法
该方法返回该对象的哈希码。关于哈希码的定义,官方是这么说的
hashcode方法返回该对象的哈希码值。支持该方法是为哈希表提供一些优点,例如,java.util.Hashtable 提供的哈希表。
hashCode 的常规协定是:
在 Java 应用程序执行期间,在同一对象上多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是对象上 equals 比较中所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
如果根据 equals(Object) 方法,两个对象是相等的,那么在两个对象中的每个对象上调用 hashCode 方法都必须生成相同的整数结果。
以下情况不 是必需的:如果根据 equals(java.lang.Object) 方法,两个对象不相等,那么在两个对象中的任一对象上调用 hashCode 方法必定会生成不同的整数结果。但是,程序员应该知道,为不相等的对象生成不同整数结果可以提高哈希表的性能。
实际上,由 Object 类定义的 hashCode 方法确实会针对不同的对象返回不同的整数。(这一般是通过将该对象的内部地址转换成一个整数来实现的,但是 JavaTM 编程语言不需要这种实现技巧。)
当equals方法被重写时,通常有必要重写 hashCode 方法,以维护 hashCode 方法的常规协定,该协定声明相等对象必须具有相等的哈希码。
官方定义给出我们对象的哈希码最好是不一致的。这样在equals()方法才能不同。
我们先看看如果给出相同哈希码的两个对象做比较时,会出现怎样的情况。我们就重写了hashCode()方法、equals()方法。
import java.util.HashSet;
import java.util.Set;
public class Main {
public static void main(String[] args) {
A a = new A("Aiden");
A b = new A("Aiden");
System.out.println(a == b);
System.out.println(a.equals(b));
Set<A> set = new HashSet<>();
set.add(a);
set.add(b);
System.out.println(set.size());
}
}
class A {
String name;
public A(String name) {
this.name = name;
}
@Override
public int hashCode() {
return 0;
}
@Override
public boolean equals(Object object) {
if (object == null)
return false;
if(object == this)
return true;
if (object instanceof A == false)
return false;
A b = (A)object;
if(name.equals(b.name))
return true;
return false;
}
}
我们用Set对象去检测两个对象是否哈希吗是否相同。我们看看具体的输出。
false
true
[test.A@0]
第一行输出false,理所当然,==号是比较两个对象的引用。
第二行输出true,是因为重写了equals()方法,比较的是两个对象的name是否相同。
第三行输出却只有一个内容,也就是说HashSet在add的时候,是比较两个对象的哈希码的,如果哈希码已经存在于Set集合中,就不加入了。如果不存在Set集合中,就加入。
看来哈希码,在HashMap、HashSet、Hashtable中有广泛的应用啊。可是怎样才能实现多个对象,哈希码不相同呢?
这里给出了通用的重写hashCode()的方案:
- 初始化一个整形变量,为此变量赋予一个非零的常数值,比如int result = 17;
- 选取equals方法中用于比较的所有域,然后针对每个域的属性进行计算:
(1) 如果是boolean值,则计算f ? 1:0
(2) 如果是byte\char\short\int,则计算(int)f
(3) 如果是long值,则计算(int)(f ^ (f >>> 32))
(4) 如果是float值,则计算Float.floatToIntBits(f)
(5) 如果是double值,则计算Double.doubleToLongBits(f),然后返回的结果是long,再用规则(3)去处理long,得到int
(6) 如果是对象应用,如果equals方法中采取递归调用的比较方式,那么hashCode中同样采取递归调用hashCode的方式。 否则需要为这个域计算一个范式,比如当这个域的值为null的时候,那么hashCode 值为0
(7) 如果是数组,那么需要为每个元素当做单独的域来处理。如果你使用的是1.5及以上版本的JDK,那么没必要自己去 重新遍历一遍数组,java.util.Arrays.hashCode方法包含了8种基本类型数组和引用数组的hashCode计算,算法同上,
public static int hashCode(long a[]) {
if (a == null)
return 0;
int result = 1;
for (long element : a) {
int elementHash = (int)(element ^ (element >>> 32));
result = 31 * result + elementHash;
}
return result;
}
5、String toString()返回该对象的字符串表示。
我们可以重写这个方法,也可以重写。如果不重写,一般来说就会返回”className@hashCode“格式的字符串。也就是返回类名+”@“+哈希码。
其实我们在System.out.println(a);的时候就默认调用了a.toString()方法,所以才会输出刚刚的格式。