为什么静态变量被认为是有害的?

717

我是一名新手企业Java程序员,最近使用 Groovy 和 Java 开发了一个应用程序。在我的代码中,我使用了相当数量的静态变量。高级技术人员要求我减少使用静态变量。我谷歌了一下,发现很多程序员都相当反对使用静态变量。

我发现静态变量更方便使用。我认为它们也很有效率(如果我错了,请纠正我),因为如果我要在一个类中进行10,000次函数调用,我会很高兴将该方法设为静态,并简单地使用 Class.methodCall(),而不是用10,000个类实例来占用内存,对吗?

此外,静态变量减少了对代码其他部分的互相依赖。它们可以作为完美的状态持有者。另外,我发现一些语言(如SmalltalkScala)广泛使用静态变量。那么,为什么程序员普遍反对静态变量(特别是在Java世界中)呢?

PS:如果我对静态变量的假设有误,请纠正我。


47
仅供参考,Smalltalk和Scala中没有静态变量或方法,这是因为静态方法和变量违背了面向对象编程的原则。 - Maurício Linhares
93
你提出的至少一个声明很有趣:“静态方法减少了代码中其他部分之间的相互依赖关系”。通常它们加强了依赖关系。调用所在的代码与被调用的代码之间非常紧密地绑定,没有抽象层,是直接依赖关系。 - Arne Deutsch
28
第二段讨论的是一个完全不同的主题,即静态方法。 - Paul
11
函数式编程也不赞成使用全局状态。如果你将来涉足函数式编程(建议这样做),要准备放弃全局状态的概念。 - new123456
8
如果你的方法可以是静态的,那就意味着它不依赖于状态。如果它不依赖于状态,为什么需要创建10000个对象?为什么不在同一个对象上调用它10000次?除非你是指从10000个不同的位置调用它,否则显然需要重构你的代码,以免让类的实例化过多导致内存混乱。 - Kshitiz Sharma
显示剩余14条评论
31个回答

779

静态变量代表全局状态。这很难推理和测试:如果我创建一个对象的新实例,我可以在测试中推理出它的新状态。如果我使用使用静态变量的代码,那么它可能处于任何状态 - 任何事都可能修改它。

我可以继续讲很长一段时间,但更大的概念是要考虑范围越小,推理就越容易。我们善于思考小事情,但是如果没有模块化,很难推理出一百万行系统的状态。顺便说一下,这适用于各种各样的事情 - 不仅仅是静态变量。


66
最近似乎有一个争论,即代码是否可测试。这是一个相当有缺陷的推理。该论点应该是“好的设计”,而通常好的设计是可测试的。但并不是相反的:“我无法测试它,因此它一定是糟糕的设计。”不过,不要误解我的意思,总的来说,我同意你的帖子。 - M Platvoet
163
如果面对两种本质上同样有效的设计,我会说可测试性更优秀。可测试性不一定等同于良好设计,但我很少遇到不可测试的好设计,而且我认为它们非常罕见,因此我毫不犹豫地认为可测试性是指向良好设计的通用有益指标。 - Jon Skeet
10
测试可行性对可维护性和可靠性都有影响,我认为它们是设计质量中的重要因素。当然,它们不是唯一的因素,但在我看来,任何给定代码的成本都是机器周期、开发者周期和用户周期的组合。测试可行性涉及其中的两个因素。 - Justin Morgan
5
可测试性通常也会影响可重用性,因为解耦的类一般更容易被重复使用。 - TrueWill
22
我不同意你在这里的第一个评论。我认为,如果某个东西无法被测试,那么它就是糟糕的设计;因为如果我不能测试它,我就无法知道它是否有效。如果销售人员告诉你:“这款车的设计使其无法进行测试,所以我不知道它是否能够运行”,你会购买这辆车吗?对于软件(以及汽车),可测试性非常重要,因此,有能力的设计必须包含它。 - Dawood ibn Kareem
显示剩余14条评论

307

它不是非常面向对象: 有些人认为静态方法是"邪恶的"原因之一是它们与面向对象编程相悖。特别是,它违反了数据封装在对象中(可以扩展、信息隐藏等)的原则。静态方法,以你描述的方式使用它们,本质上是将它们用作全局变量,以避免处理范围等问题。然而,全局变量是过程化或命令式编程范例的定义特征之一,而不是"好的"面向对象代码的特征。这并不是说过程化范例是不好的,但我觉得你的主管希望你编写"好的面向对象代码",而你真正想编写的是"好的过程化代码"。

