/*
* 几大原则
* 一、静态成员变量(Static)
* 1、静态成员变量为类变量,所有对象共享同一内存空间
* 2、静态成员变量的声明和定义仅在首次加载类时执行一次
* 3、首次加载类时首先对所有静态成员变量根据类型默认赋初值,然后再对有右值的附右值
* 二、静态初始块
* 1、静态初始化块仅在首次加载类时执行一次
* ······多个静态成员变量与静态始化快参照出现顺序先后执行······
* 三、动态成员变量
* 1、动态成员变量定义在每次实例化对象时在构造函数之前执行
* 四、动态初始化块
* 1、动态初始化块在每次实例化对象时在构造函数之前执行
* ······多个动态成员变量与动态初始化块参照出现顺序先后执行······
* 总结:总的来说,在不涉及继承的前提下,当首次加载类时,按照如下顺序执行
* 1、按照出现顺序先后执行静态成员变量定义与静态初始化块
* 2、按照出现顺序先后执行动态成员变量定义与动态初始化块
* 3、执行构造函数
* 再次实例化对象时只执行第2、3步即可
*
* ············成员变量与定义与初始化块先于构造函数执行·········
* 五、当涉及到继承时,按照如下顺序执行
* 1、执行父类的静态成员变量定义与静态初始化块,执行子类的静态成员变量定义与静态初始化块
* 2、执行父类的非静态成员变量定义与动态初始化块,执行父类构造方法
* 3、执行子类的非静态成员变量定义与动态初始化块,执行子类构造方法
* 另:父类构造方法中用到的方法如果已被子类重写,那么在构造子类对象时在调用父类构造函数中使用子类重写的方法
*/
1.实例变量和类变量的内存分配
类变量 :使用static修饰的成员变量是类变量,属于该类本身
实例变量:没有使用static修饰的成员变量是实例变量,属于该类的实例
由于同一个JVM内每个类只对应一个Class对象,因此同一个JVM内的一个类的类变量只需一块内存空间。
对于实例变量而言,该类每创建一次实例,就需要为实例变量分配一块内存空间,所以,程序中有几个实例,实例变量就需要几块内存空间。
2.类变量的初始化时机总是处于实例变量的初始化之前
我们先看下下面三段代码:
1)因为两个实例变量都是在创建变量的时候才开始分配空间,此时num2还没有分配,所以前向引用就会出现编译错误。
1 int num = num2 + 3; //非法前向引用,会报错 2 int num2 = 2
2)因为两个类变量在JVM加载类的时候分配空间,此时num2还没有分配,所以前向引用就出现编译错误。
1 static int num = num2 + 3; //非法前向引用,会报错 2static int num2 = 2
3)因为类变量num2在JVM加载类的时候空间已经分配好,而num在创建实例的时候才分配空间,此时num2已经分配成功了,所以num前向引用成功。
1 int num = num2 + 3; //正确使用 2 static int num2 = 2;
由上面三段代码块就可以验证得:类变量的初始化时机总是处于实例变量的初始化之前
3.Java对象的初始化方式及其执行顺序
Java对象的初始化方式有三种:1)构造器 2)初始化块 3)定义变量时指定初始化值
如果这三种初始化方式同时出现,也要注意,他们也有一个执行顺序的规定:
1)静态初始化块只在类第一次创建对象的时候运行一次,后面就不会再运行,而类在每次创建对象时,非静态初始化块总是会运行一次。
public class Test{
static {
System.out.println("执行---静态初始化代码块.");
}
{
System.out.println("执行---非静态初始化代码块.");
}
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
System.out.println("创建第 " + i + " 个对象");
new Test();
System.out.println();
}
}
}
运行结果:
2)构造器每次创建对象时,构造器必然有执行的机会,此时,非静态初始化块必定也将获得机会并且运行在构造器之前
public class Test{
{
System.out.println("执行---非静态初始化代码块.");
}
public Test() {
System.out.println("执行---构造器.");
}
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
System.out.println("创建第 " + i + " 个对象");
new Test();
System.out.println();
}
}
}
运行结果:
3)定义变量时指定的初始化值和初始化块中指定的初始值的执行顺序与他们在源程序中的排列顺序相同。
验证代码一:
public class Test{
String i = "定义变量时指定的初始化值";
{
i = "初始化块中指定的初始值";
}
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
System.out.println("创建第 " + i + " 个对象");
System.out.println(new Test().i);
System.out.println();
}
}
}
运行结果
验证代码二 :
public class Test{
{
i = "初始化块中指定的初始值";
}
String i = "定义变量时指定的初始化值";
public static void main(String[] args) {
for (int i = 1; i <= 2; i++) {
System.out.println("创建第 " + i + " 个对象");
System.out.println(new Test().i);
System.out.println();
}
}
}
运行结果:
4.关于父子实例的内存控制
(一般情况下是不用内部类来验证的,但是都是一样的啦,我偷懒下,所以使用了内部类,大家原谅哈)
1)当子类重写父类方法后,父类表面上只是调用属于自己的被子类重写的方法。
public class Test{
class Base {
Base() {
this.info();
}
public void info() {
System.out.println("Base");
}
public void getInfo() {
info();
}
}
public class Child extends Base{
@Override
public void info() {
System.out.println("Child");
}
}
public static void main(String[] args) {
Test test = new Test();
Base base = test.new Child();
base.info();
base.getInfo();
}
}
运行结果:
2)上述是属于多态中方法的体现,但是方法有多态,实例变量无多态。
解释下“方法有多态,变量无多态”这句话:意思是,不管怎样,父类表面上只是调用属于自己的被子类重写的方法。而变量不一样,假设父类和子类都有同一个变量名的实例变量,向上转型后,通过父类访问的实例变量得到的值是自身的而非子类的。向下转型后,通过子类访问的实例变量得到的值是自身的而非父类的。
很多书上或教学视频上都讲,创建一个子类对象的时候,Java 会顺着继承结构往上一直找到 Object,然后从 Object 开始往下依次执行构造函数。先执行父类的构造函数,然后在其子类中会创建一个成员变量指向他的父类。其实这个说法是错误的,系统并不会真正的去创建父类对象,只是在子类对象中不仅保存了本身的实例变量,还有它父类的全部实例变量。
public class Test{
class Base { //父类
int i = 2;
}
public class Child extends Base{ //子类
int i = 20;
}
public static void main(String[] args) {
Test test = new Test();
Child child = test.new Child();
Base base = child;
System.out.println(" Base.i : " + base.i);
System.out.println("Child.i : " + child.i);
}
}
运行结果:
5.final修饰符
final变量在编译时就被确定下来了,相当于一个直接量。
1)final修饰的实例变量赋值时机:
定义final实例变量时 指定初始值
在非静态初始化模块中为final实例变量指定的初始值
在构造器中为final实例变量指定初始值
2)final修饰的类变量赋值时机:
定义final类变量时指定初始值
在静态初始化模块中为final实例变量指定的初始值