最近在准备面试,把知识点复习一遍,整理出的笔记记录下,里面会穿插代码和面试例题。
内容不是原创,是总结和收集,并在理解的基础上进行一些完善,如果侵权了请联系作者,若有错误也请各位指正。因为收集的时候忘记把来源记录下来了,所以就不po出处了,请见谅(这是个坏习惯,一定改)。
面试复习之—Java基础(十一):Class类和Object类
这是面试复习内容的第十一篇——Class类和Object类,主要是Java基础的内容,所有内容将分为几篇来写。
Class类
Java中的Class类
Class类就是用来创建类的所有的“常规”对象的。每个类的运行时的类型信息就是用Class对象表示的。Java使用Class类来执行RTTI。
类是程序的一部分,每个类都会产生一个Class类的对象。换言之,每当编写且编译了一个新类,就会产生一个Class对象(更恰当的说,是保存在一个同名的.class文件中)。
Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。
这项信息(RTTI)纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。
所有的类都是在对其第一次使用时,动态加载到JVM中的(懒加载)。当程序创建第一个对类的静态成员的引用时,就会加载这个类。使用new创建类对象的时候也会被当作对类的静态成员的引用。因此java程序程序在它开始运行之前并非被完全加载,其各个类都是在必需时才加载的。
说白了就是:
- Class类也是类的一种,只是名字和class关键字高度相似。Java是大小写敏感的语言。
- Class类的对象内容是你创建的类的类型信息,比如你创建一个shapes类,那么,Java会生成一个内容是shapes的Class类的对象。
- Class类的对象不能像普通类一样,以 new shapes() 的方式创建,它的对象只能由JVM创建,因为这个类没有public构造函数。如下所示:
/*
* Private constructor. Only the Java Virtual Machine creates Class objects.
* This constructor is not used and prevents the default constructor being
* generated.
*/
//私有构造方法,只能由jvm进行实例化
private Class(ClassLoader loader) {
// Initialize final field for classLoader. The initialization value of non-null
// prevents future JIT optimizations from assuming this final field is null.
classLoader = loader;
}
- Class类的作用是运行时提供或获得某个对象的类型信息,和C++中的typeid()函数类似。这些信息也可用于反射。
Class类的原理
看一下Class类的部分源码:
//Class类中封装了类型的各种信息。在jvm中就是通过Class类的实例来获取每个Java类的所有信息的。
//主要包含了 1)原子类操作方法、2)反射信息、3)方法数组、4)注解信息 等等
public class Class类 {
Class aClass = null;
// private EnclosingMethodInfo getEnclosingMethodInfo() {
// Object[] enclosingInfo = getEnclosingMethod0();
// if (enclosingInfo == null)
// return null;
// else {
// return new EnclosingMethodInfo(enclosingInfo);
// }
// }
/**提供原子类操作
* Atomic operations support.
*/
// private static class Atomic {
// // initialize Unsafe machinery here, since we need to call Class.class instance method
// // and have to avoid calling it in the static initializer of the Class class...
// private static final Unsafe unsafe = Unsafe.getUnsafe();
// // offset of Class.reflectionData instance field
// private static final long reflectionDataOffset;
// // offset of Class.annotationType instance field
// private static final long annotationTypeOffset;
// // offset of Class.annotationData instance field
// private static final long annotationDataOffset;
//
// static {
// Field[] fields = Class.class.getDeclaredFields0(false); // bypass caches
// reflectionDataOffset = objectFieldOffset(fields, "reflectionData");
// annotationTypeOffset = objectFieldOffset(fields, "annotationType");
// annotationDataOffset = objectFieldOffset(fields, "annotationData");
// }
//提供反射信息,用于反射
// reflection data that might get invalidated when JVM TI RedefineClasses() is called
// private static class ReflectionData<T> {
// volatile Field[] declaredFields;
// volatile Field[] publicFields;
// volatile Method[] declaredMethods;
// volatile Method[] publicMethods;
// volatile Constructor<T>[] declaredConstructors;
// volatile Constructor<T>[] publicConstructors;
// // Intermediate results for getFields and getMethods
// volatile Field[] declaredPublicFields;
// volatile Method[] declaredPublicMethods;
// volatile Class<?>[] interfaces;
//
// // Value of classRedefinedCount when we created this ReflectionData instance
// final int redefinedCount;
//
// ReflectionData(int redefinedCount) {
// this.redefinedCount = redefinedCount;
// }
// }
//方法数组
// static class MethodArray {
// // Don't add or remove methods except by add() or remove() calls.
// private Method[] methods;
// private int length;
// private int defaults;
//
// MethodArray() {
// this(20);
// }
//
// MethodArray(int initialSize) {
// if (initialSize < 2)
// throw new IllegalArgumentException("Size should be 2 or more");
//
// methods = new Method[initialSize];
// length = 0;
// defaults = 0;
// }
//注解信息
// annotation data that might get invalidated when JVM TI RedefineClasses() is called
// private static class AnnotationData {
// final Map<Class<? extends Annotation>, Annotation> annotations;
// final Map<Class<? extends Annotation>, Annotation> declaredAnnotations;
//
// // Value of classRedefinedCount when we created this AnnotationData instance
// final int redefinedCount;
//
// AnnotationData(Map<Class<? extends Annotation>, Annotation> annotations,
// Map<Class<? extends Annotation>, Annotation> declaredAnnotations,
// int redefinedCount) {
// this.annotations = annotations;
// this.declaredAnnotations = declaredAnnotations;
// this.redefinedCount = redefinedCount;
// }
// }
}
我们都知道所有的java类都是继承了object这个类,在object这个类中有一个方法:getclass().这个方法是用来取得该类已经被实例化了的对象的该类的引用,这个引用指向的就是Class类的对象。
我们自己无法生成一个Class对象(构造函数为private),而 这个Class类的对象是在当各类被调入时,由 Java 虚拟机自动创建 Class 对象,或通过类装载器中的 defineClass 方法生成。
//通过该方法可以动态地将字节码转为一个Class类对象
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
我们生成的对象都会有个字段记录该对象所属类在CLass类的对象的所在位置。如下图所示:
如何获得一个Class类对象
请注意,以下这些方法获取的都是值,而不是对象本身。意思是某个类对应的Class对象已经在堆中生成以后,我们通过不同方式获取对这个Class对象的引用。而源码里的DefineClass才是真正将字节码加载到虚拟机的方法,会在堆中生成新的一个Class对象。
第一种办法,Class类的forName函数:
public class shapes{}
Class obj = Class.forName("shapes");
第二种办法,使用对象的getClass()函数
public class shapes{} shapes s1 = new shapes();
Class obj = s1.getClass();
Class obj1 = s1.getSuperclass();//这个函数作用是获取shapes类的父类的类型
第三种办法,使用类字面常量
//注意,使用这种办法生成Class类对象时,不会使JVM自动加载该类(如String类)。
//类对象的初始化阶段被延迟到了对静态方法或者非常数静态域首次引用时才执行。而其他办法会使得JVM初始化该类。
Class obj = String.class;
Class obj1 = int.class;
//包装类中的TYPE等价于基本类型的.class
//int.class == Integer.TYPE
使用Class类的对象来生成目标类的实例
获取一个Class类的对象后,可以用 newInstance() 函数来生成目标类的一个实例。然而,该函数并不能直接生成目标类的实例,只能生成object类的实例。
以下是生成实例的例子:
/**
*Class引用总是指向某个Class对象,Class引用表示的就是它所指向的对象的确切类型
*普通的类引用不会产生警告信息,普通的类引用可以被重新赋值为其他的Class对象
*泛化的Class引用,可以限制类型,让编译器强行执行额外的类型检查
*因为有了类型限制,所以使用泛化Class语法的对象引用不能指向别的类。尽量使用泛化来提高代码安全性。
**/
Class obj1 = int.class;
obj1 = double.class;//普通的类引用可以被重新赋值为其他的Class对象
Class<Integer> obj2 = int.class;
//obj2=double.class; 这一行代码是非法的,泛化的obj2不能改指向别的类
Class obj = shapes.class;
shapes newShape = obj.newInstance();
Class obj = Class.forName("shapes");
Object ShapesInstance = obj.newInstance();
//然而,有个灵活的用法,你可以用上界来让Class的对象指向基类的任何子类。
Class<? extends Number> obj = int.class;
obj = Number.class;
obj = double.class;
//因此,以下语法生成的Class对象可以指向任何类。
Class<?> obj = int.class;
obj = double.class;
obj = shapes.class;
//最后一个奇怪的用法是,当你使用这种泛型语法来构建你手头有的一个Class类的对象的基类对象时,必须采用以下的特殊语法
public class shapes{}
class round extends shapes{}
Class<round> rclass=round.class;
Class<? super round> sclass= rclass.getSuperClass();
//Class<shapes> sclass=rclass.getSuperClass();
//我们明知道,round的基类就是shapes,但是却不能直接声明 Class <shapes>,必须使用特殊语法,这就是泛化的作用
Class < ? super round >
Object类
Object类概念
Object类是java默认的提供的一个类,Object类是所有类的父类,也就是说任何一个类的定义的时候如果没有明确的继承一个父类的话,那么它就是Object的子类。
也就是说以下两种类的定义的最终效果是完全相同的:
class Person { }
class Person extends Object { }
Object类位于java.lang包中,java.lang包包含着Java最基础和核心的类,在编译时会自动导入。Object类没有定义属性,一共有13个方法,具体的类定义结构如下图:
Object类中的方法
首先简单地介绍一下这些方法:
1.clone方法
protected 方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。
2.getClass方法
final方法,获得运行时类型。返回的 Class 对象是由所表示类的 static synchronized 方法锁定的对象
3.toString方法
返回该对象的字符串表示。该方法用得比较多,一般子类都有覆盖。
4.finalize方法
该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。
5.equals方法
该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。
6.hashCode方法
该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。
一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。
7.wait方法
wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生。
(1)其他线程调用了该对象的notify方法。
(2)其他线程调用了该对象的notifyAll方法。
(3)其他线程调用了interrupt中断该线程。
(4)时间间隔到了。
此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
8.notify方法
该方法唤醒在该对象上等待的某个线程。
9.notifyAll方法
该方法唤醒在该对象上等待的所有线程。
Object类中最常用的方法:toString( )、equals()。
1、类构造器public Object()
大部分情况下,Java中通过形如 new A(args…)形式创建一个属于该类型的对象。其中A即是类名,A(args…)即此类定义中相对应的构造函数。通过此种形式创建的对象都是通过类中的构造函数完成。为体现此特性,Java中规定:在类定义过程中,对于未定义构造函数的类,默认会有一个无参数的构造函数,作为所有类的基类,Object类自然要反映出此特性,在源码中,未给出Object类构造函数定义,但实际上,此构造函数是存在的。
当然,并不是所有的类都是通过此种方式去构建,也自然的,并不是所有的类构造函数都是public。
2、registerNatives()方法
private static native void registerNatives();
registerNatives函数前面有native关键字修饰,其主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦。registerNatives函数一般都会在其后跟有静态代码块,用于执行该方法。
Java中,用native关键字修饰的函数表明该方法的实现并不是在Java中去完成,而是由C/C++去完成,并被编译成了.dll,由Java去调用。
private static native void registerNatives();
static {
registerNatives();
}
3、clone()方法
protected native Object clone() throws CloneNotSupportedException;
clone()方法又是一个被声明为native的方法,它不是Java原生方法。clone英文翻译为"克隆",其目的是创建并返回此对象的一个副本。Java术语表述为:clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。
首先来看看以下两种使用clone()方法的错误信息:
public class ObjectTest {
public static void main(String[] args) {
Object o1 = new Object();
Object clone = o1.clone();
// 报错:The method clone() from the type Object is not visible
//clone()方法被声明为protected,按理说类默认继承Object,满足“不同包中的子类可以访问protected方法”。
//这句话的情形应是使用子类引用去调用父类方法,此处是在子类中创建了一个父类,使用父类的引用直接调用其本身的方法,违反了包的访问权限
//下面针对上面的情况作了个改进,以下代码可以正常编译了
ObjectTest ot1 = new ObjectTest();
try {
ObjectTest ot2 = (ObjectTest) ot1.clone();
// 运行时抛出:java.lang.CloneNotSupportedException
//原因在于Java中的语法规定:没有实现Cloneable接口,并且Object子类直接调用Object类的clone()方法,则会抛出 CloneNotSupportedException异常。
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
Cloneable接口仅是一个表示接口,接口本身不包含任何方法,用来指示Object.clone()可以合法的被子类引用所调用。要使用clone()的类需要实现Cloneable接口。
最后修改为如下可执行的代码:
public class ObjectTest implements Cloneable {
public static void main(String[] args) {
ObjectTest ot1 = new ObjectTest();
try {
ObjectTest ot2 = (ObjectTest) ot1.clone();
System.out.println("ot2:" + ot2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
clone方法实现的是浅拷贝,只拷贝当前对象,并且在堆中分配新的空间,放这个复制的对象。但是对象如果里面有其他类的子对象,那么就不会拷贝到新的对象中。
浅拷贝和深拷贝
说拷贝之前要介绍一个概念:
- Java中的数据类型分为基本数据类型和引用数据类型。对于这两种数据类型,在进行赋值操作、用作方法参数或返回值时,会有值传递和引用(地址)传递的差别。
浅拷贝
- 浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了引用地址,就会影响到另一个对象。
- 对于数据类型是基本数据类型的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
- 对于数据类型是引用数据类型的成员变量,比如说成员变量是某个数组、某个类的对象等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为实际上两个对象的该成员变量都指向同一个实例。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
深拷贝
- 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
首先介绍对象图的概念。设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
实现深拷贝有两种方式:
- 一种是实现Clonable接口,覆盖并实现clone方法,除了调用父类中的clone方法得到新的对象,还要将该类中的引用变量也clone出来。
- 另一种是通过对象序列化实现深拷贝,将对象序列化为字节序列后,默认会将该对象的整个对象图进行序列化,再通过反序列即可实现深拷贝。不过要注意的是,如果某个属性被
transient
修饰,那么该属性就无法被拷贝了。
如果只是用Object中默认的clone方法,是浅拷贝的。
深拷贝和浅拷贝的差别
- new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。
- 分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化。构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。
- 而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域。
- 填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间。而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间;当然,基本数据类型还是会重新拷贝一份的。
4、getClass()方法
public final native Class<?> getClass();
getClass()也是一个native方法,返回的是此Object对象的类对象/运行时类对象Class<?>。效果与Object.class相同。
常常用于反射中,相关类对象概念可参考本章前段对于Class类的内容。
5、equals方法
public boolean equals(Object obj);
==
与equals
在Java中经常被使用,==
与equals
的区别:
==
表示的是变量值完成相同(对于基础类型,地址中存储的是值,引用类型则存储指向实际对象的地址);equals
表示的是对象的内容完全相同,此处的内容多指对象的值/属性。
对于equals
比较的是对象的内容这一说法是不严禁的,由于大多包装类型中对equals
方法进行了重写,比如以下Integer
类的源码:
@Override
public boolean equals(Object o){
return (o instanceof Integer) && (((Integer) o).value == value);
//比较的是Integer对象的值,且判断该对象是否为Integer对象,须满足两个条件
}
Object类中关于equals()方法的定义:
public boolean equals(Object obj) {
return (this == obj);
}
由此可见,Object原生的equals()
方法内部调用的正是==
,与==
具有相同的含义。既然如此,为什么还要定义此equals()
方法?
equlas()
方法的正确理解应该是:判断两个对象是否相等。
那么判断对象相等的标尺又是什么?这个标尺不是固定的,许多基本数据类型的包装类对equlas()
方法进行了重写,重新定义了标尺,这就是多态的魅力,可以增加类的功能型和实际编码的灵活性。
但是修改标尺的时候需要注意,Java中的约定:重写equals()
方法时必须重写hasCode()
方法。hash值是判断两个对象是否相同的条件。
6、hashCode()方法
public native int hashCode();
hashCode()方法返回一个整形数值,表示该对象的哈希码值。
Java手册中写道——hashCode 的常规协定是:
- 在 Java 应用程序执行期间,在对同一对象多次调用 hashCode 方法时,必须一致地返回相同的整数,前提是将对象进行 equals 比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
- 如果根据equals(Object)方法,两个对象是相等的,那么对这两个对象中的每个对象调用 hashCode 方法都必须生成相同的整数结果。
- 反之,两个对象调用hasCode()返回的哈希码相等,这两个对象不一定相等。
因此equals
与hashCode
之间的逻辑是:equals
相等的两个对象hashCode
也一定相等,但是hashCode
相等的两个对象,不一定equals
。
可能有人在此产生疑问:既然比较两个对象是否相等的唯一条件(也是重要条件)是equals,那么为什么还要弄出一个hashCode(),并且进行如此约定,弄得这么麻烦?
其实,这主要体现在hashCode()方法的作用上,其主要用于增强散列(Hash)表的性能。Java中创建的对象是保存在堆中的;为了提高查找的速度而使用了散列(Hash)值来对应对象在jvm中的内存地址,hashCode()方法就是用来计算对应的散列(Hash)值的。
那为什么会存在散列(Hash)值相同对象不一样的情况,主要是两个原因:
- 散列的长度是有限的,总会有用完的时候。
- 存在重写散列(Hash)算法的对象,算法不同导致散列(Hash)碰撞。
7、toString()方法
public String toString();
toString()方法返回该对象的字符串表示。看一下Object中的具体方法体:
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
//getClass()返回对象的类对象,getClassName()以String形式返回类对象的名称(含包名)。
//Integer.toHexString(hashCode())则是以对象的哈希码为实参,以16进制无符号整数形式返回此哈希码的字符串表示形式。
}
toString()是由对象的类型和其哈希码唯一确定,同一类型但不相等的两个对象分别调用toString()方法返回的结果可能相同。
toString()方法相信大家都经常用到,大多数情况下都将它重写输出我们想要的信息。
8、wait() notify() notifAll()
public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void notify();
public final void notifyAll();
一说到wait(…) / notify() | notifyAll()几个方法,首先想到的是线程。确实,这几个方法主要用于java多线程之间的协作。先具体看下这几个方法的主要含义:
wait()
:调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notify()/notifyAll()方法。
wait(long timeout)/wait(long timeout, int nanos)
:调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notisfy()/notisfyAll()方法,或超过指定的超时时间量。
notify()/notifyAll()
:唤醒在此对象监视器上等待的单个线程/所有线程。
wait(…) / notify() | notifyAll()一般情况下都是配套使用。下面来看一个简单的例子:
public class ThreadTest {
public static void main(String[] args) {
MyRunnable r = new MyRunnable();
Thread t = new Thread(r);
t.start();
synchronized (r) {
try {
System.out.println("main thread 等待t线程执行完");
r.wait();
System.out.println("被notity唤醒,得以继续执行");
} catch (InterruptedException e) {
e.printStackTrace();
System.out.println("main thread 本想等待,但被意外打断了");
}
System.out.println("线程t执行相加结果" + r.getTotal());
}
}
}
class MyRunnable implements Runnable {
private int total;
@Override
public void run() {
synchronized (this) {
System.out.println("Thread name is:" + Thread.currentThread().getName());
for (int i = 0; i < 10; i++) {
total += i;
}
notify();
System.out.println("执行notif后同步代码块中依然可以继续执行直至完毕");
}
System.out.println("执行notif后且同步代码块外的代码执行时机取决于线程调度");
}
public int getTotal() {
return total;
}
}
运行结果:
main thread 等待t线程执行完
Thread name is:Thread-0
执行notif后同步代码块中依然可以继续执行直至完毕
执行notif后且同步代码块外的代码执行时机取决于线程调度 //此行输出位置有具体的JVM线程调度决定,有可能最后执行
被notity唤醒,得以继续执行
线程t执行相加结果45
既然是作用于多线程中,为什么却是Object这个基类所具有的方法?原因是理论上任何对象都可以视为线程同步中的监听器,且wait(…)/notify()|notifyAll()方法只能在同步代码块中才能使用。
9、finalize()方法
protected void finalize();
finalize方法主要与Java垃圾回收机制有关。首先我们看一下finalized方法在Object中的具体定义:
protected void finalize() throws Throwable { }
finalize方法的调用时机是怎么样的呢?
首先,Object中定义finalize方法表明Java中每一个对象都将具有finalize这种行为,其具体调用时机在:JVM准备对此对形象所占用的内存空间进行垃圾回收前,将被调用。由此可以看出,此方法并不是由我们主动去调用的(虽然可以主动去调用,此时与其他自定义方法无异)。
我们发现Object类中finalize方法被定义成一个空方法,为什么要如此定义呢?
当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。子类重写 finalize 方法,以配置系统资源或执行其他清除。
CLass类和Object类的关系
- Object类和Class类没有直接的关系。
- Object类是一切java类的父类,对于普通的java类,即便不声明,也是默认继承了Object类。典型的,可以使用Object类中的toString()方法。
- Class类是用于java反射机制的,一切java类,都有一个对应的Class对象,他是一个final类。Class类的实例表示,正在运行的 Java 应用程序中的类和接口。
知乎里有个与此相关且很有趣的问题:
Java的对象模型中:
1 所有的类都是Class类的实例,Object是类,那么Object也是Class类的一个实例。
2 所有的类都最终继承自Object类,Class是类,那么Class也继承自Object。
3 这就像是先有鸡还是先有蛋的问题,请问实际中JVM是怎么处理的?
- 这个问题中,第1个假设是错的:java.lang.Object是一个Java类,但并不是java.lang.Class的一个实例。后者只是一个用于描述Java类与接口的、用于支持反射操作的类型。这点上Java跟其它一些更纯粹的面向对象语言(例如Python和Ruby)不同。
- 而第2个假设是对的:java.lang.Class是java.lang.Object的派生类,前者继承自后者。虽然第1个假设不对,但“鸡蛋问题”仍然存在:在一个已经启动完毕、可以使用的Java对象系统里,必须要有一个java.lang.Class实例对应java.lang.Object这个类;而java.lang.Class是java.lang.Object的派生类,按“一般思维”前者应该要在后者完成初始化之后才可以初始化。
- 事实是:这些相互依赖的核心类型完全可以在“混沌”(boostrap过程)中一口气都初始化好,然后对象系统的状态才叫做完成了“bootstrap”,后面就可以按照Java对象系统的一般规则去运行。JVM、JavaScript、Python、Ruby等的运行时都有这样的bootstrap过程。
- 在“混沌”(boostrap过程)里,JVM可以为对象系统中最重要的一些核心类型先分配好内存空间,让它们进入[已分配空间]但[尚未完全初始化]状态。此时这些对象虽然已经分配了空间,但因为状态还不完整所以尚不可使用。
- 然后,通过这些分配好的空间把这些核心类型之间的引用关系串好。到此为止所有动作都由JVM完成,尚未执行任何Java字节码。然后这些核心类型就进入了[完全初始化]状态,对象系统就可以开始自我运行下去,也就是可以开始执行Java字节码来进一步完成Java系统的初始化了。