Java中代码的执行顺序(附有3道例题)

类加载详细过程

  Java代码的执行顺序遵循一定的规则和步骤,这些规则主要涉及类加载、初始化、静态块执行、实例化过程以及方法调用等方面。以下是Java代码执行顺序的详细说明:(如果不想看可以直接看下面简要说明和例题)

1. 程序入口:main() 方法

Java程序的执行始于指定主类(带有 public static void main(String[] args) 方法的类)中的 main() 方法。这是程序的起点,也是JVM(Java虚拟机)开始执行Java代码的地方。 

 2. 类加载与初始化

 类加载:当首次遇到一个类的引用时(如创建类的实例、访问类的静态成员或调用类的静态方法),JVM会加载该类。加载过程包括:

  • 验证:检查字节码的正确性。
  • 准备:为类的静态变量分配内存并赋予默认初始值(如整型为0,对象引用为 null)。
  • 解析:将符号引用转换为直接引用(如方法表、字段表的偏移量)。

类初始化:当且仅当以下条件之一满足时,JVM会触发类的初始化:

  • 当new一个类的实例时。
  • 当访问类的静态变量(除了final且编译器可以确定其初始值的情况)或静态方法时。
  • 当子类初始化时,其父类未初始化,则先初始化父类。
  • 当使用反射API对类进行反射调用时。
  • 当初始化某个类的接口时,该接口未初始化 

3. 静态初始化块与静态成员变量初始化

在类初始化阶段,按照源代码中出现的顺序,执行以下操作:

  • 静态变量初始化:为所有声明时带有显式初始值的静态变量赋予指定值。
  • 静态初始化块(如果存在):按照在类定义中出现的顺序,依次执行静态初始化块中的代码。这些代码块通常用于集中处理静态资源的初始化或其他一次性设置。

4. 实例初始化

当创建类的实例时,执行以下步骤:

  • 分配内存:JVM为新对象分配内存空间。
  • 实例变量初始化:为所有声明时带有显式初始值的实例变量赋予指定值。
  • 构造函数调用:调用最合适的构造函数(根据提供的参数列表确定)。构造函数内部的代码按照编写顺序执行。如果构造函数中调用了超类构造函数(通过 super(...)),则先执行超类构造函数。
  • 实例初始化块(如果存在):按照在类定义中出现的顺序,依次执行实例初始化块中的代码。这些代码块用于初始化每个对象实例的通用设置,与构造函数互补。

5. 方法调用

对于非静态方法的调用,通常是在对象实例已经创建后,通过对象引用来完成。方法内部的执行顺序遵循语句的书写顺序,同时考虑到控制流(如条件、循环、异常处理等)的影响。

总结

Java代码的执行顺序大致如下:
1.从main()方法开始。
2.按需加载类并进行类初始化:

  • 静态变量初始化。
  • 静态初始化块执行。

3.创建对象实例:

  • 分配内存。
  • 实例变量初始化。
  • 构造函数调用(可能递归调用超类构造函数)。
  • 实例初始化块执行。

4.通过对象引用来调用非静态方法,方法内部代码按书写顺序执行。

简单来说代码块执行的顺序:

  1. 静态代码块

  2. 构造代码块

  3. 构造器

类加载的过程(简单版)

 类加载

    JVM都知道基本数据类型的大小, 但是对于自定义的数据类型,JVM知不知道大小。因此,对于基本数据类型,JVM 知道占多大空间,能进行哪些操作。 但是对于我们新建的引用数据类型, 它不知道里面有多少变量,有多少方法。所以在JVM刚开始使用的时候,一定会进行一个操作: 认识该类型。
    JVM认识这个类的过程,就称为类加载。
public class Demo3 {
    public static void main(String[] args) {
    
        Student student = new Student();
        int age = student.age;
        String name = student.name;
    }
}
创建对象的时候。 首先会进行的操作是类加载。
类加载其实就是JVM了解, 里面有哪些属性,有哪些方法, 方法里面的代码是怎样的
Student student = new Student(); 这行代码。 
在内存上, 第一步,会需要在堆上开辟一个空间存储对象数据
第二步, 在栈上创建一个引用,引用存储的是堆上的地址。

