static 关键字
本章定位:
static将成员从"对象级别"提升到"类级别"。理解 static 变量是所有实例共享的"全局黑板"、static 方法是"没有 this 的工具函数",以及 static 代码块在类加载时的执行机制。这是理解 JVM 类加载过程和单例模式的前置知识。
static 的哲学:从"你的"到"大家的"
周二下午,小崔在 Employee 类里定义了一个 companyName 变量。每创建一个员工,都重复存一遍"飞翔科技"这四个字。白歌看了一眼说:"小崔,公司名应该是所有员工共享的——加个 static 让它变成类级别的变量,所有对象只用存一份。"
小崔问道:"那 static 到底是什么?"
白歌在白板上画了两个圈:
static 修饰成员的含义:该成员不属于任何特定的实例对象,而是属于整个类。它在类加载时就已存在,不依赖于任何对象的创建。
static 修饰成员变量(类变量)
存储位置与生命周期
| 对比维度 | 实例变量(非 static) | 类变量(static) |
|---|---|---|
| 存储位置 | 堆内存中的对象内部 | 方法区(JDK 8:元空间 Metaspace) |
| 内存份数 | 每个对象各有一份 | 全类只有一份 |
| 创建时机 | 对象被 new 时 | 类被加载时(早于任何对象创建) |
| 销毁时机 | 对象被 GC 回收时 | 类被卸载时(通常 JVM 退出时才卸载) |
| 访问方式 | 对象.变量 | 类名.变量(推荐)或 对象.变量(不推荐) |
| 默认值 | 有 | 有 |
public class Employee {
// 实例变量 —— 每个员工各不相同
private long id;
private String name;
private double salary;
// 类变量 —— 所有员工共享
public static String companyName = "飞翔科技";
private static int employeeCount = 0; // 用 static 做全局计数器
public Employee(long id, String name, double salary) {
this.id = id;
this.name = name;
this.salary = salary;
employeeCount++; // 每次创建新员工,计数器 +1
}
public static int getEmployeeCount() {
return employeeCount;
}
}
// 使用
public class StaticDemo {
public static void main(String[] args) {
System.out.println(Employee.companyName); // ✅ 推荐:通过类名访问
Employee emp1 = new Employee(1001, "大翔", 50000);
Employee emp2 = new Employee(1002, "白歌", 35000);
System.out.println(emp1.companyName); // 可以但不推荐
System.out.println(Employee.companyName); // ✅ 最佳实践
// 修改静态变量——通过一个引用修改,对所有引用可见
Employee.companyName = "飞翔科技集团(更名)";
System.out.println(emp1.companyName); // 输出:飞翔科技集团(更名)
System.out.println(emp2.companyName); // 输出:飞翔科技集团(更名)
System.out.println("当前员工数:" + Employee.getEmployeeCount()); // 2
}
}
static 修饰方法(类方法)
核心特性
| 特性 | 说明 |
|---|---|
| 属于类 | 通过 类名.方法名() 调用,无需创建对象 |
| 不能访问实例成员 | 静态方法中没有 this,无法直接访问实例变量和实例方法 |
| 可以访问静态成员 | 可以访问其他静态变量和静态方法 |
| 不能被重写 | 静态方法可以被继承,但不能被重写(可被"隐藏") |
| 工具类的首选 | 如 Math、Arrays、Collections 中的方法 |
public class MathUtils {
// 私有构造方法防止实例化(工具类惯用手法)
private MathUtils() { }
// 静态方法 —— 可以直接通过类名调用
public static long factorial(int n) {
if (n < 0) throw new IllegalArgumentException("n 不能为负数");
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
public static double circleArea(double radius) {
return Math.PI * radius * radius;
}
}
// 使用
long f5 = MathUtils.factorial(5); // 120 —— 不需要 new MathUtils()
为什么静态方法不能访问实例成员?
public class Demo {
private int instanceVar = 10; // 实例变量
private static int staticVar = 20; // 静态变量
public void instanceMethod() {
System.out.println(instanceVar); // ✅ 有 this,可以访问
System.out.println(staticVar); // ✅ 也可以访问静态变量
}
public static void staticMethod() {
// System.out.println(instanceVar); // ❌ 没有 this,不知道是谁的 instanceVar
System.out.println(staticVar); // ✅ 静态变量是类级别的,当然可以访问
}
}
根本原因:调用实例方法时,JVM 必须知道"是哪个对象调用的",因此会传入 this;调用静态方法时,可能根本没有对象存在(Demo.staticMethod()),没有 this,自然无法访问属于某个具体对象的实例成员。
static 代码块
static 代码块在类加载时执行,且仅执行一次。常用于复杂的静态变量初始化。
public class AppConfig {
public static String DB_URL;
public static String DB_USERNAME;
public static String DB_PASSWORD;
// static 代码块 —— 类加载时执行一次
static {
System.out.println("正在加载飞翔科技系统配置...");
// 模拟读取配置文件
DB_URL = "jdbc:mysql://10.0.0.1:3306/feixiang";
DB_USERNAME = "feixiang_admin";
DB_PASSWORD = "FX@2025#Secure";
System.out.println("配置加载完成!");
System.out.println("DB_URL = " + DB_URL);
}
}
static 代码块 vs 普通代码块 vs 构造方法
| 代码块类型 | 执行时机 | 执行次数 | 典型用途 |
|---|---|---|---|
| static 代码块 | 类加载时 | 1 次 | 加载配置、注册驱动、初始化静态集合 |
构造代码块 {} | 每次 new 对象时,在构造方法前执行 | 每次 new 都执行 | 所有构造方法的公共初始化代码 |
| 构造方法 | 每次 new 对象时 | 每次 new 都执行 | 对象的特定初始化 |
public class InitOrderDemo {
// 静态变量
private static int staticVar = initStaticVar();
private static int initStaticVar() {
System.out.println("① 静态变量初始化");
return 100;
}
// static 代码块
static {
System.out.println("② 静态代码块执行");
}
// 实例变量
private int instanceVar = initInstanceVar();
private int initInstanceVar() {
System.out.println("③ 实例变量初始化");
return 200;
}
// 构造代码块
{
System.out.println("④ 构造代码块执行");
}
// 构造方法
public InitOrderDemo() {
System.out.println("⑤ 构造方法执行");
}
public static void main(String[] args) {
System.out.println("--- 开始创建第一个对象 ---");
new InitOrderDemo();
System.out.println("--- 开始创建第二个对象 ---");
new InitOrderDemo();
}
}
运行输出:
① 静态变量初始化
② 静态代码块执行
--- 开始创建第一个对象 ---
③ 实例变量初始化
④ 构造代码块执行
⑤ 构造方法执行
--- 开始创建第二个对象 ---
③ 实例变量初始化
④ 构造代码块执行
⑤ 构造方法执行
关键观察:静态初始化只执行一次(第 2 个对象时没有"①"和"②"),而实例初始化每次都执行。
静态导入(import static,JDK 5+)
静态导入允许直接使用其他类的静态成员,无需每次都写类名。
// 传统方式
public class WithoutStaticImport {
public static void main(String[] args) {
double result = Math.sqrt(Math.pow(3, 2) + Math.pow(4, 2));
System.out.println("斜边长: " + result);
}
}
// 静态导入方式
import static java.lang.Math.sqrt;
import static java.lang.Math.pow;
// 或者:import static java.lang.Math.*;
public class WithStaticImport {
public static void main(String[] args) {
double result = sqrt(pow(3, 2) + pow(4, 2)); // 省略 Math.
System.out.println("斜边长: " + result);
}
}
| 使用建议 | 说明 |
|---|---|
| ✅ 推荐 | 高频使用的静态常量或工具方法:import static java.lang.Math.PI |
| ✅ 推荐 | 测试框架断言:import static org.junit.Assert.* |
| ❌ 不推荐 | 滥用 * 导入,导致代码可读性下降——读者分不清方法是本地的还是导入的 |
| ❌ 不推荐 | 导入多个类的同名静态方法——产生歧义 |
完整示例:飞翔科技全局配置与员工计数器
场景
大翔要求小崔实现一个系统全局配置类,并让 Employee 类具备自动计数能力。孔蓝负责验证计数逻辑在多线程场景下是否仍然准确。
/**
* 飞翔科技 —— 系统配置类
* 演示:static 变量、static 代码块、静态导入
*/
public class SysConfig {
// ========== 静态配置项 ==========
public static final String COMPANY_NAME = "飞翔科技";
public static final String COMPANY_SHORT = "飞翔科技";
public static final double MIN_SALARY = 3000.0;
public static final double MAX_SALARY = 500000.0;
public static String DB_URL;
public static String DB_USER;
public static String DB_PASS;
// ========== static 代码块:系统启动时加载 ==========
static {
System.out.println(">>> 飞翔科技系统初始化 <<<");
DB_URL = "jdbc:mysql://db.feixiang.net:3306/hr";
DB_USER = "hr_admin";
DB_PASS = "hr@2025";
System.out.println("数据库配置加载完成...");
}
// 私有构造方法 —— 防止实例化
private SysConfig() { }
// 静态工具方法
public static boolean isSalaryValid(double salary) {
return salary >= MIN_SALARY && salary <= MAX_SALARY;
}
}
/**
* 飞翔科技 —— Employee 类(带 static 计数器)
*/
import static com.feixiang.SysConfig.COMPANY_NAME;
import static com.feixiang.SysConfig.isSalaryValid;
public class Employee {
private long id;
private String name;
private double salary;
// ========== static 计数器 ==========
private static long nextId = 1000; // 工号生成器
private static int employeeCount = 0; // 员工总数
public Employee(String name, double salary) {
if (!isSalaryValid(salary)) { // 使用静态导入的方法
throw new IllegalArgumentException(
"薪资 " + salary + " 不合法,范围:[" + SysConfig.MIN_SALARY + ", " + SysConfig.MAX_SALARY + "]");
}
this.id = ++nextId; // 自动生成工号
this.name = name;
this.salary = salary;
employeeCount++;
}
public void showInfo() {
System.out.printf("[%s] 工号:%d | 姓名:%s | 薪资:%.0f%n",
COMPANY_NAME, id, name, salary); // COMPANY_NAME 来自静态导入
}
// ========== 静态方法 ==========
public static int getEmployeeCount() {
return employeeCount;
}
public static String getCompanyInfo() {
return COMPANY_NAME + " — 员工总数:" + employeeCount;
}
}
/**
* 测试类
*/
public class StaticTest {
public static void main(String[] args) {
System.out.println("===== " + Employee.getCompanyInfo() + " =====\n");
// 创建员工(static 计数器自动递增)
Employee emp1 = new Employee("大翔", 50000);
Employee emp2 = new Employee("白歌", 35000);
Employee emp3 = new Employee("小崔", 8000);
Employee emp4 = new Employee("黄俪", 12000);
Employee emp5 = new Employee("孔蓝", 15000);
emp1.showInfo();
emp2.showInfo();
emp3.showInfo();
emp4.showInfo();
emp5.showInfo();
System.out.println("\n" + Employee.getCompanyInfo());
// 测试薪资校验
System.out.println("\n--- 薪资校验 ---");
System.out.println("5000 是否合法?" + isSalaryValid(5000)); // true
System.out.println("2000 是否合法?" + isSalaryValid(2000)); // false
// 尝试创建薪资不合法的员工
try {
new Employee("测试", 100);
} catch (IllegalArgumentException e) {
System.out.println("异常:" + e.getMessage());
System.out.println("员工总数仍为:" + Employee.getEmployeeCount()); // 5(没有 +1)
}
}
}
运行输出:
>>> 飞翔科技系统初始化 <<<
数据库配置加载完成...
===== 飞翔科技 — 员工总数:0 =====
[飞翔科技] 工号:1001 | 姓名:大翔 | 薪资:50000
[飞翔科技] 工号:1002 | 姓名:白歌 | 薪资:35000
[飞翔科技] 工号:1003 | 姓名:小崔 | 薪资:8000
[飞翔科技] 工号:1004 | 姓名:黄俪 | 薪资:12000
[飞翔科技] 工号:1005 | 姓名:孔蓝 | 薪资:15000
飞翔科技 — 员工总数:5
--- 薪资校验 ---
5000 是否合法?true
2000 是否合法?false
异常:薪资 100.0 不合法,范围:[3000.0, 500000.0]
员工总数仍为:5
易错场景
反例一:静态方法中访问实例变量
// ❌ 错误:静态方法中直接使用实例变量
public class Employee {
private String name; // 实例变量
public static void printName() {
// ❌ 编译错误:无法从静态上下文中引用非静态变量 name
// System.out.println(name);
// 编译器问:name 是谁的 name?没有 this,无法确定
}
}
纠正:
// ✅ 正确:如果确实需要,传入对象参数
public static void printName(Employee emp) {
System.out.println(emp.name); // 明确指定是哪个对象的 name
}
反例二:误用对象引用访问静态成员
// ❌ 不推荐:通过 null 引用访问静态方法(虽然语法上可以!)
Employee emp = null;
emp.getEmployeeCount(); // 竟然不抛 NullPointerException!
// 因为 getEmployeeCount() 是静态方法,编译器会直接替换为 Employee.getEmployeeCount()
// 这造成了"可以通过 null 调用方法"的假象,极度迷惑
// ✅ 正确:始终通过类名访问静态成员
Employee.getEmployeeCount();
反例三:静态变量导致的"状态污染"
// ❌ 隐患:单元测试中静态变量残留
public class UserService {
private static int loginAttempts = 0; // 全局登录尝试次数
public void login(String username, String password) {
loginAttempts++;
// ...
}
}
// 测试类
public class UserServiceTest {
@Test
public void test1() {
new UserService().login("a", "p");
// loginAttempts 现在是 1
}
@Test
public void test2() {
new UserService().login("b", "p");
// loginAttempts 是 2!因为 test1 没有重置它!
// 测试之间互相影响 —— 非常难排查
}
}
纠正:测试框架(如 JUnit)的 @Before 或 @After 中手动重置静态变量,或避免使用可变静态变量。
面试考点
Q1:static 关键字可以修饰哪些内容?
① 成员变量:变成类变量,所有实例共享,存储在方法区/元空间; ② 成员方法:变成类方法,可通过类名直接调用,内部不能使用
this; ③ 代码块:类加载时执行一次,常用于复杂静态变量的初始化; ④ 内部类:静态内部类(static nested class),不持有外部类引用(详见内部类章节); ⑤ 导入:import static静态导入,可直接使用其他类的静态成员。
Q2:为什么静态方法中不能使用 this 和 super?
this和super都依赖于当前对象。静态方法属于类级别,调用时可能根本没有对象存在(如ClassName.staticMethod())。JVM 不会为静态方法传入this引用,因此在静态方法中使用this或super会导致编译错误。
Q3:静态变量和实例变量的内存分配有什么区别?
静态变量存储在方法区(JDK 8 后为元空间 Metaspace),类加载时分配内存,类卸载时释放,全类只有一份。 实例变量存储在堆内存中,位于每个对象内部,随对象创建而分配,随对象被 GC 回收而释放,每个对象各有一份。 一个常见误解是"对象存储在堆中,静态变量当然也在堆中"——实际上,堆中的对象通过 Klass Pointer(类型指针)指向方法区中的类元数据来访问静态变量。
Q4:什么情况下应该使用静态导入?
① 高频使用的常量(如
Math.PI、TimeUnit.SECONDS); ② 测试断言方法(如 JUnit 的assertEquals、Mockito 的mock); ③ 静态工厂方法频繁调用时。 避免滥用:不要用*导入,不要导入多个同名的静态方法,不要让代码丢失"来源可追溯性"。
Q5:static 和 final 一起使用的含义是什么?
static final表示全局常量。static确保只有一份拷贝,final确保不可修改。命名惯例为全大写+下划线(MAX_VALUE)。如果是基本类型或 String,编译期常量会在编译时内联到引用处,提高性能。
小崔的笔记:"
static把东西从'我的'变成'大家的'。静态变量存在方法区,所有对象共享同一份。静态方法没有this,所以不能碰实例变量——就像公司公告不能写'今天张三请假',只能写'所有员工注意'。static代码块在类加载时跑一次,非常适合初始化配置。"