在Java中使用静态方法时,有许多需要注意的地方,这些地方并不总是显而易见。例如,如果您在同一VM中运行两个程序副本,它们是否会共享静态变量的值并干扰彼此的状态?或者当您扩展类时会发生什么,您可以覆盖静态成员吗?您的VM是否由于具有疯狂数量的静态变量而耗尽内存,而该内存无法回收用于其他所需的实例对象?

此外,静态变量的生命周期与程序的整个运行时间相匹配。这意味着,即使你使用完你的类,所有这些静态变量的内存也不能被垃圾回收。例如,如果你将变量设置为非静态,并在main()函数中创建一个类的单个实例,并要求该类执行特定函数10,000次,那么一旦这10,000次调用完成并删除对单个实例的引用,所有静态变量都可以被垃圾回收和重用。
此外,静态方法无法用于实现接口,因此静态方法可能会阻止某些面向对象功能的可用性。
如果效率是您的主要关注点,可能有其他更好的方法来解决速度问题,而不仅仅考虑调用通常比创建快的优势。考虑是否需要transient或volatile修饰符。为了保留内联的能力,方法可以标记为final而不是static。方法参数和其他变量可以标记为final,以允许基于对这些变量可能发生的更改的假设进行某些编译器优化。一个实例对象可以多次重复使用,而不是每次都创建一个新实例。可能应该打开编译器优化开关以提高应用程序的性能。也许,设计应该设置为可以多线程运行10,000次,并利用多处理器核心。如果可移植性不是问题,也许本地方法会比静态方法更快。
如果您不想要对象的多个副本,单例设计模式相对于静态对象具有优势,例如线程安全(假定您的单例编码良好),允许延迟初始化,在使用对象时保证对象已经正确初始化,子类化,测试和重构代码的优势,更不用说,如果您改变了只想要一个对象实例的想法,删除防止重复实例的代码要比重构所有静态变量代码以使用实例变量容易得多。我以前就做过这样的事情,不好玩,你最终不得不编辑更多的类,这会增加引入新错误的风险...所以最好第一次设置正确,即使它似乎有其缺点。对我来说,如果您决定将来需要多个副本,则需要重新工作可能是尽可能少使用静态变量的最有力的原因之一。因此,我也不同意您的声明,即静态变量减少了相互依赖性,我认为如果您有许多可以直接访问的静态变量,而不是“知道如何在自身上执行某些操作”的对象,您最终会得到更耦合的代码。

11
我喜欢你的回答,我认为它侧重考虑静态方面的正确权衡,而不是一些像并发和范围之类的转移话题。对于单例模式的支持,再加上一分赞同。实际上,更好的问题可能是何时使用静态变量/方法而不是单例模式...... - studgeek
2
即使单例本身可能是线程安全的(例如使用synchronized方法),这并不意味着调用代码在单例状态方面没有竞态条件。 - André Caron
8
此外,静态方法并不违背面向对象编程范式。很多面向对象编程的狂热者会告诉你,类是一个对象,而静态方法是该类对象的方法,而不是它的实例。这种现象在Java中不那么常见。其他语言,比如Python允许你将类用作变量,并且你可以将静态方法作为该对象的方法来访问。 - André Caron
4
如果我没记错的话,第三段的最后一行应该写成“所有非静态变量”,请您确认一下。 - Steve
2
"对象生命周期",是@jessica提到的一个非常重要的点。 - Abhidemon
显示剩余4条评论

103

邪恶是一个主观的术语。

在创建和销毁方面,您无法控制静态变量。它们存在于程序加载和卸载的支配下。

由于静态变量存在于一个空间中,所有希望使用它们的线程都必须经过您管理的访问控制。这意味着程序更加耦合,而且这种变化很难预见和管理(就像J Skeet所说)。这会导致隔离变化影响的问题,从而影响测试的管理。

这是我对它们的两个主要问题。


64

不,全局状态本身并不是邪恶的。但我们需要查看 你的 代码以确定你是否正确使用它。新手滥用全局状态的可能性很大,就像他会滥用每个语言特性一样。