类加载其实就是JVM认识一个类的过程。
类加载要在创建对象之前进行,换句话说创建一个类的对象必然触发该类的类加载!

例题

第一题

class SuperClass {
    static int superStaticVar = 1; // 静态变量初始化
    static {
        System.out.println("SuperClass: Static block");
    }

    SuperClass() {
        System.out.println("SuperClass: Constructor");
    }
}

class SubClass extends SuperClass {
    static int subStaticVar = 2; // 静态变量初始化
    static {
        System.out.println("SubClass: Static block");
    }

    {
        System.out.println("SubClass: Instance initialization block");
    }

    SubClass(int value) {
        super(); // 调用超类无参构造函数
        System.out.println("SubClass: Constructor with value " + value);
    }

    public static void main(String[] args) {
        System.out.println("Main method starts");

        System.out.println("Accessing static variable: " + SubClass.subStaticVar); // 触发类初始化

        SubClass instance = new SubClass(42); // 实例化SubClass对象

        System.out.println("Main method ends");
    }
}

执行上述代码时,控制台输出将按照以下顺序显示:

Main method starts
SuperClass: Static block
SubClass: Static block
Accessing static variable: 2
SuperClass: Constructor
SubClass: Instance initialization block
SubClass: Constructor with value 42
Main method ends
 

1.程序从SubClass的main()方法开始执行。
2.访问SubClass.subStaticVar时,触发SubClass及其超类SuperClass的类初始化:
    先执行SuperClass的静态变量初始化,设置superStaticVar为1。
    执行SuperClass的静态初始化块,打印"SuperClass: Static block"。
    执行SubClass的静态变量初始化,设置subStaticVar为2。
    执行SubClass的静态初始化块,打印"SubClass: Static block"。
    输出访问的静态变量值:2。
3.实例化SubClass对象:
    分配内存给新对象。
    执行SuperClass的构造函数,打印"SuperClass: Constructor"。
    执行SubClass的实例初始化块,打印"SubClass: Instance initialization block"。
    执行SubClass构造函数(传入参数42),打印"SubClass: Constructor with value 42"。
4.打印"Main method ends",结束main()方法执行

第二题

 public class Demo5 {
        static {
            System.out.println("Demo5类开始初始化步骤了!");
        }

        static Cat5 cat5 = new Cat5();
        Dog5 dog5 = new Dog5();

        public Demo5() {
            System.out.println("Demo5 constructor");
        }

        public static void main(String[] args) {
            System.out.println("hello world!");
            Demo5 d = new Demo5();
        }
    }

    class Cat5 {
        static {
            System.out.println("Cat5类开始初始化步骤了!");
        }

        static Dog5 dog5 = new Dog5();

        public Cat5() {
            System.out.println("Cat5  constructor");
        }
    }

    class Dog5 {
        static {
            System.out.println("Dog5类开始初始化步骤了!");
        }

        static Demo5 demo = new Demo5();

        public Dog5() {
            System.out.println("Dog5  constructor");
        }
    }

简述:程序会先进入主函数,进入后第一步会开始类加载,会开始执行各个class中的static语句。

先按顺序加载类,第一个类是Demo4的类加载,开始执行Demo4中的static语句(static只会在类加载过程执行一次,之后不会再次执行),输出Demo4类开始初始化了。

之后开始执行Static Cat5=new Cat5()的语句(这句会分开执行,会先执行Static Cat5,也就是JVM会先进行类加载,加载Cat5的类),进入到class Cat5,开始执行这里的static语句,第一个static语句是输出Cat5类开始初始化步骤了

然后发现第二个static语句又是一个new语句,Static Dog5=new Dog5()的语句(和上个Cat5的new语句一样会先进行Dog5类的类加载)

