在Java语言里,static是一个很基础却容易被误用的修饰符。它用来限定某个成员归属于类本身,而不是类的某一个具体对象。理解这一点,是掌握static语法的前提。

一、static修饰成员变量
当用static修饰字段时,这个字段成为类变量,在类加载的准备阶段就会分配内存,并且所有实例共享同一份数据。没有static的字段是实例变量,每个对象在堆里都有自己独立的一份拷贝。
下面这段代码展示了两种变量的差异。counter是静态的,所有User对象对它做的修改彼此可见;name是实例的,每个对象互不影响。
public class User {
// 类变量,所有实例共享
public static int counter = 0;
// 实例变量,各对象独立
public String name;
public User(String name) {
this.name = name;
counter++;
}
public static void main(String[] args) {
User u1 = new User("张三");
User u2 = new User("李四");
System.out.println(u1.name); // 输出 张三
System.out.println(u2.name); // 输出 李四
System.out.println(User.counter); // 输出 2,因为是共享的
}
}
从内存模型看,static变量存在方法区(或元空间)中,生命周期和类一致,因此适合放配置项、全局计数、常量等。但如果把大对象声明为static,类卸载前都无法回收,容易造成内存压力。
二、static修饰方法
静态方法同样属于类,调用时不需要创建对象,直接用类名点方法名即可。它内部只能访问static成员,不能使用this或super,也不能直接调用实例方法。
工具类如Math、Collections里大量使用静态方法,因为这类操作不依赖对象状态。下面写一个简单的温度转换工具方法:
public class TempUtil {
public static double cToF(double c) {
return c * 9 / 5 + 32;
}
public static void main(String[] args) {
double f = TempUtil.cToF(25);
System.out.println(f); // 输出 77.0
}
}
静态方法不能被重写,但能被隐藏。子类若声明了相同签名的static方法,父类引用调用时依旧走父类版本,这点和实例方法的多态不同,写继承代码时要留心。
三、static代码块与静态内部类
static代码块在类第一次被加载时执行,且只执行一次,常用来初始化静态资源。静态内部类则不持有外部类引用,可脱离外部实例独立创建,是实现懒汉单例的常用手段。
以下示例用静态内部类实现线程安全的单例,既保证延迟加载,又避免加锁开销:
public class Singleton {
private Singleton() {}
private static class Holder {
static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
对比普通内部类,静态内部类不会隐式持有外部类对象,因此在Android等注重内存的场景中更安全。而static代码块若抛异常会导致类初始化失败,整个JVM都可能收到影响,所以要包裹好错误处理。
四、使用static的常见误区
第一个误区是以为static能解决并发问题。实际上多个线程同时修改同一static变量仍需加锁或用原子类。第二个误区是在static方法里依赖实例注入的Spring Bean,由于static早于容器启动,直接引用会得到空指针。
合理用法是把无状态工具、全局配置、工厂方法设为static;不合理用法是把用户会话、请求数据塞进static字段,这会引发数据串号和内存泄漏。明确成员是否真的与对象无关,再决定是否加static。
| 修饰目标 | 归属 | 典型用途 |
|---|---|---|
| 变量 | 类 | 全局计数、常量 |
| 方法 | 类 | 工具函数、工厂 |
| 代码块 | 类 | 静态资源初始化 |
| 内部类 | 类 | 懒加载单例 |
掌握static的基础语法后,建议你在写每个字段或方法前先问一句:它是否依赖具体对象的状态?答案是否定的,再用static,代码会更清晰稳健。