static是一个很重要的关键字,它可以修饰类的成员(变量和方法)和代码块。
本文一起来看下static的含义和用法。
一、static的核心概念
理解static关键字一定要先记住一句话:"被static修饰的成员属于类本身,而不是类的某个特定实例(对象)"。
如果对类、对象(实例)还没什么概念的,可以回顾下:Day9 | 类、对象与封装全解析。
"属于类本身"就意味着:
静态成员被类的所有实例共享。不管你创建了多少个类的对象,它们访问的都是同一份静态成员。
静态成员随着类的加载而加载,随着类的卸载而销毁,他们的生命周期跟类一样,通常比对象的生命周期长。
可以通过类名直接访问静态成员(这是推荐方式),也可以通过对象引用访问(但不推荐,因为它可能引起混淆,误以为是实例成员)。
二、静态变量
静态变量也叫类变量,是使用static关键字声明的字段。
package com.lazy.snail.day20;
/**
* @ClassName StaticVarDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 15:55
* @Version 1.0
*/
public class StaticVarDemo {
static int count = 0;
String name;
public StaticVarDemo(String name) {
this.name = name;
count++;
}
public static void main(String[] args) {
StaticVarDemo d1 = new StaticVarDemo("懒惰");
StaticVarDemo d2 = new StaticVarDemo("蜗牛");
System.out.println(StaticVarDemo.count);
System.out.println(d1.count);
d1.count = 10;
System.out.println(d2.count);
}
}
count是StaticVarDemo类中使用static修饰的静态成员变量。
"StaticVarDemo.count"是直接使用了"类名.变量"访问的静态成员,这是推荐做法。
当然也允许通过实例名("d1.count")这种语法访问静态成员,但是不推荐。
通过"d1.count =10"修改了count的值后,d2.count的值也发生了变化,因为d1、d2共享的同一个count。
实际上你通过实例名访问静态成员,编译器会改成类名访问。
从字节码可以看出,通过实例操作静态成员变量之前(
getstatic/bipush/putstatic),都有一个同样的操作先aload_1(aload_2)然后立即pop。
这里的aload_n其实就是加载实例的引用,然后马上通过pop指令把实例的引用弹出栈了。
说到底,就是完全没用实例引用。用实例操作静态成员变量,无非就是生成了多余的两条指令(aload_n+pop)。
Tips:
静态变量存储在方法区(在Java 8及之后,静态变量存储在堆中的类元数据旁边,但逻辑上仍属于类)。
它在类加载时就被分配内存,并且只分配一次。
静态变量可以在声明的时候初始化,也可以在静态代码块里初始化。
三、静态方法
静态方法是使用static关键字声明的方法。
package com.lazy.snail.day20;
/**
* @ClassName StaticMethodDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 17:36
* @Version 1.0
*/
public class StaticMethodDemo {
public static void printSomething(String message) {
System.out.println(message);
}
public static void main(String[] args) {
StaticMethodDemo.printSomething("懒惰蜗牛");
}
}
printSomething方法使用static修饰,直接使用类名调用,不需要创建类的实例。
静态方法存在下面这些限制:
- 静态方法不能直接访问类的实例变量(非静态变量)。
- 静态方法不能直接调用类的实例方法(非静态方法)。
- 静态方法不能使用this或super关键字,因为它们都和特定实例相关。
- 静态方法可以访问类的静态变量和调用类的其他静态方法。
下面的案例基本涵盖了上述的限制:
package com.lazy.snail.day20;
/**
* @ClassName StaticMethodDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 17:36
* @Version 1.0
*/
public class StaticMethodDemo {
private static String staticVar = "静态变量";
private String instanceVar = "实例变量";
public static void staticMethod1() {
System.out.println(staticVar);
System.out.println(instanceVar); // 编译错误
this.instanceMethod(); // 编译错误
staticMethod2();
}
public void instanceMethod() {
System.out.println(staticVar);
System.out.println(instanceVar);
staticMethod1();
}
public static void staticMethod2() {
System.out.println("staticMethod2");
}
public static void main(String[] args) {
}
}
看一下方法重写的相关案例:
子类和父类有同名的静态方法(相同签名),调用谁的方法取决于引用的类型而不是对象的实际类型。
package com.lazy.snail.day20.override;
/**
* @ClassName Dog
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 17:55
* @Version 1.0
*/
public class Dog extends Animal {
public static void run() {
System.out.println("狗子,跑起来");
}
public static void main(String[] args) {
Animal animal = new Dog();
animal.run();
}
}
class Animal{
public static void run() {
System.out.println("跑起来");
}
}
实际开发也不会有人通过实例调用静态方法。
静态方法不能被子类重写为非静态方法。
实例方法也不能被子类重写成静态方法。
其实最典型的静态方法就是main方法。
public static void main(String[] args) {
}
这是Java程序的入口点,JVM在启动时不需要创建任何对象就能调用它。
上面的这些案例是为了让大家熟悉静态方法,不需要纠结一会实例,一会类的,绕来绕去。
核心还是在于static修饰的方法是属于类的。
四、静态代码块
静态代码块是用static关键字和{}包裹起来的一段代码。
package com.lazy.snail.day20;
/**
* @ClassName StaticBlockDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 18:13
* @Version 1.0
*/
public class StaticBlockDemo {
static {
System.out.println("静态代码块1");
}
static {
System.out.println("静态代码块2");
}
public StaticBlockDemo() {
System.out.println("构造方法");
}
public static void main(String[] args) {
StaticBlockDemo demo = new StaticBlockDemo();
}
}
// 输出结果:
// 静态代码块1
// 静态代码块2
// 构造方法
这个案例中写了两段静态代码块。
通过输出结果可以看出:
静态代码块在类加载期间执行(早于构造方法),并且只执行一次(在首次加载到JVM时)。
一个类中有多个静态代码块,它们会按照在代码中出现的顺序自上而下依次执行。
静态代码块主要用来初始化静态变量,特别是静态变量的初始化逻辑比较复杂,不能通过简单的赋值语句完成的时候。(比如从配置文件读取值来初始化静态变量。)
静态代码块也是静态的,同理,代码块里不能访问实例变量或实例方法。
五、静态内部类
static还可以用来修饰内部类。在Day15 | Java内部类详解中也列举了HashMap的源代码进行说明。
package com.lazy.snail.day20;
/**
* @ClassName OuterClass
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 18:30
* @Version 1.0
*/
public class OuterClass {
private static String outerStaticVar = "外部静态变量";
private String outerInstanceVar = "外部实例变量";
public static class StaticInnerClass {
public void display() {
System.out.println(outerStaticVar);
//System.out.println(outerInstanceVar); // 编译错误
}
}
public class InstanceInnerClass {
public void display() {
System.out.println(outerStaticVar);
System.out.println(outerInstanceVar);
}
}
public static void main(String[] args) {
// 静态内部类:不需要外部类实例
OuterClass.StaticInnerClass staticInner = new OuterClass.StaticInnerClass();
// 非静态内部类:必须先创建外部类实例
OuterClass.InstanceInnerClass instanceInner = new OuterClass().new InstanceInnerClass();
}
}
静态内部类可以访问外部类的所有静态成员(包括私有的)。
静态内部类不能直接访问外部类的实例成员。
创建静态内部类的对象不需要先创建外部类的对象,因为静态内部类不持有对其外部类实例的引用。
为了方便理解上面这句话:
new OuterClass.StaticInnerClass();等价于下面
// 步骤1:直接访问外部类的静态成员(不需要实例)
OuterClass.StaticInnerClass
// 步骤2:使用new创建对象
new OuterClass.StaticInnerClass();
new OuterClass().new InstanceInnerClass();等价于下面
// 步骤1:先创建外部类实例(必须)
new OuterClass()
// 步骤2:通过外部实例创建内部类对象
.new InstanceInnerClass();
六、static与类加载流程
由于还没讲到类的加载流程,下面简单的梳理一个简化的流程:
1、加载(Loading):JVM查找并加载类的.class文件字节码到内存中,并在方法区(或元空间)创建一个Class对象来表示这个类。
2、链接(Linking):
- 验证(Verification):确保加载的类信息符合JVM规范,没有安全问题。
- 准备(Preparation):为类的静态变量分配内存,并设置默认初始值(例如,int为0,对象为null)。注意,这里是默认值,不是代码中指定的初始值。
- 解析(Resolution):将符号引用(如类名、方法名)替换成直接引用(内存地址)。
3、初始化(Initialization):这是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中所有静态变量的赋值语句和静态代码块中的语句合并产生的。
- JVM会保证<clinit>()方法在多线程环境中被正确地加锁和同步。
- 静态变量的显式赋值和静态代码块的执行都在这个阶段完成,并且是按照它们在源代码中出现的顺序执行。
- 子类的<clinit>()方法执行前,会保证其父类的<clinit>()方法已经执行完毕。
静态变量在“准备”阶段被赋予默认值。
静态变量的程序设定初始值和静态代码块在“初始化”阶段执行。
static成员的加载和初始化优先于实例成员。
七、静态导入
静态导入允许你导入一个类的所有静态成员,或者特定的静态成员,从而在代码中可以直接使用静态成员名,而不需要类名作为前缀。
package com.lazy.snail.day20;
import static java.lang.Math.PI;
import static java.lang.Math.pow;
/**
* @ClassName StaticImportDemo
* @Description TODO
* @Author lazysnail
* @Date 2025/6/5 19:31
* @Version 1.0
*/
public class StaticImportDemo {
public static void main(String[] args) {
double radius = 2.0;
double area = PI * pow(radius, 2);
System.out.println("面积: " + area);
}
}
代码里直接使用了PI和pow,而不是此前的Math.PI和Math.pow,省略了类名。
主要就是因为使用了import static进行了静态导入。
我个人还是不太喜欢这种写法,因为不能直观的看出静态成员是哪个类的。
在某个类里面频繁的使用某个类的少量静态成员的时候可以考虑。
结语
今天给大家简单的捋了一下static关键字的各种使用方法和背后的含义。
还是那句话,其实理解和合理的使用static关键就在于"静态成员属于类而非对象"。