进入class Dog5来执行static语句,输出Dog5类开始初始化步骤了。Dog5类中第二个static语句是Demo4类的new语句,又会进行Demo4类的部分,Demo4类已经进行了类加载,static语句已经执行了,就不会再次执行了。那么第一个执行的语句就是Dog5的new语句,Dog5已经完成了类加载,就会按顺序执行其中的其他语句,也就是public中的语句,输出Dog5 constructor,之后返回Demo4中执行下一条语句,也就是public Demo4,输出Demo4 constructor。至此class Dog5完成了类加载

返回上一步class Cat5,Cat5也完成了类加载,开始完成newCat5的后半句,执行class Cat5中的语句,只有public Cat5需要执行,输出Cat5 constructor。至此Demo4类加载完成

类加载这也一过程完事,开始执行main函数中的语句,输出hello world!。

开始执行Demo4的语句(Demo4已经完成了类加载,不需要再次执行static语句),开始执行Dog5的语句(类也加载完了),直接执行类中的输出语句,输出Dog5 constructor。然后再执行public的输出语句,输出Demo5 constructor。

输出结果:

Demo5类开始初始化步骤了!
Cat5类开始初始化步骤了!
Dog5类开始初始化步骤了!
Dog5  constructor
Demo5 constructor
Dog5  constructor
Cat5  constructor
hello world!
Dog5  constructor
Demo5 constructor

 

  • static代码块,类加载的时候会直接执行。
  • static修饰的成员变量,在类加载的时候也会执行。
  • static修饰的方法,在类加载的时候,不会自动执行

静态成员变量 

  • 和普通成员变量一样,都具有默认值(默认值和普通成员变量是一样的)

  • 静态成员变量属于类的,完全不需要创建对象使用。

  • 访问和使用静态成员变量不推荐使用"对象名.",而应该使用"类名."

  • 静态成员变量的访问/赋值/使用都不依赖于对象, 而是依赖于类

  • 静态成员需要在类加载时期,完成准备,类加载结束就能够使用。所以访问类的静态成员,一定会触发该类的类加载。

 内存及原理解析:

静态成员的访问并不依赖于创建对象,可以直接通过类名访问,其原因在于:

随着类加载完毕,静态成员就存在,并且能够使用了!

某个类的某个静态成员变量只有一份,且被所有对象共享,属于类,无需创建对象使用。

 

 注意:只存在静态成员变量,不存在"静态局部变量"

静态成员方法

  • 无需创建对象就可以直接通过类名点直接调用。

  • 同一个类中的static方法互相调用可以省略类名,直接用方法名调用。(这就是我们之前方法的调用)

  • 一个类中,静态方法无法直接调用非静态的方法和属性,也不能使用this,super关键字(super后面会讲),静态的方法只能访问静态的。原因:静态方法调用的时候,有可能没有对象,没有对象普通成员就无法访问。

  • 普通成员方法当中,既可以访问静态成员的, 也可以访问非静态成员。普通成员方法访问任意的

  • 访问静态成员变量的时候,使用类名.变量名的形式访问,以示区别,增加代码可读性

第三题

public class ExerciseBlock {
    static {
        System.out.println("main方法静态代码块!");
    }
    {
        System.out.println("main方法构造代码块!");
    }
    public static void main(String[] args) {
        System.out.println("main方法开始执行!");
        Star s = new Star(18,"马化腾");
        System.out.println(Star.name);
        System.out.println(s.age);
    }
}
class Star{
    {
        age = 18;
        Star.name = "杨超越";
        System.out.println("我喜欢杨超越");
    }
    static String name = "王菲";
    int age = 28;
    static {
        name = "杨幂";
        System.out.println("我喜欢杨幂");
    }
    public Star(int age,String name) {
        this(age);
        System.out.println("age,name:构造器!");
        Star.name = name;
        Star.name = "刘亦菲";
    }
    public Star(int age) {
        System.out.println("age:构造器!");
        this.age = age;
    }
    public Star() {
    }
}

