一、💡 一句话理解
核心结论
static表示成员属于类本身,而不是属于某一个对象。静态成员由同一个类的对象共享,通常使用类名.成员名访问。
面试时先回答这一句,再展开静态变量、静态方法、静态代码块和 static final。
二、🧭 理论:它是什么
2.1 static 和普通成员有什么区别
class Student {
// 静态变量:属于 Student 类,所有 Student 对象共享。
static String schoolName = "第一中学";
// 实例变量:属于具体对象,每个 Student 都有自己的 name。
String name;
}| 对比项 | 静态成员 | 实例成员 |
|---|---|---|
| 属于谁 | 类 | 对象 |
| 有多少份 | 同一个类通常一份 | 每个对象一份 |
| 推荐访问方式 | Student.schoolName | student.name |
| 是否需要先创建对象 | 不需要 | 需要 |
能否直接使用 this | 不能 | 能 |
2.2 static 可以修饰什么
面试常见的四种形式:
class Example {
static int count; // 静态变量
static void print() { // 静态方法
}
static { // 静态代码块
count = 10;
}
static class Helper { // 静态内部类,更准确叫静态嵌套类
}
}static 不能修饰构造方法和普通局部变量。普通顶层类也不能声明为 static。
三、⚙️ 理论:面试常问什么
3.1 static 变量和实例变量有什么区别
静态变量属于类,所有对象共享;实例变量属于对象,每个对象单独保存。
class User {
static int total;
String name;
}创建两个 User 后:
total只有一份,两个对象都能看到;name有两份,每个对象保存自己的值。
共享不代表线程安全
多个线程同时修改静态变量时,仍可能出现数据竞争。
static不提供原子性、可见性或互斥,相关知识见 6.volatile、7.AtomicInteger 和 锁与线程同步。
3.2 静态方法能直接访问实例变量吗
不能。静态方法属于类,调用时可能还没有创建任何对象,因此 Java 不知道应该访问哪个对象的数据。
class User {
String name;
static void printName() {
// 编译错误:静态方法不知道要读取哪个 User 对象的 name。
// System.out.println(name);
}
}如果静态方法获得了具体对象,就可以通过该对象访问:
static void printName(User user) {
System.out.println(user.name);
}访问规则可以记成:
| 当前位置 | 直接访问静态成员 | 直接访问实例成员 |
|---|---|---|
| 实例方法 | 可以 | 可以 |
| 静态方法 | 可以 | 不可以,必须先获得对象 |
3.3 静态方法中为什么不能使用 this
this 表示当前对象。静态方法不依赖某个对象调用,所以没有当前对象,也就不能使用 this。
class User {
String name;
void instanceMethod() {
// 实例方法针对当前对象执行,可以使用 this。
System.out.println(this.name);
}
static void staticMethod() {
// 编译错误:静态上下文中没有 this。
// System.out.println(this.name);
}
}3.4 static 代码块什么时候执行
静态代码块在类初始化时执行,并且对一次类初始化只执行一次。静态字段的显式赋值和静态代码块按照源码中的先后顺序执行。
class InitDemo {
static int first = print("1. 静态变量");
static {
System.out.println("2. 静态代码块");
}
static int print(String message) {
System.out.println(message);
return 1;
}
}类的加载、准备和初始化不是同一个阶段,详细过程见 2. JVM的类加载机制。
3.5 static final 一定是常量吗
static 表示类共享,4.final 表示变量只能赋值一次。两者常一起用于声明常量:
static final int MAX_RETRY_TIMES = 3;但如果保存的是对象引用,final 只保证引用不能重新指向其他对象,不保证对象内容不能改变:
import java.util.ArrayList;
import java.util.List;
static final List<String> NAMES = new ArrayList<>();
// 可以修改 List 内部的数据。
NAMES.add("小明");
// 不能让 NAMES 指向另一个 List。
// NAMES = new ArrayList<>();因此,static final 引用不等于不可变对象。
3.6 static 方法可以被重写吗
先记住结论:static 方法不能被重写。
子类可以定义一个与父类同名、参数也相同的静态方法,但这叫方法隐藏(method hiding),不是方法重写。
3.6.1 为什么它不算重写
方法重写解决的问题是:
同一个实例方法,面对不同的实际对象,运行时应该执行哪一个版本?
例如,变量表面上是 Parent 类型,实际装入的是 Child 对象。调用实例方法时,Java 会在运行时查看实际对象,因此执行 Child 的方法:
class Parent {
void instanceMethod() {
System.out.println("Parent 实例方法");
}
}
class Child extends Parent {
@Override
void instanceMethod() {
System.out.println("Child 实例方法");
}
}
Parent value = new Child();
value.instanceMethod(); // 输出 Child 实例方法判断过程是:
value的编译时类型是Parent;value实际指向的对象是Child;instanceMethod()是实例方法,具有运行时多态;- 所以运行时根据实际对象找到
Child.instanceMethod()。
但是静态方法属于类,不属于某个实际对象。调用静态方法时,编译器根据左边的类名或变量的编译时类型,就已经决定调用哪个类的方法,不会等到运行时再检查对象。
3.6.2 同名 static 方法实际发生了什么
class Parent {
static void staticMethod() {
System.out.println("Parent 静态方法");
}
}
class Child extends Parent {
// 这里不能写 @Override,因为这不是重写,而是隐藏父类的同名静态方法。
static void staticMethod() {
System.out.println("Child 静态方法");
}
}
Parent first = new Child();
Child second = new Child();
first.staticMethod(); // 输出 Parent 静态方法
second.staticMethod(); // 输出 Child 静态方法逐行判断:
| 调用代码 | 变量的编译时类型 | 编译器绑定的方法 | 输出 |
|---|---|---|---|
first.staticMethod() | Parent | Parent.staticMethod() | Parent 静态方法 |
second.staticMethod() | Child | Child.staticMethod() | Child 静态方法 |
虽然 first 实际也指向 Child 对象,但这件事对静态方法没有影响。因为静态方法不是交给对象执行的。
不要用对象变量调用静态方法
Java 允许写
first.staticMethod(),但这种写法很容易让人误以为存在运行时多态。实际开发中应写成Parent.staticMethod()或Child.staticMethod(),直接表明方法属于哪个类。
可以把两者记成:
- 实例方法看实际对象:可以被重写,运行时决定;
- 静态方法看声明类型:只能被隐藏,编译时决定。
3.7 main 方法为什么是 static
public static void main(String[] args) {
}先把程序启动前的场景想清楚。执行下面的命令时:
java ApplicationJava 启动器只得到了一个类名 Application。它会加载这个类,然后寻找约定好的入口方法:
public static void main(String[] args)此时启动器还没有创建 Application 对象,也不应该擅自决定怎样创建。
3.7.1 如果 main 不是 static,会出现什么问题
假设入口方法是实例方法:
public void main(String[] args) {
}那么启动器必须先做类似下面的事情:
Application application = new Application();
application.main(args);问题马上出现了:
- 如果类没有无参构造方法,应该调用哪个构造方法?
- 构造参数从哪里来?
- 如果类是抽象类,根本不能创建对象,怎么办?
- 如果创建对象时就要依赖配置、数据库连接或其他对象,又该由谁准备?
Java 没有替程序员猜这些规则,而是把入口方法设计为 static。这样启动器只要知道类名,就能直接调用类方法;不需要先获得某个对象。
可以把启动过程简单理解成:
执行 java Application
↓
加载 Application 类
↓
直接调用 Application.main(args)
↓
从 main 内部开始执行程序3.7.2 static main 不代表程序不能创建对象
main 只是程序启动的入口。进入 main 以后,当然可以根据程序自己的规则创建对象:
public class Application {
public static void main(String[] args) {
// Java 启动器只负责调用静态入口。
// 进入入口以后,由程序自己决定创建什么对象、传什么参数。
Application application = new Application("生产环境");
application.start();
}
private final String environment;
Application(String environment) {
this.environment = environment;
}
void start() {
System.out.println("启动环境:" + environment);
}
}这里的职责分工是:
| 角色 | 负责什么 |
|---|---|
| Java 启动器 | 根据固定规则找到并调用静态 main |
main 方法 | 接收启动参数,组织程序的启动过程 |
| 程序自己的代码 | 按真实需要创建对象、传入参数并调用实例方法 |
一句话记忆
main是static,因为程序启动时“只有类,还没有对象”;等进入main后,再由程序自己决定要创建哪些对象。
3.8 static 变量存在哪里
面试时不要简单回答“都在方法区”。更稳妥的说法是:
静态字段属于类,它的生命周期通常与对应的
Class一致。具体物理存放位置属于 JVM 实现细节,不能仅凭 Java 语言规范断言所有 JVM 都放在同一个固定区域。
如果面试官继续问 HotSpot 的内存结构,再结合具体 JDK 版本讨论。Java 内存区域见 1. JVM运行时数据区。
四、🚀 实践:把面试题跑一遍
4.1 前置准备
只需要 JDK 17 或更高版本,不需要第三方依赖:
java -version
javac -version4.2 这个例子验证什么
下面的完整程序只验证面试高频问题:
- 静态变量是否被多个对象共享;
- 实例变量是否属于各自对象;
- 静态代码块何时执行;
- 静态方法如何访问实例数据;
static final如何使用;- 静态方法隐藏和实例方法重写有什么区别。
4.3 完整实践:StaticInterviewDemo.java
创建 StaticInterviewDemo.java:
public class StaticInterviewDemo {
public static void main(String[] args) {
// 第一次主动使用 Student 时,会先初始化 Student 类。
// 因此静态代码块会在创建第一个对象前执行。
Student first = new Student("小明");
Student second = new Student("小红");
// name 是实例变量,每个对象保存自己的值。
System.out.println("第一个学生:" + first.name);
System.out.println("第二个学生:" + second.name);
// total 是静态变量,两个对象共享同一份计数。
System.out.println("学生总数:" + Student.total);
// static final 常量通过类名访问。
System.out.println("学校名称:" + Student.SCHOOL_NAME);
// 静态方法没有 this,但可以通过传入的对象读取实例变量。
Student.printName(first);
// 静态方法发生隐藏:调用结果由引用的编译时类型决定。
Parent parent = new Child();
parent.staticMethod();
// 实例方法发生重写:运行时调用实际对象 Child 的方法。
parent.instanceMethod();
}
static class Student {
// static final:属于类,并且不能重新赋值。
static final String SCHOOL_NAME = "第一中学";
// 静态变量:所有 Student 对象共享。
static int total;
// 实例变量:每个 Student 对象各自保存。
String name;
static {
// 类初始化时执行一次。
System.out.println("Student 静态代码块执行");
}
Student(String name) {
// this 代表当前创建的 Student 对象。
this.name = name;
// 每创建一个对象,共享计数加一。
total++;
}
static void printName(Student student) {
// 静态方法通过明确的对象访问实例变量。
System.out.println("静态方法打印姓名:" + student.name);
}
}
static class Parent {
static void staticMethod() {
System.out.println("Parent 静态方法");
}
void instanceMethod() {
System.out.println("Parent 实例方法");
}
}
static class Child extends Parent {
// 这是静态方法隐藏,不是重写。
static void staticMethod() {
System.out.println("Child 静态方法");
}
// 这是实例方法重写,具有运行时多态。
@Override
void instanceMethod() {
System.out.println("Child 实例方法");
}
}
}编译并运行:
javac StaticInterviewDemo.java
java StaticInterviewDemo预期输出:
Student 静态代码块执行
第一个学生:小明
第二个学生:小红
学生总数:2
学校名称:第一中学
静态方法打印姓名:小明
Parent 静态方法
Child 实例方法结果说明:
- 静态代码块只输出一次;
- 两个对象共享
total,最终值是2; - 两个对象的
name互不影响; parent.staticMethod()调用Parent,说明静态方法是隐藏;parent.instanceMethod()调用Child,说明实例方法发生了重写。
4.4 高频报错:静态方法直接访问实例变量
class User {
String name;
static void print() {
// 编译错误:non-static variable name cannot be referenced
// from a static context
System.out.println(name);
}
}原因:静态方法没有当前对象,不知道应该读取哪个 name。
不要为了消除报错就随手把 name 改成 static。如果姓名属于具体用户,应把 print() 改成实例方法,或者向静态方法传入一个 User 对象。
五、📌 总结
static成员属于类,实例成员属于对象。- 静态方法没有
this,不能直接访问实例成员。 - 静态代码块在类初始化时执行,一次类初始化只执行一次。
static final引用不能重新赋值,但对象内容仍可能可变。- 静态方法在继承中是隐藏,不是重写。
static共享变量不会自动获得线程安全。
面试记忆句:类共享用 static,对象独有不用 static;静态方法没有当前对象,所以没有 this。