本文介绍一个java对象的建立过程,代码上一句简单的new Demo(),实际包含了很多细微的过程,这里将此过程掰开揉碎了来详细描述。
先介绍对象建立过程中涉及的多个概念。
构造函数:
作用:给对象进行初始化。
构造函数特点:函数名与类名相同;不需要定义返回值类型,不能用return语句。
每个类中都必须有构造函数,类中没有显式的定义构造函数时,系统会默认给该类加入一个空参数的构造函数;当在类中自定义了构造函数后,默认的构造函数就没有了。
同一个类中多个构造函数以重载形式存在。
对象一建立就会调用与之对应的构造函数,且只调用这一次。
构造代码块
类中用{}括起来的一块代码,用于给对象进行初始化;对象一建立就运行,而且优先于构造函数执行。
构造代码块是给所有对象进行统一初始化(抽取不同对象共性的初始化内容),而构造函数是给对应的对象初始化。
构造代码块的位置,不一定要放在类中所有成员函数之前,可以放在后面,用{}标识即可。
静态代码块:
形式为:static{
执行语句;
}
特点:随着类的加载而执行,只执行一次。
作用:用于给类进行初始化。
对象初始化过程是这样的:
1. 首先加载类到内存中:JVM加载器将.class文件从硬盘加载到内存
2. 对于类变量(就是静态变量),系统默认初始化为null或0等默认值
3. 执行静态代码块或静态变量的显式初始化(代码中谁在前面谁就先执行)
4. 在堆内存中开辟空间,分配内存地址
5. 对于成员变量,首先系统默认初始化为null或者0等默认值
6. 然后执行构造代码块或成员变量显示初始化(代码中谁在前面谁就先执行)
7. 最后才是构造方法初始化
8 将堆内存中的地址付给栈内存中的引用
//验证 java对象的初始化顺序
public class InitStatic
{
public static String staticField = "静态变量";// 静态变量
public String field = "成员变量";// 变量
static{
System.out.println(staticField);
System.out.println("静态初始化块1,位置在前");
}// 静态初始化块
{
System.out.println(field);
System.out.println("构造代码块1,位置在前");
}// 初始化块
static{
System.out.println("静态初始化块2,位置在后");
}// 静态初始化块
public InitStatic() {
System.out.println("构造函数");
}// 构造函数
public static void main(String[] args) {
new InitStatic();
}
{
System.out.println("构造代码块2,位置在后");
}// 初始化块
}
运行结果是:
静态变量
静态初始化块1,位置在前
静态初始化块2,位置在后
成员变量
构造代码块1,位置在前
构造代码块2,位置在后
构造函数
如果一个对象继承自一个父类呢?对象初始化过程是怎么样的?不能想当然,先用代码验证一下:
//验证一个有父类的java对象的初始化顺序
class InitFu{
public static String staticFuField = "父类静态变量";
public String field="父类成员变量";
public int f;
static{
System.out.println(staticFuField);
System.out.println("父类静态初始化块");
}// 静态初始化块
{
System.out.println("父类成员变量默认值:"+f);
System.out.println(field);
System.out.println("父类构造代码块");
}// 初始化块
InitFu(){
System.out.println("Fu类初始化");
}
}
public class InitStatic extends InitFu
{
public static String staticField = "子类静态变量";// 静态变量
public String field = "子类成员变量";// 变量
public int z;
static{
System.out.println(staticField);
System.out.println("子类静态初始化块1,位置在前");
}// 静态初始化块
{
System.out.println("子类成员变量还是默认值:"+z);
System.out.println(field);
System.out.println("子类构造代码块1,位置在前");
}// 初始化块
static{
System.out.println("子类静态初始化块2,位置在后");
}// 静态初始化块
public InitStatic() {
System.out.println("子类构造函数");
}// 构造函数
public static void main(String[] args) {
new InitStatic();
}
{
//System.out.println(field);
System.out.println("子类构造代码块2,位置在后");
}// 初始化块
}
运行结果是:
父类静态变量
父类静态初始化块
子类静态变量
子类静态初始化块1,位置在前
子类静态初始化块2,位置在后
父类成员变量默认值:0
父类成员变量
父类构造代码块
Fu类初始化
子类成员变量还是默认值:0
子类成员变量
子类构造代码块1,位置在前
子类构造代码块2,位置在后
子类构造函数
由上面的验证可见,一个有父类的java对象的初始化过程是:
1. 首先加载父类class文件到内存中
2. 对于父类静态变量,系统默认初始化为null或0等默认值
3. 执行父类静态代码块或父类静态变量的显式初始化(代码中谁在前面谁就先执行)
4. 加载子类class文件到内存中
5. 对于子类静态变量,系统默认初始化为null或0等默认值
6. 执行子类静态代码块或子类静态变量的显式初始化(代码中谁在前面谁就先执行)
7. 在堆内存中为子类开辟空间,分配内存地址(并没有new InitFu(),所以堆内存中不会有父类的空间)
8. 子类从父类继承的成员变量和子类自己的变量默认初始化,系统会默认初始化为null或者0等默认值
9. 执行父类构造代码块或子类从父类继承的成员变量显式初始化(代码中谁在前面谁就先执行)
10. 父类构造方法初始化
11. 执行子类构造代码块或子类自己的成员变量显式初始化(代码中谁在前面谁就先执行)
12. 子类构造方法初始化
13. 将堆内存中的地址付给栈内存中的引用
下面再介绍2个常用的关键字,this和static关键字,这2个在对象初始化过程中也有涉及。
this的2个作用:
1. this关键字用于区分局部变量和成员变量同名情况
this代表它所在函数所属对象的引用,简单说,哪个对象在调用this所在的函数,this就代表哪个对象。
每个非静态成员函数在被调用执行时,在栈内存空间中都会有一个this引用变量,指向调用该函数的对象
使用场景:当定义类中功能时,该函数内部要用到调用该函数的对象时,这时用this来表示这个功能。但凡本类功能内部使用了本类对象,都用this表示。
2. this语句用于构造函数之间互相调用(不能用于一般函数中)
构造函数间调用不能使用函数名;
不允许2个构造函数间相互调用;
一个类中可能有多个构造函数,但可能只对外提供一个构造函数,这时就可能出现构造函数间调用;
this语句只能定义在构造函数的第一行,因为初始化动作要先执行。
static关键字
static是一个修饰符,用于修饰成员(成员变量,成员函数)。
static的特点:
1. 随着类的加载而加载到方法区,随着类的消失而消失,从而生命周期最长.
除栈内存和堆内存外,还有一个方法区/共享区/数据区,类中static成员变量和类中的成员方法都放在这个区域。
static成员变量和static成员函数放在方法区中的静态区,非静态方法放在方法区中的非静态区。
成员方法有局部变量(包括形参)时,对象调用此方法时,会在栈内存中给这些局部变量开辟空间;除此之外,非静态方法内部默认有2个局部引用变量,一个this指向自身对象,一个super指向父类对象,静态方法内部没有this和super引用变量
2. 优先于对象而存在,普通成员变量随着对象创建而存在于堆内存,随着对象的消失而消失;
3. 被所有对象所共享;
4. static成员除了由对象调用外,还可直接被类调用,类名.静态成员。
同一个类中的方法相互调用时,静态方法名前可省略类名,非静态方法名前省略this.
static使用注意事项:
1. 静态方法只能访问静态成员(包括成员变量和成员方法);
---注:可以在静态方法中创建或传递一个引用变量,这样就可以访问该引用变量指向的对象的非静态变量和方法了;可以说静态方法只是不能直接访问非静态成员,而可以通过引用变量间接访问。
Static的上述特点和使用注意事项也明确了static的用途:
当对象中出现共享数据时,该数据应该被定义成静态变量;
当功能内部没有访问到非静态数据(对象的特有数据),该功能应该被定义成静态函数。--典型应用的是工具类
2. 静态方法中不能显式或隐式地使用this,super关键字,因为static成员优先于对象存在