简述:先执行main函数,第一步永远是类加载,加载ExerciseBlock类,输出main方法静态代码块(类加载是懒加载用不到就不加载),然后执行主函数中的第一条语句(输出语句),输出main方法开始执行。然后创建s对象,开始加载Star类,执行static语句,name=王菲,然后name=杨幂米,之后输出我喜欢杨幂,之后开始进行new Star。开始传入参数执行public Star(age,name),第一句就是this(age),开始调用下面单参的构造器(先显示赋值,然后再开始构造器赋值)开始从上面class Star按顺序执行代码块,输出我喜欢杨超越, int age=28。开始单参构造器 ,输出age:构造器!,age=18。返回双参构造器,输出age,name:构造器!, 把name赋值成马化腾,在改成刘亦菲。所以最后输出的是刘亦菲18

附:构造器赋值顺序(下面详细解释)

1.默认初始化,具有默认值。

2.显示赋值,直接将值写在成员变量声明的后面。

3.构造器赋值

成员变量赋值中,构造器是最后执行的。

构造器的赋值顺序指的是在创建对象时,构造器内部对成员变量进行赋值的先后顺序。构造器内成员变量的赋值遵循以下规则:

  1. 默认初始化:如果成员变量在声明时指定了默认值,那么首先会进行默认初始化。
  2. 显式初始化:如果成员变量在声明时进行了赋值(即显式初始化),那么在构造器调用前,这些赋值会被执行。
  3. 构造器中赋值:在构造器的主体中,按照代码书写的顺序依次对成员变量进行赋值。
public class User {
    // 默认初始化为 null
    private String name;

    // 显式初始化为 0
    private int age = 0;

    // 显式初始化为 "Unspecified"
    private String occupation = "Unspecified";

    public User(String newName, int newAge) {
        // 构造器中赋值,先赋值 name,再赋值 age
        name = newName;
        age = newAge;

        // 在构造器中更改 occupation 的值
        occupation = "Developer";
    }

    public void displayUserDetails() {
        System.out.println("Name: " + name);
        System.out.println("Age: " + age);
        System.out.println("Occupation: " + occupation);
    }

    public static void main(String[] args) {
        User user = new User("Alice", 30);
        user.displayUserDetails();
    }
}

Name: Alice
Age: 30
Occupation: Developer

 解析赋值顺序:

  1. 默认初始化:成员变量 name 被默认初始化为 null。
  2. 显式初始化:
  • 成员变量 age 被显式初始化为 0。
  • 成员变量 occupation 被显式初始化为 "Unspecified"。

     3.构造代码块赋值 (下面详细讲述)

     4.构造器中赋值:

  • 构造器 User(String newName, int newAge) 被调用时,首先将传入参数 newName 赋给 name,此时 name 的值变为 "Alice"。
  • 接着将传入参数 newAge 赋给 age,此时 age 的值变为 30。
  • 最后,将 occupation 的值显式改为 "Developer"。

     5.显示用户详情:

  • 调用 displayUserDetails() 方法,输出当前 User 对象的 name、age 和 occupation 的值,即按上述顺序赋值后的结果。

综上所述,构造器中成员变量的赋值顺序遵循默认初始化 → 显式初始化 → 构造器中赋值的规则。在这个示例中,成员变量 name、age 和 occupation 分别经历了默认初始化、显式初始化和构造器中赋值的过程,最终输出了经过构造器赋值后的值。

 构造代码块

 定义在类的成员位置,使用以下声明方式声明的代码块,称之为构造代码块。

作用是随着构造器的执行,用于在创建对象过程中,给成员变量赋值

//成员位置
{
	// 局部位置
}
//成员位置

 构造代码块内部属于局部位置,在里面定义变量,就是一个仅在构造代码块中生效的局部变量。

public class Demo2 {

    int a = 20;
    // 在这个位置定义块,就是构造代码块
    {
        // 这个a,仅在局部生效。
        int a = 10;
        System.out.println(a);
    }
}

这里总结给成员变量赋值的几种方式(创建对象过程中):

  • 默认初始化,具有默认值

  • 显式赋值

  • 构造代码块

  • 构造器

