static关键字
- static关键字是什么
- static修饰变量
- static修饰方法
- static 修饰代码块
- 类的加载顺序
static关键字是什么
static 是 Java 中的修饰符,用于表示某个成员(变量、方法、代码块、内部类)属于类本身,而不是属于类的某个特定实例。
举例:
这里 run() 方法带 static 关键字,而 eat() 方法不带 static 关键字,在 main() 方法中直接调用 eat() 方法时会产生报错,关键点是类和对象的区别。
被 static 修饰的成员(run()方法)属于类本身,在 JVM 加载类的字节码(类加载阶段)时完成初始化,存储在方法区,整个程序运行期间仅存在一份,不依赖任何对象实例;
未被 static 修饰的成员(eat()方法)属于对象实例,仅当通过 new Test() 创建对象后,才会随对象在堆内存中初始化,每个对象拥有该方法的独立调用入口。
先创建 Test 类的对象实例,通过实例调用非静态方法,实例引用调用实例方法
静态成员与非静态成员的差异:
- 生命周期差异
静态成员::在 JVM 加载类的字节码至内存时(类加载阶段)完成初始化,存储于方法区,整个程序运行期间仅初始化一次,其生命周期与类的生命周期完全绑定;
非静态成员:仅在通过 new 关键字创建类的对象实例时才初始化,存储于堆内存中,每个对象实例都会拥有独立的非静态成员副本,其生命周期与对象实例的生命周期绑定。 - 访问方式规则
静态成员:可直接通过「类名.成员名」访问,无需创建类的对象实例;
非静态成员:仅能通过类的对象实例「对象名.成员名」访问,非静态成员的存在依赖具体的对象实例。
static修饰变量
被 static 修饰的变量称为类变量,归属类本身而非类的具体对象实例,由该类所有实例化的对象共享同一份内存副本,任意一个对象修改该值,所有对象访问到的都会是修改后的值。
静态变量在 JVM 加载类时(类加载阶段)完成初始化,存储于方法区,整个程序运行期间仅初始化一次,生命周期与类的生命周期绑定;可通过「类名.变量名」直接访问。
举例:
publicclassPerson{privateintage;privateStringname;staticStringfrom;publicPerson(intage,Stringname){this.age=age;this.name=name;}@OverridepublicStringtoString(){return"Person [age="+age+" name="+name+",from="+from+"]";}publicstaticvoidmain(String[]args){Personperson1=newPerson(20,"张三");Personperson2=newPerson(21,"李四");Person.from="中国";System.out.println(person1.toString());System.out.println(person2.toString());}}输出:
内存图:
结合内存图对该代码进行详解:main() 函数入栈,创建数据结构为 Person 的 person1 和 person2 对象,在堆中开辟内存空间,该内存空间中有 name、age、from 三块区域,当使用 new 关键字创建对象时,构造方法会被自动调用,构造函数的作用是在创建对象的时候给对象赋值,此时两个对象的 name 和 age 均被赋值。而 form 是 static 修饰的 String 类型的数据,String 是被 final 修饰过的数据类型,所以 form 存储的数据属于静态常量,from 的值存储在方法区的静态常量池里,给 from 赋值为"中国",假设该静态常量池的地址是0x1,那么 person1 和 person2 对象的 from 区域存储的就是 from 的地址 0x1。
总结:person1 和 person2 的 name 和 age 属性的值都在堆内存当中进行存储,且是该对象私有的,但是 from 属性值是存储在方法区的静态常量池当中的,是属于公共的。
static修饰方法
被 static 关键字修饰的方法被规范称为静态方法(Static Method),也常称作类方法(Class Method),其核心本质是归属类的全局范畴,而非类实例化后的具体对象实例,是类的固有行为,不依赖任何对象的状态(属性)。
静态方法执行时无 this 引用:
this 指向当前对象实例的引用参数,而静态方法归属类本身,执行时不绑定任何对象实例,因此不存在 this 引用,也无法使用 this 关键字;
同理,指向父类实例上下文的 super 关键字也无法在静态方法中使用。
成员访问的规则:
静态方法无法直接访问类的非静态成员,非静态成员的生命周期依赖对象实例,而静态方法在 JVM 类加载阶段即完成初始化,此时可能尚无任何对象实例被创建,但可通过「创建对象实例 + 实例.非静态成员」的方式间接访问非静态成员;
静态方法可直接访问类的静态成员(静态属性、静态方法),二者同属类级别,加载时机与生命周期一致;
非静态方法可自由调用静态方法(静态方法归属类,无需依赖实例即可访问),也可直接访问所有非静态成员;
内存与生命周期特征:静态方法在 JVM 类加载的初始化阶段完成解析与初始化,其字节码指令存储于方法区,整个 JVM 进程运行期间,类的静态方法的生命周期与类的生命周期完全绑定,而非静态方法虽也存储于方法区,但执行时需绑定堆内存中的具体对象实例;
静态方法的继承与重写特性:
被static修饰的静态方法可被子类继承,子类无需额外实现,即可通过「子类名.静态方法」或「父类名.静态方法」直接调用父类的静态方法,本质是子类共享了父类类级别的方法资源。
但静态方法无法被真正重写(Override),重写依赖运行时多态(由对象的运行期类型决定方法执行逻辑),而静态方法的调用由编译期声明的类名决定,子类若定义与父类同名同参数的静态方法,仅为隐藏父类方法(子类方法覆盖父类方法的访问入口),而非重写。调用时仅取决于引用变量的声明类型,而非对象的实际类型(无多态特性)。
不加 static 关键字的用法:
加 static 关键字的用法:
Animal() 和 main() 在不同类里,在 main() 中调用 eat() 方法使用的是 Animal.eat();
若 Animal() 和 main() 在同一个类里,在 main() 中调用 eat() 方法可以直接使用 eat(); ,如图:
这是 Java 提供的简化写法,在当前类的作用域内调用本类的静态方法时,允许省略类名,编译器会自动补全;而调用其他类的静态方法时,必须显式指定类名,避免歧义。
static 修饰代码块
被 static 关键字修饰的代码块称为静态代码块(Static Block),也叫静态初始化块,是 Java 中专门用于初始化类级静态成员(静态变量、静态资源)的特殊代码结构,其核心设计目的是为静态成员提供复杂的初始化逻辑。
publicclassPerson{static{System.out.println("你好");}publicstaticvoidmain(String[]args){System.out.println("hello world");}static{System.out.println("hello ~");}}输出结果:
从结果中我么能够看到:
静态代码块仅在类加载的初始化阶段执行一次;
同一类中多个静态代码块按代码声明的从上到下顺序执行;
先执行父类的静态代码块,再执行子类的静态代码块,最后再执行实例相关代码(实例代码块、构造方法)。
那么我们看看几个案例来分析一下类的加载顺序
我们来看如下代码:
publicclassTestextendsBase{static{System.out.println("test static");}publicTest(){System.out.println("test constructor");}publicstaticvoidmain(String[]args){newTest();}}classBase{static{System.out.println("base static");}publicBase(){System.out.println("base constructor");}}这是代码的输出结果:
我们来看当前这个创建流程是什么?
在执行开始,先要寻找到 main 方法,因为 main 方法是程序的入口,但是在执行main方法之前,必须先加载 Test 类,而在加载 Test 类的时候发现 Test 类继承自 Base 类,因此会转去先加载 Base 类,在加载 Base 类的时候,发现有 static 块,便执行了 static 块。在 Base 类加载完成之后,便继续加载 Test 类,然后发现 Test 类中也有 static 块,便执行 static 块。在加载完所需的类之后,便开始执行 main 方法。在main方法中执行 new Test() 的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。
简单来说:先创建父类的 static 代码块输出 “base static” 再创建子类的 static 代码块输出 “test static”,这个过程属于扫描过程。然后 main 方法入栈,之后 new test() 创建当前子类对象,但创建子类对象的是先创建父类对象,所以在这个地方先表达的是这个 base 功能函数输出 base constructor,然后回过头来再创建输出 “test constructor”。
我们再来看这个代码:
publicclassDemo{publicDemo(Stringnum){System.out.println("==="+num);}static{System.out.println("111");}publicstaticDemodemo=newDemo("+++");static{System.out.println("222");}}classDemoTest{publicstaticvoidmain(String[]args){Demodemo=newDemo("---");}}这是输出结果:
在Java中类的加载是按需进行的,当JVM启动时并不会一次性加载所有的类,而是在程序运行过程中,当需要用到某个类的时候才会加载它。
程序的完整执行顺序如下:
JVM 启动,首先加载并初始化 DemoTest 类(它是包含 main 方法的程序入口类),由于 DemoTest 类种无其他静态变量 / 静态代码块直接执行 main 方法。
main 方法中执行 new Demo(“—”) 语句,JVM 检测到 Demo 类尚未完成初始化,暂停 main 方法的执行,进入Demo 类的初始化阶段。
Demo 类的初始化阶段,按代码书写顺序执行所有静态初始化操作(静态代码块和静态变量显式赋值属于同一优先级的静态初始化语句,无先后优先级,仅按顺序执行):
第一步:执行第一个静态代码块,控制台输出 111;
第二步:执行静态变量demo的显式赋值语句public static Demo demo = new Demo("+++"):
执行 new Demo(“+++”) 触发 Demo 类的实例化流程,调用 Demo 的构造函数 Demo(String num),传入参数"+++“,控制台输出===+++,实例创建完成,将该实例赋值给静态变量demo;
第三步:执行第二个静态代码块,控制台输出 222;
此时 Demo 类的静态初始化全部完成,JVM 回到 main 方法继续执行暂停的 new Demo(”—“) 语句,触发 Demo 类的实例化流程,调用构造函数 Demo(String num),传入参数”—",控制台输出===---,程序执行结束。
类的加载顺序
类初始化阶段(仅执行一次)
① 父类静态变量显式赋值 → ② 父类静态代码块(按声明顺序)
→ ③ 子类静态变量显式赋值 → ④ 子类静态代码块(按声明顺序)
对象实例化阶段(每次new子类都执行)
⑤ 父类实例变量显式赋值 → ⑥ 父类实例代码块(按声明顺序)
→ ⑦ 父类构造方法
→ ⑧ 子类实例变量显式赋值 → ⑨ 子类实例代码块(按声明顺序)
→ ⑩ 子类构造方法
// 父类classParent{// 父类静态变量1staticintparentStaticVar=printNum("1.父类静态变量显式赋值");// 父类静态代码块1static{printNum("2.父类静态代码块1");}// 父类静态代码块2static{printNum("3.父类静态代码块2");}// 父类实例变量1intparentInstanceVar=printNum("5.父类实例变量显式赋值");// 父类实例代码块{printNum("6.父类实例代码块");}// 父类构造方法publicParent(){printNum("7.父类构造方法");}// 辅助打印方法(静态)publicstaticintprintNum(Stringmsg){System.out.println(msg);return0;// 仅为赋值用,无实际意义}}// 子类classChildextendsParent{// 子类静态变量1staticintchildStaticVar=printNum("4.子类静态变量显式赋值");// 子类静态代码块static{printNum("4.子类静态代码块");// 注意:和静态变量按声明顺序,这里是第4步后}// 子类实例变量1intchildInstanceVar=printNum("8.子类实例变量显式赋值");// 子类实例代码块{printNum("9.子类实例代码块");}// 子类构造方法publicChild(){// 隐含super(),会先执行父类实例初始化printNum("10.子类构造方法");}}// 测试类publicclassClassLoadOrderTest{publicstaticvoidmain(String[]args){System.out.println("=====第一次创建子类实例=====");Childc1=newChild();System.out.println("\n=====第二次创建子类实例=====");Childc2=newChild();}}执行结果如下:
阶段 1:加载入口类,执行 main 方法开头
JVM 启动后,首先加载 ClassLoadOrderTest 类(包含 main 方法的入口类),执行main方法第一行控制台输出第一次创建子类实例;
阶段 2:第一次创建 Child 实例触发类初始化与实例化
执行 new Child() 时,JVM 发现 Child 类未初始化,且 Child 继承自 Parent,因此先完成父类→子类的静态初始化,再执行父类→子类的实例化。(静态资源是类级别的,父类是子类的基础,因此先初始化父类静态资源)
父类 Parent 的静态初始化(仅执行 1 次):按代码书写顺序执行父类的静态资源(静态变量显式赋值 + 静态代码块);
子类 Child 的静态初始化(仅执行 1 次):父类静态初始化完成后,按代码书写顺序执行子类的静态资源(静态变量显式赋值 + 静态代码块);
父类 Parent 的实例化(子类构造隐含 super ()):子类静态初始化完成后,开始实例化 Child 对象,子类构造方法第一行隐含super()(调用父类无参构造),因此先执行父类的实例资源:执行父类实例变量显式赋值,再执行父类实例代码块;
子类 Child 的实例化:父类实例化完成后,执行子类的实例资源:子类实例变量显式赋值与子类实例代码块,再执行子类构造方法;
阶段 3:第二次创建 Child 实例仅执行实例化
此时 Parent 和 Child 的静态初始化已完成,因此仅执行父类→子类的实例化。