package test1.demo; class Person{ String name; int age; static String nationality; public Person(){} public Person(String name,int age){ this.name = name; this.age = age; } public Person(String name,int age,String nationality){ this.name = name; this.age = age; Person.nationality = nationality; } public void show(){ System.out.println(name+"--"+age+"--"+nationality); } } public class PersonDemo { public static void main(String[] args) { Person p1 = new Person("张",12,"中国"); p1.show(); Person p2 = new Person("陈",14,"美国"); p2.show(); Person p3 = new Person("周",16,"日本"); p3.show(); p3.nationality = "法国"; p1.show(); p2.show(); p3.show(); } }
针对这段代码,来说一下具体的创建对象的过程
具体的加载类的时机,不完整的说可以分为以下两种(其余的情况比较复杂,这里不说):
(1)遇到new关键字的时候,将需要将创建对象的类加载初始化
(2)Java虚拟机启动的时候,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
满足这些时机时,就会触发这一步,将 .class文件读入内存
然后是将类的静态成员变量和静态成员方法,加载到方法区的静态区里面
(1)把.class文件中的所有静态内容加载到方法区下的静态区域内
(2)静态内容加载完成之后,对所有的静态变量进行初始化,初始化的顺序是
默认初始化 ——> 显示初始化 ——> 静态代码块
默认初始化完成后才会进行显式初始化,显式初始化完成后才是静态代码块
具体的初始化过程与含义往下看
将类的非静态变量和非静态方法加载到方法区的非静态区域内,这个方法包括构造方法。
2、3步的顺序可以记为:在.class加载到方法区时,先加载父类再加载子类;先加载静态内容,再加载非静态内容
到这里为止,整个类的加载就完成了。
.class文件读入内存,让类加载完之后会生成一个Class类型对象,这个对象会包含类的数据结构,包含所有信息(比如类名,方法,变量等)
这个Class对象用来创建这个类的所有对象(一个类只能有一个Class对象)
程序开始时,先将main方法加载到栈里面,然后在栈内存中开辟一个新的空间,用来存放新建的引用变量,比如Person p1,这里并不是新建了一个对象,而是新建了一个变量用来指向堆内存中的对象。
直到遇到new关键字的时候,才真的在堆内存中划分一块空间用来存储对象。
(1)使用new关键字创建你对象时,会先在堆内存中开辟一块空间。
(2)给开辟的空间分配一个地址,比如说0x001
(3)把对象的所有非静态成员变量加载到所开辟的空间下,加载完非静态变量后还有
空间还有一个静态标记,用来指向方法区中的类的静态变量的地址(一个类的所有对象共享这个类的静态变量)
空间还会有一个方法标记,指向这个对象所在类的方法在方法区中的地址
(4)非静态成员变量加载到空间后会默认初始化(默认初始化的默认值:数字为 0,字符为 null,布尔为 false,而所有引用被设置成 null)
然后调用构造方法,构造方法会被调用栈里面执行,入栈执行时,分为两部分:
一、先执行构造函数中的隐式三步,三步分别为
▶ 1. 执行super()语句(即先初始化父类的构造方法)
▶ 2. 对开辟空间下的所有非静态成员变量进行显示初始化
▶ 3. 执行构造代码块
二、执行构造方法中的代码
一般来说,成员变量最后的值都是调用构造函数时传进去的值,也就是第二步“执行构造方法中的代码”
对象调用方法时,根据方法标记到方法区中找到方法,然后方法入栈。方法调用完后就会被清除。
之后的show方法中需要调用对象的成员变量的时候,先根据栈里面的引用变量找到堆内存中的对象空间,然后发现空间里面只有两个实例对象name和age,没有静态变量,此时就会根据静态标记到方法区去找。找到后返回。
最后还修改了一下静态变量,根据这个路径找到静态变量:引用变量——>堆内存空间——>方法区——>静态变量,修改静态变量后,之前几个对象的静态标记依旧指向的是这个地址。所以修改了静态变量后,所有对象的静态变量结果都会改变。而实例变量属于对象所有,一个对象改变自己的实例变量不会影响到其他的实例对象。
所有语句结束后,main方法出栈,程序运行完毕
默认初始化(null,0)——> 显示初始化(name=“zhang”,age=16)——> 构造代码块 ——> 构造方法
package review; class Demo1{ //默认初始化 public String name; public int age; //构造方法 public Demo1(){} @Override public String toString() { return "Demo{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class gouzaoDemo1 { public static void main(String[] args) { Demo1 d = new Demo1(); System.out.println(d.toString()); } }
结果为
package review; class Demo{ //显式初始化 public String name = "zhang"; public int age = 16; //构造代码块 { name = "cao"; age = 12; } //构造方法 public Demo(){} public Demo(String name , int age){ this.name = name; this.age = age; } @Override public String toString() { return "Demo{" + "name='" + name + '\'' + ", age=" + age + '}'; } } public class gouzaoDemo { public static void main(String[] args) { Demo d1 = new Demo(); System.out.println(d1.toString()); Demo d2 = new Demo("chen",20); System.out.println(d2.toString()); } }
结果为
package test1; public class VariableDemo { public static void main(String[] args) { int a = 1; int b = 2; change(a,b); System.out.println(a+"--"+b); } public static void change(int a,int b){ a = 3; b = 4; } }
因为当a和b是非静态变量的时候,方法内部使用的是新建的局部变量,返回值类型是void,所以局部变量的生命周期会随着方法的调用结束而结束,不会影响到外面的变量。
package test1; public class VariableDemo { static int a = 1; static int b = 2; public static void main(String[] args) { change(); System.out.println(a+"--"+b); } public static void change(){ a = 3; b = 4; } }
调用静态变量的时候,会直接修改方法区里面的静态变量的值,所以外面的静态变量的值也会受到影响。
package test1; import java.util.Arrays; public class VariableDemo1 { public static void main(String[] args) { int a = 5; int b = 10; change(a,b); System.out.println(a+"---"+b); int[] arr = {1,2,3,4,5}; changeArr(arr); System.out.println(Arrays.toString(arr)); } public static void change(int a,int b){ a = 20; b = 30; } public static void changeArr(int[] arr){ for(int i=0;i<arr.length;i++){ if(arr[i]%2==0){ arr[i] += 10; } } } }
会发现都是void类型的方法,change方法没有改变a,b的值,但是changeArr方法改变了arr数组的值
这是因为基本数据类型作为参数传递的时候,传递的是具体的值,本身变量里面的值不参与运行,不会发生变化。基本数据类型的值被调入栈内存中进行运行,方法结束后会随着方法一起消失。栈内存中放着的是局部变量,而变量本身的实体在堆内存中,只在栈里面修改不会影响到堆内存,除非修改void类型,让方法有返回值。
而int[] arr不是基本数据类型,是引用数据类型。引用数据类型作为参数传递的时候,传递的是地址值,所以在方法里面改变的是实际空间里面的值,因此会影响到外面的数据的值。引用数据类型会在堆内存中开辟空间,并执行修改。