@TryLoveCatch
2022-04-17T11:44:50.000000Z
字数 4040
阅读 914
Java知识体系
这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。
Student student = new Student();
通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象。
Student student2 = (Student)Class.forName("Student类全限定名").newInstance();
或者:
Student stu = Student.class.newInstance();
java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象,该方法和Class类中的newInstance方法很像,但是相比之下,Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数。
事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。
用clone方法创建对象的过程中并不会调用任何构造函数,必须先实现Cloneable接口并实现其定义的clone方法。
当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,我们需要让我们的类实现Serializable接口。
从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。
当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。
在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。
在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。
在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。
我们在定义(声明)实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。
如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。
实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
public class InstanceVariableInitializer {
private int i = 1;
private int j = i + 1;
public InstanceVariableInitializer(int var){
System.out.println(i);
System.out.println(j);
this.i = var;
System.out.println(i);
System.out.println(j);
}
{ // 实例代码块
j += 3;
}
public static void main(String[] args) {
new InstanceVariableInitializer(8);
}
}/* Output:
1
5
8
5
*///:~
上面代码中的构造函数相当于:
private int i = 0;
private int j = 0;
public InstanceVariableInitializer(int var){
super();
// 这里!!!!
i = 1;
j = i + 1;
j += 3;
System.out.println(i);
System.out.println(j);
this.i = var;
System.out.println(i);
System.out.println(j);
}
实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。
每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。
在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。
事实上,这一点是在构造函数中保证的:Java强制要求Object对象之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数。
如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用。
我们显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面,也就是必须是构造函数的第一条指令。
正因为如此,Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。
实例化一个类的对象的过程是一个典型的递归过程。
//父类
class Foo {
int i = 1;
Foo() {
System.out.println(i);
int x = getValue();
System.out.println(x); // 重点!!!!
}
{
i = 2;
}
protected int getValue() {
return i;
}
}
//子类
class Bar extends Foo {
int j = 1;
Bar() {
j = 2;
}
{
j = 3;
}
@Override
protected int getValue() {
return j;
}
}
public class ConstructorExample {
public static void main(String... args) {
Bar bar = new Bar();
System.out.println(bar.getValue());
}
}/* Output:
2
0
2
*///:~
根据上面说的,我们整理一下这两个类构造函数:
//Foo类构造函数的等价变换:
Foo() {
super();
i = 1;
i = 2;
System.out.println(i);
int x = getValue();
System.out.println(x);
}
//Bar类构造函数的等价变换
Bar() {
Foo();// super()
j = 1;
j = 3;
j = 2
}
重点处输出是0,为什么呢?
因为在执行Foo的构造函数的过程中,由于Bar重载了Foo中的getValue方法,所以根据Java的多态特性可以知道,其调用的getValue方法是被Bar重载的那个getValue方法。但由于这时Bar的构造函数还没有被执行,因此此时j的值还是默认值0,因此输出是0。
在Java知识体系之类加载机制里面,我们已经了解了类的初始化。
类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕。
在一个类的生命周期中,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用多次,只要程序员还在创建对象。
也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次。
父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。