全局状态是绝对必要的。我们无法避免全局状态,也无法避免思考全局状态——如果我们在意理解我们应用程序的语义。

试图为了摆脱全局状态而摆脱它的人最终会得到一个更加复杂的系统——而全局状态仍然存在,在许多间接层次的巧妙/愚蠢的伪装下;在解开所有间接层次后,我们仍然需要考虑全局状态。

就像 Spring 的人们会在 xml 中慷慨地声明全局状态,并认为这样做非常优越。

@Jon Skeet,如果我创建一个对象的新实例,现在你有两件事情要考虑——对象内部的状态和托管对象的环境的状态。


10
我有两件事要思考。但这不是只依赖于对象状态的测试。拥有较少全局状态会使这更容易。 - DJClayworth
3
依赖注入与全局状态或全局可见性无关,即使容器本身也不是全局的。与“普通”代码相比,容器管理的对象只能被容器本身看到。实际上,依赖注入非常常用于避免单例模式。 - Floegipoky

37
如果您在不使用“final”关键字的情况下使用“static”关键字,则应该仔细考虑设计。即使有“final”的存在,可变的静态final对象也可能同样危险。
我估计,在使用“static”而没有“final”的情况下,错误的概率约为85%。通常,我会发现奇怪的解决方法来掩盖或隐藏这些问题。
请勿创建静态可变对象,尤其是集合。一般而言,集合应在其包含对象初始化时初始化,并且应设计成在其包含对象被遗忘时重置或遗忘。
使用静态可能会导致非常微妙的错误,这些错误会使维护工程师痛苦数天。我知道,因为我曾经制造并追踪过这些错误。
如果需要更多详细信息,请继续阅读...
为什么不使用静态?
静态存在许多问题,包括编写和执行测试以及不明显的微妙错误。
依赖于静态对象的代码无法轻松地进行单元测试,并且通常无法轻松地模拟静态对象。
如果使用静态,则无法交换类的实现以测试更高级别的组件。例如,想象一个静态的CustomerDAO,它返回从数据库加载的Customer对象。现在我有一个需要访问某些客户对象的CustomerFilter类。如果CustomerDAO是静态的,则无法编写CustomerFilter的测试,而不先初始化数据库并填充有用信息。
而且,数据库的填充和初始化需要很长时间。根据我的经验,您的DB初始化框架会随着时间的推移而改变,这意味着数据将发生变化,并且测试可能会失败。即,想象一下Customer 1曾经是VIP,但DB初始化框架已更改,现在Customer 1不再是VIP,但是您的测试已被硬编码为加载Customer 1...
更好的方法是实例化一个CustomerDAO,并在构造CustomerFilter时将其传递进去。(甚至更好的方法是使用Spring或另一个IoC框架。
一旦这样做,您可以快速模拟或存根CustomerFilterTest中的备用DAO,从而允许您更多地控制测试,
没有静态DAO,测试将更快(没有DB初始化),并且更可靠(因为它不会在DB初始化代码更改时失败)。例如,在这种情况下,确保Customer 1是并始终是VIP,就测试而言。当运行单元测试套件(例如在持续集成服务器上)时,静态变量会导致实际问题。想象一下,有一个网络Socket对象的静态映射,在一个测试与另一个测试之间仍然保持开放状态。第一个测试可能在端口8080上打开一个Socket,但是当测试被销毁时,您忘记清除Map。现在,当第二个测试启动时,它很可能会崩溃,因为它尝试为端口8080创建新的Socket,而该端口仍然被占用。另外,想象一下,当不删除静态集合中的Socket引用时(除了WeakHashMap之外),它们永远无法被垃圾回收,从而导致内存泄漏。
这只是一个过度概括的例子,但在大型系统中,这个问题一直存在。人们不会考虑单元测试在同一个JVM中反复启动和停止他们的软件,但这是对软件设计的好测试,如果您有高可用性的愿望,那么这是您需要注意的事情。
这些问题通常出现在框架对象中,例如您的DB访问、缓存、消息传递和日志记录层。如果您使用Java EE或某些最佳框架,则它们可能会为您管理大部分内容,但如果您像我一样在处理遗留系统,则可能有许多自定义框架来访问这些层。
如果适用于这些框架组件的系统配置在单元测试之间发生更改,并且单元测试框架不会拆除和重建这些组件,则这些更改将无法生效,当测试依赖于这些更改时,它们将失败。
即使非框架组件也会受到此问题的影响。想象一个名为OpenOrders的静态映射。您编写了一个测试,创建了几个打开的订单,并检查它们是否都处于正确的状态,然后测试结束。另一位开发人员编写了第二个测试,将其需要的订单放入OpenOrders映射中,然后断言订单数量准确。分别运行这些测试,都会通过,但是在套件中同时运行时,它们将失败。
更糟糕的是,失败可能基于测试运行的顺序。
在这种情况下,通过避免使用静态变量,可以避免跨测试实例保持数据,从而确保更好的测试可靠性。
如果您在高可用性环境中工作,或者任何线程可能启动和停止的地方,则单元测试套件中提到的相同问题也可能适用于您的代码在生产中运行时。
在处理线程时,与其使用静态对象来存储数据,不如在线程的启动阶段初始化一个对象。这样,每次启动线程时,都会创建一个新的对象实例(具有潜在的新配置),从而避免了数据从一个线程实例流入下一个线程实例的情况。当一个线程结束时,静态对象不会被重置或垃圾回收。假设有一个名为“EmailCustomers”的线程,在启动时使用一个静态字符串集合填充电子邮件地址列表,然后开始向每个地址发送电子邮件。假设该线程被某种方式中断或取消,因此您的高可用性框架重新启动该线程。那么当线程启动时,它会重新加载客户列表。但是,由于集合是静态的,它可能会保留上一个集合中的电子邮件地址列表。现在,一些客户可能会收到重复的电子邮件。
附注:Static Final
使用“static final”实际上相当于Java中的C #define,尽管有技术实现差异。 C/C++ #define 在编译之前被预处理器交换出代码。 Java的“static final”将最终停留在JVM类内存中,使其(通常)在RAM中永久存在。在这方面,它更类似于C ++中的“static const”变量,而不是# define。
总结
我希望这有助于解释为什么静态问题很常见。如果您正在使用现代Java框架,如Java EE或Spring等,则可能不会遇到许多这些情况,但如果您正在使用大量的旧代码,则可能会更频繁地遇到这些问题。

Java中的“static final”会在堆栈上保留内存吗?我认为静态变量存储在方法区。 - wlnirvana
@wlnirvana - 我的回答已经有10年了,但是很好的观点 - 正如在这里指出的那样,静态变量存储在类内存中:https://dev59.com/smw15IYBdhLWcg3wfr9y - JBCP
我的观点是它被分配在更“永久”的内存中,通常在应用程序本身的控制范围之外(除非您正在使用OSGi或其他动态类加载)。 - JBCP

34

静态变量有两个主要问题:

  • 线程安全 - 静态资源本质上不是线程安全的
  • 代码隐式 - 您不知道静态变量何时实例化以及它是否会在其他静态变量之前实例化

13
我不理解线程安全这个点,我认为除非你采取措施,否则没有什么是线程安全的。这似乎与静态内容没有关系,如果我漏掉了什么,请纠正我。 - Zmaster
1
@Zmaster - 虽然线程安全不仅限于静态变量,但由于它们的定义是从不同的上下文中调用和调用它们,因此更容易受到影响。 - sternr
2
@sternr 我理解你的意思,即使“不同的上下文”并不一定等同于“不同的线程”。但是确实需要经常考虑静态资源的线程安全性。你应该考虑澄清这个句子。 - Zmaster
有一些有效的线程安全使用静态资源的例子,例如:private static final Logger LOG = Logger.getLogger(Foo.class); private static final AtomicInteger x = new AtomicInteger(0);据我所知,像这样的静态资源分配是由类加载器保证线程安全的。Logger实例是否线程安全与您分配指针的位置无关。在静态变量中保持状态可能不是一个好主意,但没有理由它不应该是线程安全的。 - teknopaul

21

总结一下在Java中使用静态方法的几个基本优缺点:

优点:

  1. 全局可访问,即不与任何特定对象实例绑定。
  2. JVM中每个实例只有一个。
  3. 可以通过使用类名(无需对象)来访问。
  4. 包含适用于所有实例的单个值。
  5. 在JVM启动时加载并在JVM关闭时停止。
  6. 它们不修改对象的状态。

缺点:

  1. 静态成员始终是内存的一部分,无论它们是否正在使用。
  2. 您无法控制静态变量的创建和销毁。通常它们在程序加载时被创建,在程序卸载时(或JVM关闭时)被销毁。
  3. 您可以使用同步使静态变量线程安全,但需要做出额外的努力。
  4. 如果一个线程更改静态变量的值,可能会破坏其他线程的功能。
  5. 在使用之前必须了解“静态”。
  6. 无法覆盖静态方法。
  7. 序列化与其不兼容。
  8. 它们不参与运行时多态性。
  9. 如果使用大量静态变量/方法,会存在内存问题(在某种程度上,但我猜不会太多)。因为它们在程序结束之前不会被垃圾收集。
  10. 静态方法也很难测试。

6、7、8和10的缺点是所使用的语言/框架的缺点,而不是静态变量一般的缺点。1、4和5的缺点也存在于其他解决方案中,比如某些框架提供的单例模式。(我没有投票支持这个答案,因为我同意其余部分并且这是一个很好的集合。) - peterh
@peterh:缺点#7对于静态字段来说是根本性的。如果将静态字段作为对象的一部分进行序列化,那么当代码尝试反序列化具有不同指定值的两个实例时,没有明智的处理方式。 - supercat

17

静态变量通常被认为是不好的,因为它们代表全局状态,因此更难理解。特别是,它们打破了面向对象编程的假设。在面向对象编程中,每个对象都有自己的状态,由实例(非静态)变量表示。静态变量表示跨实例的状态,这可能更难进行单元测试。主要是因为很难将对静态变量的更改隔离到一个单独的测试中。

话虽如此,重要的是要区分常规静态变量(通常被认为是不好的)和final静态变量(也称为常量;不那么糟糕)之间的区别。


4
“静态变量代表类之间的状态”,我认为你想说的是“静态变量代表实例之间的状态”?对于“final static,也称为常量,不错”的评论点个赞。由于该值无法更改,任何依赖它的东西在某一时刻的行为都不会隐式地在以后的时间改变——该值始终相同。 - Jared Updike
“静态变量代表跨实例的状态”,这是更好的陈述方式。我已经编辑了我的回答。 - Jack Edmonds

15

由于没有人提到它:并发性。如果您有多个线程读写静态变量,那么静态变量可能会让您感到意外。这在 Web 应用程序(例如 ASP.NET)中很常见,它可能会导致一些非常令人疯狂的错误。例如,如果您有一个由页面更新的静态变量,并且该页面被“几乎同时”请求了两次,其中一位用户可能会得到另一位用户期望的结果,甚至更糟。

静态变量可以减少与代码其他部分的相互依赖关系。它们可以作为完美的状态保持器

我希望您准备好使用锁并处理争用。

*实际上,Preet Sangha提到过它。


5
实例变量相对于静态变量来说没有线程安全的优势,它们都是无保护的变量。重要的是要考虑如何保护访问这些变量的代码。 - studgeek
2
我并没有完全做出这样的声明,但为了讨论起见:分离是一种保护形式。线程状态被分离;全局状态则不是。实例变量不需要保护,除非明确在线程之间共享;静态变量始终由进程中所有线程共享。 - Justin M. Keyes
我希望线程静态变量更像一流的概念,因为它们可以非常有用地安全地使信息可用于包装子例程调用,而无需通过每个包装层传递该信息。例如,如果一个对象有将其呈现到线程当前图形上下文的方法,并且有保存/恢复当前图形上下文的方法,则使用这些方法通常比通过每个方法调用传递图形上下文更清晰。 - supercat

13
如果我需要在一个类中调用函数达到一万次,我宁愿将该方法声明为静态的,并使用简单的class.methodCall()来调用它,而不是通过创建一万个类实例来浪费内存空间,对吗?
您需要平衡将数据封装成具有状态的对象与仅计算一些数据的函数结果之间的需求。
此外,静态成员可以减少对代码其他部分的相互依赖性。
但是,封装也可以达到这一目的。在大型应用程序中,静态成员往往会产生混乱的代码,并且难以进行重构或测试。
其他答案还提供了反对过度使用静态成员的好理由。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接