学习对象中成员变量的赋值,和赋值顺序要遵循"掐头去尾"的原则:

  1. :默认初始化,具有默认值,在对象结构存在于对象中,对象中的成员变量就已经具有了默认值。

    77777我们程序员所有能干预的赋值方式,都是在默认初始化的基础上进行的。

  2. :构造器,构造器在整个对象的成员变量赋值过程中,处在最后的阶段,最后被执行。

明确以上两点后,我们现在只需要研究显式赋值构造代码块的赋值顺序,

显式赋值和构造代码块的执行顺序,并不是固定的,而是按照代码的书写顺序去执行的:

  1. 这两个结构,谁写在代码书写顺序的上面,谁就先执行。

  2. 后执行结构的结构,自然会覆盖先执行结构的结果。

通过查看反编译class文件(通过IDEA),我们发现编译后的代码中并不存在构造代码块的结构,而是:

直接将成员变量的显式赋值和构造代码块中的代码智能地加入,类所有的构造器中的前几行:

所谓智能是为了保证:成员变量的显式赋值和构造代码块,按照代码的书写顺序从上到下执行!

于是,我们可以得出以下结论:

  1. 使用new对象的方式创建对象,不论使用哪个构造器,构造代码块都会随之执行。

  2. 构造器是每一次new对象都会执行一次,所以构造代码块也会随之执行一次。

  3. 构造代码块中的代码要放入构造器的首几行,所以在同一个类中,构造代码块总是先于它的构造器而执行。

创建对象过程中的执行顺序

new对象过程中,各种结构的执行顺序

  1. 对象结构存在后就进行默认初始化,所有成员变量都具有默认值后,再开始其余赋值操作

  2. 找到new对象的那个构造器

                  如果它的首行显式地调用了另一个构造器this(实参)

                ( 注:显式调用构造器目前指的是this调用自身构造器,其它场景这里先不考虑)

那么程序会先跳转到那个构造器,但是不会立刻执行,而是:

  1. 按照类中构造代码块和显式赋值的代码书写顺序,从上到下执行其中的代码,执行完毕后:

  2. 跳转回this语句要指示执行的构造器,执行其中的代码,然后:

  3. 跳转回new对象构造器,执行完毕后,创建对象结束。

注:整个过程中,构造代码块和显式赋值的代码只会执行一次,不会执行多次!!

                   如果它的首行没有显式调用另一个构造器   

那么会先从上到下执行构造代码块和显式赋值代码

执行完毕后:跳转回new对象构造器,执行完毕后,创建对象结束。

注:整个过程中,构造代码块和显式赋值的代码只会执行一次,不会执行多次!!

  • 44
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
好的,以下是一个示例程序,可以从 Excel 表格读取数据并封装到 List 集合: ```java import java.io.FileInputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; public class ExcelReader { public static void main(String[] args) { String fileName = "data.xlsx"; // Excel 文件路径 int sheetIndex = 0; // 要读取的工作表索引 List<String> dataList = readExcelToList(fileName, sheetIndex); System.out.println(dataList); } public static List<String> readExcelToList(String fileName, int sheetIndex) { List<String> dataList = new ArrayList<>(); try (FileInputStream fis = new FileInputStream(fileName); Workbook wb = WorkbookFactory.create(fis)) { Sheet sheet = wb.getSheetAt(sheetIndex); for (Row row : sheet) { StringBuilder sb = new StringBuilder(); for (Cell cell : row) { sb.append(cell.toString()).append(","); } sb.deleteCharAt(sb.length() - 1); // 删除最后一个逗号 dataList.add(sb.toString()); } } catch (IOException e) { e.printStackTrace(); } return dataList; } } ``` 这个程序使用了 Apache POI 库来读取 Excel 表格,它可以读取多种格式的 Excel 文件,包括 .xls 和 .xlsx 等。在上面的程序,我们只读取了一个工作表,并将每一行的数据封装到一个字符串,再将这些字符串添加到 List 集合。你可以根据需要修改程序,将 Excel 的数据封装成自定义的对象,而不是字符串。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值