① 默认初始化
② 显式初始化
⑤ 代码块中初始化
③ 构造器中初始化
#############################
④ 有了对象以后,通过"对象.属性"或"对象.方法"的方法进行赋值
(造对象之前叫初始化,造对象之后叫赋值)
① - ② - ③ - ④
⑤ 代码块中初始化应该放在哪?
【举例】
先看一段代码:
package yuyi06;
/**
* ClassName: FieldTest
* Package: yuyi06
* Description:
*
* @Author 雨翼轻尘
* @Create 2023/11/19 0019 16:25
*/
public class FieldTest {
public static void main(String[] args) {
Order o1=new Order();
System.out.println(o1.orderId); //1
}
}
class Order{
int orderId=1;
}
输出结果:
这个很简单,显而易见。
现在整一个代码块,看一下它和显式赋值谁先谁后。
public class FieldTest {
public static void main(String[] args) {
Order o1=new Order();
System.out.println(o1.orderId); //2
}
}
class Order{
int orderId=1;
{
orderId=2;
}
}
输出结果:
那么一定是先有1,后有2。所以代码块初始化肯定是在显示初始化之后。
接下来是构造器和代码块。
创建一个空参构造器,那么在创建对象的时候一定会调用它。
ublic class FieldTest {
public static void main(String[] args) {
Order o1=new Order();
System.out.println(o1.orderId); //3
}
}
class Order{
int orderId=1;
{
orderId=2;
}
public Order(){
orderId=3;
}
}
输出结果:
结果是3,所以3将2覆盖了。所以代码块初始化在构造器初始化前面。
所以目前来看,执行顺序是这样的:① - ② - ⑤ - ③ - ④
将光标放在Order类中,看一下字节码文件。
插件在这里:
先运行然后重新编译一下,确保生成的字节码文件和代码一致。
然后点击这个即可:
构造器会以
方法的方式呈现在字节码文件中,如下:
看一下代码:
方法里面对应的是个栈帧,栈帧里面会放局部变量,
aload_0
就是指局部变量第0个位置–this
,表示当前正在创建的对象,通过aload_0调用现在的方法。
如下:
画个图解释一下Code的意思:
根据上面得出来的结论,代码块赋值在显式赋值之后,那么将它们俩的代码换个位置呢?
如下:
public class FieldTest {
public static void main(String[] args) {
Order o1=new Order();
System.out.println(o1.orderId);
}
}
class Order{
{
orderId=2;
}
int orderId=1;
public Order(){
//orderId=3;
}
}
输出结果:
怎么是1了呢?肯定是先有2,后有1。
看字节码文件:
这样来看,代码块赋值又先于了显示赋值。
刚才的① - ② - ⑤ - ③ - ④ 明显不太对。
②和⑤就是看谁先声明,谁就先执行。
所以应该是这样的:① - ②/⑤ - ③ - ④
为啥将代码块写在显示赋值上面,不会报错,这时候变量还未声明啊?
其实这个地方一直有个误区,举个例子:
可以看到,在eat()
方法中可以调用sleep()
方法。
若是按照刚才的说法,先有eat(),后有sleep(),怎么一上来就可以sleep(),此时sleep()还没有声明啊,但是怎么没有报错?
我们只需要考虑,编译的时候,会看到eat()里面调用了sleep()方法,这个方法找一下有没有,发现有,那能确保调用sleep()的时候,内存中有吗?
其实在加载类的时候(将类放入了方法区),其实sleep()也好,eat()也好,方法都加载了的。
所以只需要确保调用这个方法之前,这个方法加载了就行。
回到这里:
//代码块赋值
{
orderId=2;
}
//显示赋值
int orderId=1;
现在这种情况也可以用类似的方式去解释,以后再说类加载的详细过程,现在就说最核心的点。
orderId
在整个类的加载中有一个过程,在其中某一个环节,就已经将orderId给加载了,而且还给了一个默认赋值0,这个时候orderId属性就已经有了。在后续的环节中,才开始做显示赋值和代码块的赋值。
现在是先有代码块的赋值,那么就将orderId改为2,后面又显示赋值,将它改为1。
一般习惯将代码块写显示赋值的下面
可以给类的非静态的属性(即实例变量)赋值的位置有:
① 默认初始化
② 显式初始化 或 ⑤ 代码块中初始化
③ 构造器中初始化
#############################
④ 有了对象以后,通过"对象.属性"或"对象.方法"的方法进行赋值
(造对象之前叫初始化,造对象之后叫赋值)
执行的先后顺序:
① - ②/⑤ - ③ - ④
给实例变量赋值的位置很多,开发中如何选?
显示赋值
:比较适合于每个对象的属性值相同的场景。构造器中赋值
:比较适合于每个对象的属性值不相同的场景(通过形参的方式给它赋值)。非静态代码块
:用的比较少,在构造器里面基本能完成。静态代码块
:静态(与类相关)属性不会选择在构造器(与对象相关)中赋值。静态的变量要么默认赋值,要么显示赋值,要么代码块中赋值。(超纲)关于字节码文件中的的简单说明(通过插件jclasslib bytecode viewer
查看)
刚才查看字节码文件的时候,可以看到,这里做个简单说明,便于大家理解。
说明
①
方法在字节码文件中可以看到。每个方法都对应着一个类的构造器。(类中声明了几个构造器就会有几个)
既然构造器和一 一对应,在字节码文件中也看不到“构造器”这一项。
所以构造器就是以方法的形式呈现在字节码文件中的。
比如这里声明了俩构造器:
class Order{
{
orderId=2;
}
int orderId=1;
public Order(){
//orderId=3;
}
public Order(int orderId){
this.orderId=orderId;
}
public void eat(){
sleep();
}
public void sleep(){
}
}
看一下字节码文件有两个,如下:
点开第二个,一起来看一下它的Code:
角标为1的值:
所以通过第二个有参构造器去造对象的时候,也会有显示赋值和代码块的执行,然后才是构造器。对应字节码文件中就是方法。
②编写的代码中的构造器在编译以后就会以
方法的方式呈现。(方法和构造器不是一回事)
③
方法内部的代码包含了实例变量的显示赋值、代码块中的赋值和构造器中的代码。
④
方法用来初始化当前创建的对象的信息的。
构造器和方法不是一回事,字节码文件中没有“构造器”,是以方法的形式呈现的。
下面代码输出结果是?
package yuyi06;
//技巧:由父及子,静态先行。
class Root{
//静态代码块
static{
System.out.println("Root的静态初始化块");
}
//非静态代码块
{
System.out.println("Root的普通初始化块");
}
//构造器
public Root(){
super();
System.out.println("Root的无参数的构造器");
}
}
class Mid extends Root{
static{
System.out.println("Mid的静态初始化块");
}
{
System.out.println("Mid的普通初始化块");
}
public Mid(){
System.out.println("Mid的无参数的构造器");
}
public Mid(String msg){
//通过this调用同一类中重载的构造器
this();
System.out.println("Mid的带参数构造器,其参数值:"
+ msg);
}
}
class Leaf extends Mid{
static{
System.out.println("Leaf的静态初始化块");
}
{
System.out.println("Leaf的普通初始化块");
}
public Leaf(){
//通过super调用父类中有一个字符串参数的构造器
super("雨翼轻尘");
System.out.println("Leaf的构造器");
}
}
public class LeafTest{
public static void main(String[] args){
new Leaf(); //涉及到当前类,以及它的父类、父类的父类的加载包括相应功能的执行
// System.out.println();
// new Leaf();
}
}
分析
new Leaf();
涉及到当前类,以及它的父类、父类的父类的加载包括相应功能的执行。
分析先后执行的顺序。
上面的类中,分别都有静态代码块、非静态代码块和构造器。
首先应该是静态代码块
,进行类加载的时候,一定先加载父类的,然后才是子类。
之前说的方法的重写,一定是先有父类的方法,才能覆盖它。(先加载父类)
当我们通过leaf()
造对象,首先会通过super()
找到父类。(没有写也是super)
画个图看一下逻辑:
所以最先加载的类是Object
,只不过改不了代码,也没有输出语句,
所以看似好像没加载,其实是先加载它,其次是Root类,然后就是Root类里面的static代码块
,下面的非静态代码块和无参构造器就别先执行了,因为要先将类的加载都执行了。
如下:
所以,看一下执行结果:(前面三行是“静态初始化块”)
类的加载
就完成了。
下面才涉及造对象。
静态加载之后,先去new了一个leaf(),然后执行super(),一直到最上层的Root类,先考虑它的构造器的加载(涉及到非静态结构的加载,然后才是子类),代码块的执行又早于构造器,所以会先输出代码块中的内容。
刚才说到调用的过程如下:
输出的话,就是反过来:
运行结果如下:
技巧:由父及子,静态先行。(先加载父类,后加载子类,静态结构早于非静态(init方法)的,非静态代码块的执行又早于构造器的执行)
方法包括代码块的,每个构造器都默认调用父类的构造器。
方法不是通过对象.
去调用的,而是自动执行的。
下面代码输出结果是?
class HelloA {
public HelloA() {
System.out.println("HelloA");
}
{
System.out.println("I'm A Class");
}
static {
System.out.println("static A");
}
}
class HelloB extends HelloA {
public HelloB() {
System.out.println("HelloB");
}
{
System.out.println("I'm B Class");
}
static {
System.out.println("static B");
}
}
public class Test01 {
public static void main(String[] args) {
new HelloB();
}
}
分析
执行输出顺序:①->②->③->④->⑤->⑥
先将类的加载搞定。
HelloA中,有静态先调用静态,输出“static A”,
然后回到HelloA中,调用静态,输出“static B”。
然后考虑当前要创建的对象的构造器HelloB(),此时第一行会调用super(),
调用HelloA()构造器。
再HelloA()构造器中,有非静态代码块,先执行它,输出“I’m A Class”,
然后输出构造器中“HelloA”。
super()执行结束之后,回到HelloB(),此时HelloB类中也有非静态代码块,
所以先输出代码块中“I’m B Class”,最后输出HelloB()构造器中“HelloB”。
代码运行结果
下面代码输出结果是?
public class Test02 {
static int x, y, z;
static {
int x = 5;
x--;
}
static {
x--;
}
public static void method() {
y = z++ + ++z;
}
public static void main(String[] args) {
System.out.println("x=" + x);
z--;
method();
System.out.println("result:" + (z + y + ++z));
}
}
分析
画个图:(执行顺序:①->②->③->④->⑤->⑥)
输出结果:
下面代码输出结果是?
public class Test03 {
public static void main(String[] args) {
Sub s = new Sub();
}
}
class Base{
Base(){
method(100);
}
{
System.out.println("base");
}
public void method(int i){
System.out.println("base : " + i);
}
}
class Sub extends Base{
Sub(){
super.method(70);
}
{
System.out.println("sub");
}
public void method(int j){
System.out.println("sub : " + j);
}
}
分析
画个图:(执行顺序:①->②->③->④->⑤->⑥->⑦->⑧)
调试
大家也可以自行调试,这里就做个示范。
输出结果: