在 Android 面试里,static 关键字经常被包装成一道开放题:"你在项目里是怎么用的?"这道题答不好,往往不是知识不够,而是从来没把做过的事按"场景、做法、结果"的结构梳理过。static 看着简单,真要讲透,它牵出的是类加载、内存布局、生命周期三件大事。
先把结论放在前面:static 成员属于"类"而非"实例",在类加载的 <clinit> 阶段完成初始化,且 JVM 保证这个初始化过程线程安全。静态方法没有 this,只能访问静态成员,也因此无法被重写——它只能被"隐藏"。这题要答好,关键是把概念落到具体的工程场景里,讲出取舍和代价。
这篇把static 关键字全景一次讲透:它是什么、怎么工作、项目里怎么用、坑在哪。
static 到底改了什么
普通字段属于每个实例各一份,static 字段则全类只有一份,所有实例共享。静态方法、静态代码块、静态内部类同理,都挂在类上而不是对象上。类加载时,JVM 会对 <clinit> 加锁,保证静态初始化只执行一次,这意味着"静态变量的初始化是线程安全的"——但这只是初始化那一下,后续对它的读写并不自带同步。
一个关键区分:静态方法不能被重写(override),因为重写依赖运行期虚分派,而静态方法在编译期就绑定到声明类型;子类写同名静态方法,那叫"隐藏"(hide),用父类引用调到的仍是父类那份。非静态内部类会隐式持有外部实例引用 Outer.this,静态内部类则不会——这正是单例、Builder、HandlerThread 的 ViewHolder 这类写法爱用静态内部类来规避泄漏的根本原因。
这些坑的正确绕法
最常见的坑是把 Activity、Context、View 这类有生命周期的对象赋给静态变量,导致 Activity 销毁后依旧被静态引用钉在内存里无法回收,这是 Android 内存泄漏里最常见的一类。正确做法是用弱引用,或把静态持有的引用在 onDestroy 时主动置空,必要时干脆不持有。
其次是用静态集合做缓存却只增不减,各个页面不断 list.add(...),从不清理,时间一长就是 OOM。如果确实要用静态缓存,至少配上 LRU 上限、或定时/按事件触发清理,别让它在那里无限生长。
还有一个更隐蔽的坑:静态字段的初始化顺序依赖。静态代码块和静态字段按出现顺序执行,若 A 依赖 B 而 B 写在后面,初始化阶段就会拿到默认值而不是预期值。把这类强依赖显式理顺,或挪到方法里懒加载,比靠书写顺序赌运气稳得多。
一段代码看懂差异
看一段能直接跑的代码,把上面的机制落到具体写法上:
class Cfg {
static final int MAX = 100; // 类级共享一份,且不可改
static int count; // 静态字段,所有实例共享
static {
count = 0; } // 静态代码块,类加载时执行一次
static void inc() {
count++; } // 静态方法:没有 this,不能碰实例字段
static class Node {
int v; } // 静态内部类:不持外部实例引用
class Inner {
void ok() {
Cfg.this.inc(); } } // 非静态内部类:持有 Outer.this
}
这段代码值得盯三处:第一处,static final int MAX 是类级常量,全类一份且不可变;第二处,静态方法 inc 没有 this,只能操作 count 这类静态成员;第三处,静态内部类 Node 不持有外部引用,非静态内部类 Inner 则通过 Cfg.this 持有。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
"请简单介绍一下 static,它在 Android 开发中起什么作用?"按"是什么 → 干什么用 → 在你的项目里怎么用"递进,全程不超过三分钟。比如静态工具方法减少对象创建、静态内部类实现懒加载单例、静态常量收敛配置。结尾补一句"静态内部类不持外部引用"的取舍,立刻能拉开差距。
"static 的底层原理是什么?"先讲动机:类级共享、省去每实例一份;再拆机制:类加载 <clinit> 加锁保证初始化只跑一次、静态方法编译期绑定;最后主动交代边界:初始化的线程安全不等于后续读写同步、静态方法无法重写只能隐藏。讲机制时配上面那段核心代码更稳。
"使用 static 时遇到过什么问题?"事故案例四段讲:现象(内存只涨不跌)、定位(用 Profiler/LeakCanary 看到 Activity 被静态引用链钉住)、修复(改弱引用或置空)、验证(反复进出页面观察内存曲线)。定位手段和量化效果是重点。
"static 和替代方案相比有什么优劣?"从性能、易用性、生命周期安全等维度对比。比如要共享状态,静态字段简单但风险高,单例可控但需注意初始化时机,依赖注入更解耦但引入框架成本。选型时把"静态持有 Context 造成泄漏""静态集合只增不减导致 OOM"这类代价摆到台面上,再决定引入与否。
static 的初始化顺序还有一层常考细节:父类的静态块和静态字段,先于子类的静态块执行;而静态初始化又早于任何实例的创建。这意味着依赖"父类 static 给子类 static 供值"是成立的,反过来则不行。顺着这条线,序列化也是个易错点:static 字段属于类、不属于对象,因此它不参与对象的默认序列化,反序列化得到的对象其 static 字段取的是"当前类加载时那份值",而不是当初序列化的那份——以为 static 会被一起存盘,是新手常见的误解。
放到 Android 语境里,static 的坑更具体。多进程场景下,每个进程有独立的类加载器和独立的 static 命名空间,一个进程里改了 static 变量,另一个进程根本看不到;把跨进程共享状态押在 static 上只会得到诡异的不一致。配置变更(比如屏幕旋转)会销毁重建 Activity,但 static 变量不随 Activity 重建而重置,若它在某个旧 Activity 生命周期里被写脏却没清理,新 Activity 一进来就读到错误状态。再加上 static 字段无法被垃圾回收,长期驻留,这些都是把它当"全局变量"随手用必须付出的代价。
static 还常在单例与并发里被考。经典的双检锁单例既要 static 持有实例,又要给实例字段加 volatile 禁止"分配内存—赋值引用—调用构造"这三步被重排,否则别的线程可能拿到"引用非空但构造未完"的半成品。这里 static 负责"类级唯一一份",volatile 负责"发布可见且有序",两者职责不同,缺一不可。另一个容易忽略的点是测试污染:static 变量跨测试用例存活,前一个用例写进去的状态若没清理,会悄悄影响后一个用例的结果,所以带 static 状态的类在测试里要么重置、要么用独立 ClassLoader。
把上面几条串起来——类加载一次性初始化、静态内部类不持外部引用、静态字段不随序列化走、多进程各一份、配置变更不重置、单例要配 volatile——static 就不再是"背几个定义",而是一张覆盖内存、生命周期、并发的完整地图,这恰恰是面试官想听到的层次。
最后给你几条可落地的建议
static 的理论光读容易忘,配合一次真实编码与复盘,理解才算落袋。面试中关于这题,关键是从原理、应用、踩坑三个层面给出有深度的回答,而不是列定义。
再补一条工程经验:写一个静态内部类单例并解释它如何同时实现懒加载与线程安全(双重检查锁配合 volatile),是展示你真正懂 static 的高分动作。
复习时别孤立刷题:final 关键字、类加载顺序这些和 static 是绑定的,理清之后再去看下一篇 final 的四种用法,和今天这篇正好凑成一对。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:this-与-super:最容易混淆的两个指向
下一篇预告:final-的四种用法:变量、方法、类与参数
有任何问题欢迎在评论区留言交流。