不夸张地说,围绕“面向对象”和“static”这两个关键词,每天都有大量开发者在踩坑、搜索、试错。你大概率也遇到过这种情况:代码里加了一个static,程序突然好使了,但你根本说不清它为什么好使;又或者后端接口地址明明是对的,前端却一直报“no static resource course/course/list”,再比如C语言里函数写得没问题,编译器偏偏甩一句“static declaration of 'checkprime' follows non-static declaration”把你堵死。这篇博文我打算换个讲法——不按教科书顺序把static的语法规则列一遍,而是从大家真实遇到的高频报错和设计困境入手,把static在面向对象体系里的内存模型、方法语义、初始化顺序和跨语言差异一次讲透。如果你正在学Java或C++的面向对象部分,或者工作里碰到过静态资源、静态方法的报错却没想明白根因,这篇文章就是给你准备的。
1. 热搜词里的static:一个关键字,半个事故现场
1.1 三条高频报错背后的共同主角
我们先看几个真实出现过的报错信息,它们表面上互不相干,实际都牵涉到“static”在不同层面的语义。
第一条是后端同学最常见的:“no static resource course/course/list for request '/course/course/list'”。这条报错出现在Spring Boot 3.x里,请求/course/course/list没有命中任何Controller方法,Spring Web框架就会尝试把它当成静态资源去找。它会在classpath下的/static、/public、/resources、/META-INF/resources这几个目录里寻找course/course/list这个路径对应的文件,找不到时就抛出NoResourceFoundException,最终呈现给你的是带“no static resource”字样的错误。很多人一看到“static resource”就以为是静态资源配置出了问题,其实它往往只是“没有对应的接口映射”这个事实的另一种表达方式。
第二条是C语言开发者的老朋友:“static declaration of 'checkprime' follows non-static declaration”。这句话翻译过来是“checkprime的静态声明出现在非静态声明之后”。意思是头文件或前向声明里没写static,但函数定义处写了static,编译器发现同一个函数的前后声明中链接属性不一致,于是直接报错。这属于C语言中“static修饰函数时控制内部链接属性”的典型规则问题。
第三条来自前端工程化:“error in static/js/vendor.4dc62648b0eecb22e5bf.js from uglifyjs undefined”。这条报错是构建工具在用UglifyJS压缩static/js/vendor文件时,解析到某段语法却无法识别,于是抛出undefined。这里的static只是静态文件目录名,和关键字本身无关,但它同样说明了“static/静态”这个词在整个技术栈里出现频率有多高,误导性就有多强。
1.2 从abaqus到ffmpeg:static的多义性澄清
热搜词里还有两条容易让人彻底迷惑的:abaqus static linear perturbation和ffmpeg 3.4 win64 static。
前者是Abaqus结构分析里的“静力线性摄动分析”,用于计算结构在预载状态下的线性响应,比如屈曲分析和频率提取。这里的“static”是“静力”的意思,和编程没有任何关系。后者是FFmpeg的“静态编译版本”,指的是二进制文件把所有依赖库都打进了一个可执行文件里,运行时不需要额外安装一堆DLL或so文件。这里的“static”指的是链接方式。
把这两个词和前面三条编程报错放在一起看,你会发现“static”这个词在不同领域里各说各话。所以很多初学者查资料时容易跑偏——搜static出来的结果里,一半是编程,一半是结构分析、静态链接、静态资源。这篇文章后续只讨论面向对象语言里的static修饰符,不再涉及其他领域的含义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. static变量:它到底存在哪里?生命周期有多长?
2.1 对象字段与静态字段的内存差异
从Java开始聊。类里定义的普通字段是实例变量,每次new一个对象,JVM就会在堆里为这个对象分配一份独立的实例变量存储空间。而用static修饰的字段是类变量,它不随对象创建而创建,而是随类的加载而初始化,所有对象共享同一份。
看这段代码:
java复制public class Counter {
public int instanceCount = 0;
public static int classCount = 0;
public void increment() {
instanceCount++;
classCount++;
}
public static void main(String[] args) {
Counter a = new Counter();
Counter b = new Counter();
a.increment();
b.increment();
System.out.println("a.instanceCount = " + a.instanceCount); // 1
System.out.println("b.instanceCount = " + b.instanceCount); // 1
System.out.println("Counter.classCount = " + Counter.classCount); // 2
}
}
同样的increment()方法跑了一次,a和b各自的instanceCount都只加了1,但classCount被加到了2。原因很简单:instanceCount属于每一个具体对象,两个对象互不干扰;而classCount属于Counter类本身,无论你创建几个对象,它始终只有一份。
内存位置上,JDK 8之后HotSpot虚拟机把静态变量的引用和值对象都存放在堆中——具体说是堆里的Class对象中。以前的教科书会写“静态变量存在方法区”,那是JDK 7及之前永久代的说法,现在不少资料已经改口。不过面试时如果被问到,你回答“逻辑上属于类元数据,物理上JDK 8之后随Class对象在堆中”是比较稳的。
这个“共享”特性带来一个很实际的好处:全局配置项、共享计数器、缓存池这类数据,用静态变量实现非常方便。但它也有代价——所有对象甚至所有线程都盯着这一份数据,并发修改时如果不加控制,很容易出现可见性和原子性问题。日常开发里,能用局部变量和实例变量解决的问题,没必要非往静态变量上靠。
2.2 类加载、初始化时机与常见误解
静态变量是什么时候被初始化的?很多人以为“类一编译就初始化”,这是错的。Java里静态变量的初始化时机和“类加载”绑在一起,而且精确地说,是“类的初始化”阶段。
类初始化发生在类第一次被主动使用时,典型场景包括:
- 使用
new创建类的实例; - 访问类的静态字段(比如
Counter.classCount); - 调用类的静态方法;
- 使用
Class.forName()反射加载类; - 初始化某个类的子类时,如果父类还没被初始化,会先触发父类的初始化。
注意一个反直觉的点:通过子类访问父类中定义的静态字段,只会触发父类的初始化,不会触发子类的初始化。比如class Cat extends Animal,Animal里有个static String species,你写Cat.species时,JVM只会初始化Animal,Cat不会被初始化。原因是这个静态字段在字节码层面属于Animal,访问它不需要加载Cat。这个细节在很多面试题和实际性能排查中都会出现。
另一个常见误解是“static变量就是全局变量”。Java语言层面并没有传统意义上的全局变量,但静态字段确实可以近似扮演这个角色。用好了,它是共享配置的合理载体;用不好,它就变成藏在一堆类里的隐藏状态,而且这种状态在多线程环境下如果没有加volatile或同步控制,某个线程的修改其他线程不一定能及时看到。
2.3 C++与Java静态存储期的对比
C++里也有静态成员变量,它也属于类而不属于某个对象,但存储上位于静态存储区,生命周期和程序运行周期一致。这里有一个很多人会忽略的差异:Java的静态字段在类加载时由JVM自动分配和初始化,不需要开发者写额外语句;C++则要求在类外单独定义一次。
cpp复制class Counter {
public:
static int classCount;
};
// 必须在类外某个.cpp文件里写这一行
int Counter::classCount = 0;
如果不写类外定义这行代码,链接阶段通常会报“undefined reference to Counter::classCount”。这是C++的语法规则,static成员变量只是声明,类外那行才是定义。不过C++17引入了inline static,允许在类内直接初始化:
cpp复制class Counter {
public:
inline static int classCount = 0;
};
另外C++里还有一个Java没有的用法:函数内的局部静态变量。这种变量只在第一次执行到声明语句时初始化,之后函数每次调用都会跳过初始化语句,直接沿用上一次的值。它的生命周期很长,但作用域仍然被限制在函数内部。这一点和类静态变量完全是两个概念,初学者非常容易混。
3. static方法:为什么main必须静态,工具类为什么偏爱静态
3.1 main(String[] args)为什么必须是static
很多人的第一行Java代码就是public static void main(String[] args),但很少有人停下来想:为什么一定是static?
原因要从JVM的启动流程说起。JVM启动时,它面对的是一个类的名称,比如java com.example.Main。此时JVM还没有创建任何对象,它要做的事情非常简单:加载Main类,然后调用入口函数。如果main不是静态方法,JVM就需要先创建一个Main对象才能调用入口方法——但问题来了,程序本身就是从main开始执行的,还没有任何现成的对象可用。你不能在启动程序之前就“new”一个对象,因为对象创建的代码就写在main里。这成了一个先有鸡还是先有蛋的问题。
解决方法就是让main成为静态方法。静态方法属于类本身,类加载完成后JVM就可以通过类名直接调用,不需要先创建对象。void意味着入口方法不需要返回值,返回值交给谁?启动器只能是整个进程的退出码那个层面。至于String[] args,就是命令行参数的入口载体。
顺带说一句,C#的Main也是static,C语言的main虽然不带static,但它是自由函数,并不属于任何对象,本质思路是一样的。
3.2 静态方法不能访问实例成员的本质原因
理解了main为什么是static之后,另一个高频问题就顺理成章了:为什么静态方法里不能直接访问实例字段和实例方法?
关键在于“this引用”。Java的实例方法在字节码层面会隐式接收一个this参数,方法体内所有实例成员的访问都通过this来完成。而static方法压根没有this,它不属于任何对象,所以编译器无法确定你写的instanceCount到底指哪个对象的字段。你当然可以在static方法里写new Counter().instanceCount,通过显式创建或传入对象引用的方式去访问实例成员,但你不能直接写裸的instanceCount。
这里有一个容易踩的小坑:有些同学在static方法里访问了静态字段和实例字段混在一起,编译器报错后误以为是“static方法里不能访问任何成员”,于是把实例字段也改成static,结果整个类被改得一团糟。其实编译器只是不允许你隐式使用this,通过对象引用访问实例成员完全合法。
3.3 静态导入与工具类设计的取舍
Java里还有一个和static相关的语法叫静态导入:import static java.lang.Math.max;,之后代码里就可以直接写max(a, b),不用再写Math.max(a, b)。
这个特性偶尔用起来很爽,比如写数学计算密集的代码时,能少敲不少字符。但过度使用会严重损害可读性——别人读代码时看到max(),还要去文件头部找它到底是从哪个类导入的。我个人的习惯是:只在某一处反复使用同一个静态方法时做静态导入,而且尽量导入方法而不是通配符import static java.lang.Math.*;,通配符导入会让代码的出处在直觉上变得非常模糊。
工具类偏爱static方法,核心原因是“无状态”。像Math、Collections、Arrays这些工具类,本身不需要保存任何数据,方法只是一个纯粹的函数,输入参数、返回结果,不依赖对象状态。既然没有状态,为什么要创建对象?直接用静态方法调用最干净。
设计自己的工具类时,建议把构造器私有化,防止外部new出无意义的实例,然后把能力通过静态方法暴露出去。但有一点要注意:静态方法难以替换、难以mock,如果工具类内部逻辑复杂、依赖外部服务,强塞成静态方法会拖累单元测试。这时候更应该创建一个普通实例,通过依赖注入把服务传进去,而不是把所有东西都做成static。
4. 静态代码块与静态内部类:初始化顺序的隐形杀手
4.1 静态代码块、实例代码块、构造器的执行顺序
Java里有一个很容易被忽略的语法点:初始化代码块。它分为静态代码块和实例代码块,长得一模一样,只是静态代码块多了一个static关键字。
执行顺序是很多人搞不清楚的:
- 类加载时:静态字段初始化和静态代码块按代码书写顺序执行,且只执行一次;
- 每次创建对象时:实例字段初始化和实例代码块按代码书写顺序执行;
- 最后:构造器方法体执行。
看这个例子:
java复制public class InitOrder {
static { System.out.println("1. 静态代码块"); }
{ System.out.println("2. 实例代码块"); }
public InitOrder() {
System.out.println("3. 构造器");
}
public static void main(String[] args) {
new InitOrder();
new InitOrder();
}
}
输出结果是:
text复制1. 静态代码块
2. 实例代码块
3. 构造器
2. 实例代码块
3. 构造器
注意,静态代码块只打印了一次,实例代码块每次创建对象都会执行。如果存在继承关系,顺序会变成:父类静态代码块 -> 子类静态代码块 -> 父类实例代码块 -> 父类构造器 -> 子类实例代码块 -> 子类构造器。这个顺序在Spring、Hibernate这类框架中经常能派上用场——比如静态代码块里初始化全局连接池,实例代码块里做对象级别的字段校验。
4.2 静态内部类的加载时机与内存泄漏隐患
Java的内部类分为普通内部类和静态内部类,区别不只是有没有static。
普通内部类在编译后会持有外部类实例的引用,也就是那个著名的this$0字段。正因为持有外部类引用,普通内部类可以直接访问外部类的实例字段。但这个引用的代价是:只要内部类对象活着,外部类对象就永远不会被垃圾回收。
静态内部类不持有外部类实例的引用,它和外部类更像是一种“命名空间”的从属关系,而不是对象层面的强关联。所以,一个重要的设计建议是:如果内部类不需要访问外部类的实例成员,一律声明为static。这样既能减少不必要的引用持有,也能避免一部分潜在的内存泄漏。
一个典型的内存泄漏场景是:Android开发中,有人在静态集合里存了Activity的引用,页面销毁后集合依然持有这个Activity,导致它无法被回收,最终OOM。这是“静态引用链过长”的典型问题,和静态内部类无关,但根源同样是“static的生命周期比对象长得多”——静态变量一旦持有实例引用,这个实例就被迫跟着一起延长寿命。
4.3 饿汉式、懒汉式与静态内部类单例
static在单例模式里几乎是绕不开的。最基础的饿汉式写法:
java复制public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
饿汉式的优点是非常简单,线程安全由JVM类加载机制保证。缺点是类一加载就创建实例,如果整个程序运行期根本没用过这个单例,创建实例的开销就白花了。
懒汉式把创建时机推迟到第一次调用getInstance()时,但需要处理并发问题,双检锁写法虽然常见,却也容易写错。
一个兼顾延迟加载和线程安全的经典写法是用静态内部类:
java复制public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
原理是:静态内部类Holder不会被主动加载,只有当getInstance()方法第一次执行、需要访问Holder.INSTANCE时,JVM才会去加载Holder类并执行其静态字段初始化。而JVM保证类初始化过程是线程安全的,所以这个写法天然兼具延迟加载和线程安全两个特性。这是我个人最推荐的单例写法之一,简单、可靠、没有锁。
5. 实战翻车现场:三个与static有关的报错排查全过程
5.1 报错一:no static resource course/course/list,是资源问题还是映射问题?
这条报错我见过太多次,它经常出现在Spring Boot 3.x项目中,完整信息长这样:
text复制No static resource course/course/list for request '/course/course/list'
很多人第一反应是“static目录下的资源没配好”,然后去查application.yml里的spring.web.resources配置,折腾半天还是不行。实际上,排查链路应该是这样的:
第一步,确认这个请求是不是真的没有对应Controller映射。打开Controller类,检查类上的@RequestMapping("/course")和方法上的@GetMapping("/list"),拼出来是/course/list,而不是/course/course/list。很多情况就是路径少写或多写了一层,请求打到了不存在的接口上。
第二步,如果确实没有映射,Spring Boot 3.x会把请求当作静态资源查找。默认的静态资源目录包括classpath:/static/、classpath:/public/、classpath:/resources/、classpath:/META-INF/resources/。它会尝试在这些目录下找course/course/list这个文件,找不到就抛NoResourceFoundException。
第三步,检查自定义配置是否干扰了静态资源路由。如果你在WebMvcConfigurer的addResourceHandlers方法里自定义了资源映射,比如:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/course/**")
.addResourceLocations("file:/opt/course-resources/");
}
}
这样配置后,/course/**的请求会优先走这个自定义的handler,不会进入默认的静态资源目录。如果这个目录下也不存在对应文件,报错信息同样会带“no static resource”。
第四步,检查是否有拦截器或过滤器把请求吞掉了。有时请求确实匹配了Controller,但被拦截器拦截在到达Controller之前,返回了非预期结果。排查时可以临时注释掉拦截器配置,看报错是否消失。
综合下来,这条报错的核心经验是:看到“no static resource”不等于一定是资源文件缺失,先确认接口路径无误,再查静态资源配置,最后看拦截器——按这个顺序排查,大多数情况下五分钟内能定位。
5.2 报错二:static declaration of 'checkprime' follows non-static declaration,C语言链接属性冲突
C语言里这个报错也很有代表性,特别是初学者写头文件和源文件时容易踩中。先看一段会报错的代码:
c复制// main.c
#include <stdio.h>
int checkprime(int n); // 前向声明:非static
static int checkprime(int n) { // 定义:static
if (n < 2) return 0;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return 0;
}
return 1;
}
int main() {
printf("%d\n", checkprime(7));
return 0;
}
编译时报的错正是static declaration of 'checkprime' follows non-static declaration。
原因很明确:C语言里,static修饰函数时表示这个函数具有内部链接属性,即只能在当前编译单元(.c文件)内使用。而文件开头那句int checkprime(int n);没有static,它表示外部链接,意味着其他编译单元也能使用这个函数。编译器在同一个编译单元里看到同一个函数先被声明为外部链接,随后又被定义为内部链接,前后矛盾,无法判定该函数的最终链接属性,于是直接拒绝编译。
修复方式取决于你的真实意图:
- 如果这个函数只在当前
.c文件内部使用,头文件里就不应该暴露它,同时前向声明处也要加上static:
c复制static int checkprime(int n);
static int checkprime(int n) {
// ...
}
- 如果其他编译单元也要用这个函数,那就不要加
static,保持声明和定义一致。
还有一个隐蔽场景:你写的头文件里声明了非static版本,而源文件里定义时写了static。头文件被其他.c文件include后,其他文件看到的是外部链接声明,链接时却找不到符号定义,同样会出问题。排查时记得看头文件。
这个报错给我们的经验是:C语言里static的语义和Java/C++里的“类成员”完全不同,它更多是在控制作用域和链接属性。不要因为Java里static修饰方法是常规操作,就默认C语言里static函数也一样安全,两边差着十万八千里。
5.3 报错三:error in static/js/vendor... from uglifyjs undefined,构建工具的静态文件解析失败
这条报错常见于Vue CLI 2.x或较老的前端工程里,多在执行npm run build时出现。错误信息中包含了static/js/vendor.xxxx.js和from uglifyjs undefined,字面意思是UglifyJS压缩vendor文件时遇到了无法处理的语法。
先说结论:UglifyJS的2.x版本只支持ES5语法,当你项目中的vendor文件里混入了ES6+的语法(比如箭头函数、let、const、模板字符串、解构赋值),老版本的UglifyJS解析到就会直接报错。undefined这个信息虽然看着很懵,但本质是解析器在处理某个语法节点时拿到了未定义结构,无法继续。
排查过程一般是这样:
第一步,先确认报错是不是真的由语法引起。打开报错里对应的vendor.xxxx.js,搜索ES6语法特征。如果发现了箭头函数或const声明,就可以基本确定是UglifyJS版本跟不上语法。
第二步,查看构建工具的压缩配置。如果是Vue CLI 2.x,webpack.prod.conf.js里可能直接配置了new UglifyJsPlugin(...),而且传入的compressor配置里写了warnings: false之类的选项。老插件不认新语法,最快的解法是把压缩插件换成TerserPlugin。
第三步,升级构建工具链。Vue CLI 3.x之后默认使用terser-webpack-plugin做压缩,Terser基于新版解析器,对ES6+支持非常完善,基本不会再出现这类问题。如果你维护的是老项目,直接把uglifyjs-webpack-plugin替换为terser-webpack-plugin是成本最低的迁移方式。
第四步,排查一下splitChunks或CommonsChunkPlugin的配置。有时开发者把异步加载的chunk错误地合并进了vendor,导致vendor里出现动态导入相关的语法,压缩时同样可能爆错。
这条报错和static关键字没有语法层面的关系,但它是“static目录/静态文件”搜索词下的高频问题,遇到的人非常多。如果你的构建链跑在比较新的框架上,大概率不会再遇到,但如果你还在维护两三年前的老项目,这一条值得收藏。
6. static的跨语言对照:Java、C++、Python谁最特别
6.1 C++:static的三重身份
C++里的static可能是三语言中最复杂的,因为它至少有三重身份,而且语义差异极大。
第一重:类外部的static,修饰全局变量或全局函数时,表示这些符号具有内部链接属性,仅在当前编译单元可见。这个用途主要在C语言中延续而来,用来避免多文件编译时的符号冲突。
第二重:类内部的static。静态成员变量属于类,所有实例共享;静态成员函数不接收this指针,只能访问静态成员。这一重和Java的语义最接近,但多了一个“必须在类外定义”的语法要求。
第三重:函数内部的局部static变量。这种变量不会随函数调用结束而销毁,生命周期延长到程序结束,但作用域仍然被限制在函数内部。最经典的使用场景是“只在第一次调用时初始化一次的局部缓存”:
cpp复制void process(int value) {
static int callCount = 0;
callCount++;
// ...
}
这个callCount在每次函数调用时都不会重新初始化,它记录的“函数被调用的总次数”。很多从Java转C++的开发者第一次看到这种写法会疑惑:变量明明定义在函数里,为什么不会被重置?这就是局部静态变量的特点。
6.2 Python:@staticmethod与@classmethod的百年之争
Python没有Java那样的static关键字,它用装饰器实现类似功能,而且分成两个:@staticmethod和@classmethod。
先看一个例子:
python复制class MathUtils:
version = "1.0"
@staticmethod
def add(a, b):
return a + b
@classmethod
def get_version(cls):
return cls.version
@staticmethod修饰的方法本质上就是一个普通函数,只是放在类的命名空间里。它不接收self也不接收cls,不能访问类和实例的任何属性。@classmethod修饰的方法接收cls参数,可以访问类属性、调用其他类方法,而且支持多态——当子类继承并调用get_version时,cls指向的是子类,而父类中的类属性值是父类的版本。
Python里还有一个和Java的static变量行为截然相反的经典坑:类属性。在类体里直接赋值的变量就是类属性,可以通过类名访问:
python复制class Demo:
count = 0
d1 = Demo()
d2 = Demo()
print(Demo.count) # 0
print(d1.count) # 0
d1.count = 99 # 这不是修改类属性,而是给d1创建了实例属性
print(d1.count) # 99
print(Demo.count) # 0
很多初学者在Python里写d1.count = 99,期望它像Java里修改静态变量一样影响所有对象,结果发现只有d1变了。这是因为Python的赋值操作默认在实例命名空间里创建新属性,而不是修改类属性。要修改类属性,必须显式通过类名赋值,或者在@classmethod里给cls.count赋值。
6.3 用一张表理清三种语言的静态语义
把Java、C++、Python的static语义放在一起对比,可以看得很清楚:
| 维度 | Java | C++ | Python |
|---|---|---|---|
| 类变量 | static修饰,类加载时初始化,所有实例共享 |
static成员变量,需类外定义,静态存储期 |
类体内直接赋值,所有实例共享 |
| 类方法 | static修饰,无this,类名调用 |
static成员函数,无this,类名调用 |
@classmethod接收cls,可访问类属性 |
| 纯函数式静态方法 | 普通static方法 | 普通static成员函数 | @staticmethod,不接收self或cls |
| 局部静态变量 | 不支持 | 支持,首次进入函数时初始化 | 不支持 |
| 静态内部类/嵌套类 | static嵌套类不持有外部类引用 | 无直接对应 | 嵌套类默认不持有外部实例引用 |
| 延迟加载 | 由类加载时机决定 | 程序启动即存在 | 模块导入时类定义执行即存在 |
| 修改类变量后是否影响所有实例 | 是 | 是 | 否,实例赋值会创建实例属性遮蔽类属性 |
这张表基本覆盖了三种语言在“静态”这个语义上的主要差异。Java的static最规整,C++的static语义最丰富也最容易被绕晕,Python用装饰器把“类方法”和“静态方法”分开,反而更清晰,但类属性的赋值坑又让它变成了另一种“老朋友”。
说了这么多,最后聊点我自己的习惯。每次面试候选人,我都喜欢问一个看似简单的问题:static变量能不能被垃圾回收?能答上来的人不多。答案是Java里static变量的值对象本质上存储在Class对象中,当类被卸载时(类加载器可回收、类不再被引用),静态变量也会随之释放;但在典型的长驻服务里,类一旦加载基本不会卸载,所以很多人把static当成“永不释放”来用,也就容易埋下内存隐患。落到日常开发里,我个人的建议是:能用实例状态解决的,尽量别用static;static适合存放那些真正与单个实例无关的共享信息。每次动手前多问自己一句——这个数据属于每一个对象,还是属于整个类?想清楚这一点,很多设计上的问题根本不会发生。
