在Java语言中,除了byte、short、int、long、boolean、char、float和double这8种基本类型意外,你所能够看到、操作到的都属于对象类型。并且以上8种类型在Java中也提供了相应的封装类,为:Byte、Short、Integer、Long、Boolean、Character、Float、Double。
Java中所有的类——除了Object类本身之外——都直接或者间接继承了Object类,也就是说,Java中的所有类都拥有继承自Object类的属性和方法。基本上可以说Object类是Java语言的开端。因此把Object类研究清楚还是非常重要的。
Object类被定义在java.lang包中,这个包里面包含了Java语言最基础和最核心的内容,比如以上提及过的8种基本类型封装类和String等。在Java文件编译时会自动导入java.lang这个包,所以我们所编写的代码中是无需导入该包的。Object类中并没有定义属性,一共有13个方法如下:
方法定义 | 简单说明 |
1 + public Object() | 空参构造法,返回一个Object对象 |
2 - private static native void registerNatives() | Java系统内部使用 |
3 # protected native Object clone() throws CloneNotSupportedException | “克隆”当前对象并返回 |
4 + public final native Class<?> getClass() | 获取当前类的类型 |
5 + public boolean equals(Object obj) | 判断是否相等 |
6 + public native int hashCode() | 获取当前对象的哈希码 |
7 + public String toString() | 返回当前对象的字符串表示 |
8 + public final native void notify() | 唤醒此对象监视器上等待的单个线程 |
9 + public final native void notifyAll() | 唤醒此对象监视器上等待的所有线程 |
10 + public final void wait() | 让调用此方法所在的当前线程等待 |
11 + public final native void wait(long timeout) | 让调用此方法所在的当前线程等待相应时长 |
12 + public final void wait(long timeout, int nanos) | 让调用此方法所在的当前线程等待相应时长 |
13 # protected void finalize() | Java垃圾回收机制相关 |
1. public Object()
在Java中,如果你要创造一个Object类型的对象,那么就要使用new Objec()来获得。类似的,如果你要获取一个其他类的对象例如Person也需要new Person()来获得。public Object()并没有在源码中找到,但它实际上是存在的,否则我们也无法使用它。
与此相对应,在我们定义的类中,如果我们没有为该类定义构造方法,系统也会为它隐性添加一个空参的构造方法。同时,我们也可以为类添加有参数的构造方法,这样带参数的构造方法一般都需要先调用空参构造方法来初始化对象。
我们还可以将构造方法设为private,这时往往意味着该类是不能被实例化的。例如Enum类型与java.lang.Math类,你只能使用该类的静态变量和静态方法。
2. private static native void registerNatives()
Java中被native类型修饰的方法说明该方法是使用C或者C++来完成的,并且被编译成了.dll文件。registerNatives这个方法是Java自身需要使用的,而我们平时一般用不到。它的主要作用是将C/C++的方法映射到Java中的native的方法中,实现方法命名的解耦。
在源码中,该类的定义结束后有一段代码:
static {
registerNatives();
}
static {...}这样的代码块成为静态代码块,它随着类的加载而执行,并且只会加载一次。如果一个类中有多个静态代码块,会按照书写顺序依次执行。静态代码块一般用来初始化静态变量。
相对的,还有一种代码块叫做构造代码块,与静态代码块有些类似,不过它并没有前面的static字样,而是被一个空落落的大括号包起来{...}。构造代码块在创建对象时被调用,每次创建对象都会调用一次,而且优先于构造函数执行。
3. protected native Object clone() throws CloneNotSupportedException
与上面的registerNatives()一样,这个方法也是被native关键字修饰的,而且这两个方法都是没有方法体的。一般来说,Java中规定只有接口(interface)和抽象类(abstract class)才能够具有无方法体的方法。但是如果你在方法前使用native关键字的话也能够通过编译,但是一旦执行该方法报错(java.lang.UnsatisfiedLinkError)。
clone直接翻译过来就是“克隆”,而clone()这个方法是就是“克隆”某一个对象并返回。使用这个方法所得到的对象与原来的对象属性是一样的,但它们的内存空间不同。
clone()这个方法是被protected修饰的,很多介绍Java语言的书籍(包括《Java编程思想》)都对protected介绍的比较的简单,基本都是一句话,就是: protected 修饰的成员对于本包和其子类可见。 这句话读起来很拗口,而且常常会出错。比如当我们在Main类中实例化一个Student类的对象Student tom = new Student()
时,当我们试图引用tom对象的clone()方法时,这时候编译器会报错。
这是为什么呢?Student类不是Object类的子类吗,为什么无法调用Object类的方法呢?这些问题的根源在于protected这个修饰符。其实这个关键字并没有字面上那么容易理解,这里可以举一个例子说明一下:
现在在Main对象的main方法中实例化一个Person对象与一个Student对象。前置条件:Main与Student这两个类在同一个包中,而Person在另一个包中。Student对象继承自Person。
// package person
public Class Person {
protected void sayHello() { System.out.println("Hello"); }
protected void regret() { System.out.println("I'm sorry"); }
}
// package main
public Class Student extends Person {
protected void sayHi() { System.out.println("Hi"); }
protected void sayHello() { super.sayHello(); }
}
// package main
public class Main {
public static void main(String[] args) {
Person person = new Person();
person.sayHello(); // 错误
person.regret(); // 错误
Student student = new Student();
student.sayHello(); // 正确
student.sayHi(); // 正确
student.regret(); // 错误
}
}
- main函数中person对象无法调用sayHello()与regret()这两个方法,是因为Main与Person不在同一个包中。
- 在Student类中sayHello()可以调用Person的sayHello()方法,是因为Student是Person的子类。
- main函数中student对象可以调用sayHello()与sayHi()方法,是因为Student类与Main在同一个包中,而且这两个方法都在Student类里显示定义了。
- main函数中student对象不可以调用regret()方法,是因为Student类中没有显示定义该方法,而且Person与Main不在同一个包中。 如果把Person放到与Main和Student的同一个包中,则可以调用。
搞清楚protected这个关键字之后,现在我们把话题扯回到clone()方法,如果我们要使用clone来“克隆”一个Student类,该怎么写呢?如果把上面protected关键字搞明白的话,就会知道现在我们要在Student类中重写从Object继承的这一方法。具体的写法为:
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e.getMessage());
}
}
这里需要使用try...catch这个代码块是因为在clone()方法后面跟着“throws CloneNotSupportedException”这样的字样,翻译过来也就是“抛出不支持克隆异常”,从字面意思来说,就是哪怕你调用了这个方法,也很可能会因为不支持克隆而出现异常。那么怎么让Student类支持克隆呢?这就需要实现Cloneable接口,也就是在类的后面加上“implements Cloneable”这样的字段。在本例中代码写为:
public class Student extends Person implements Cloneable {
protected void sayHi() { System.out.println("Hi"); }
protected void sayHello() { super.sayHello(); }
@Override
protected Object clone() {
try {
return super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e.getMessage());
}
}
}
Cloneable接口本身不含任何方法,它仅仅只用来表示clone()方法可以被正常调用。
4. public final native Class<?> getClass()
与clone()一样,getClass()也是一个被native修饰的方法,所以我们无法在源码中找到其方法体。除了native之外,它还被final关键字修饰,意味着它的子类是不可以重写这个方法的。
在Java执行的时候,当一个类被引用加载之后,会在内存中生成一个相应的Class类型的类对象,该对象中存有类的变量和方法等。除了这个方法,我们还可以通过某类的.class来获取这个类的类对象。由于这个“类对象”存储了该类的所有属性,所以我们可以利用反射技术来根据这个“类对象”创建一个该类的实例,或者获得该类的信息。
5. public boolean equals(Object obj)
Object类中的equals方法其实调用的就是 == 这个运算符,而 == 运算比较的是两个对象的内存地址是否相等,这跟我们平时使用的不太一样。主要是因为我们平时一般会重写equals()方法,例如我们平时使用的String类,它就将equals()方法改写为:
public boolean equals(Object anObject) {
// 如果内存地址相等,那么返回true
if (this == anObject) {
return true;
}
// 如果传入的不是String类型,那么返回false
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
// 如果长度不一致,那么返回false
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 逐个对比每一个char元素,如果都相同那么返回true
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
这里稍微提一下,从以上的方法可以看出,String类内部其实是使用一个char类型的数组来存储数据的。
我们平时在自己的类中还可以把它改写其他的形式,只要符合你的需求就行。所以在面试的时候如果被问到equals与“==”有什么区别,大概知道怎么答了吧?
参考答案:“ == ” 比较的是两者的内存地址是否一样。在Object类中,原生的equals()方法的定义“ == ”相同,也是比较两者的内存地址。但是在平时我们可以自己改写equals()方法,比如String类的equals()方法就是比较两个字符串的值是否一样。在实际工作中,我们一般讲equals()改写为比较两个对象的值是否相等。
6. public native int hashCode()
方法功能:返回一个int类型的哈希码(也称为哈希值)。
这是一个看起来很简单的方法,但是如果要解释清楚哈希码是什么、Object类中为什么要有这个方法以及哈希码有什么用这三个问题,就会变得很复杂。
OK,现在让我们一个一个来解决上面的三个问题。第一个问题,哈希码是什么?
让我们先来看一下百度百科中对哈希值的说明:
哈希值,又称:散列函数(或散列算法,又称哈希函数,英语:Hash Function)是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。好的散列函数在输入域中很少出现散列冲突。在散列表和数据处理中,不抑制冲突来区别数据,会使得数据库记录更难找到。
嗯…相信很多小伙伴看完之后都跟我有一样的想法:这到底是在说什么???
其实我们可以理解为:哈希值(哈希码)是一个数字,它用来(唯一)匹配某一个对象。
- 为什么唯一这两个字要用括号括起来呢?这是因为我们的算法不是尽善尽美的,有可能两个不同的对象会算得同一个哈希码。
第二个问题:Object类中为什么要有hashCode()这个方法?
在Java中,有一种存储类型成为散列表,比如HashMap、Hashtable和HashSet等,这种散列类型的存储结构中只能存储不同的对象。如果你将同一个对象存储两次,也只不会存进去两个相同的对象。那么如何比较两个对象是否相同呢?第一种方式是比较两个对象的内存地址是否相等,但是比较内存地址一方面会比较影响性能,另一方面会无法比较两个对象是否相等。因为理论上来讲,如果两个对象所有属性都相等的话,我们就可以说这两个对象是相同的。这时候我们的哈希码就可以出场啦,通俗简单来说:如果两个对象的哈希码相等,那么我们就认为它们俩相等。通过哈希码我们可以大大提高散列类型存储结构的性能。
以HashMap为例,因为所有的对象都有可能被存储进HashMap中,所以就必须要在Object这个基类中加上hashCode()这个方法。
第三个问题:哈希码有什么用?
上面我们已经解释了,如果要将对象存储进HashMap等散列类型存储结构中,就要比较对象之间的哈希码是否相同,如果两个或多个对象的哈希码相同则只储存其中一个。所以我们如果要将对象存储进哈希表中的话,就要重写它的hashCode()方法。具体算法我们暂时不用管它,因为eclipse和idea等都有快捷键可以快速帮你实现这个功能。
7. public String toString()
toString()方法是我们平时用到比较多的一个方法,在使用System.out.println()这个方法在控制台中打印某一个对象时,调用的就是这个对象的toString()方法。在Object类中,toString()方法的定义为:return getClass().getName() + "@" + Integer.toHexString(hashCode());
也就是类名 + @ + 用16进制表示该对象的哈希码。在我们所写的类中,如果你没有重写它的toSting()方法,那么调用的就是这样的原生方法。不过我们可以根据需要重写该方法。同时,Java的IDE中也基本上都集成了重写toString()方法的功能。
8. public final native void notify()
9. public final native void notifyAll()
10. public final void wait()
11. public final native void wait(long timeout)
12. public final void wait(long timeout, int nanos)
to be continued..