为什么静态变量被视为邪恶?

Vam*_*ani 606 java static

我是一名Java程序员,对企业界不熟悉.最近我使用Groovy和Java 开发了一个应用程序.我编写的所有代码都使用了相当多的静态代码.高级技术部门要求我减少使用的静力学数量.我用Google搜索了同样的东西,我发现很多程序员都反对使用静态变量.

我发现静态变量使用起来更方便.而且我认为它们也是有效的(如果我错了,请纠正我),因为如果我必须对一个类中的函数进行10,000次调用,我很乐意将该方法设置为静态并使用简单的方法Class.methodCall()而不是用10,000个类的实例来混乱内存,对吧?

此外,静态减少了代码其他部分的相互依赖性.他们可以充当完美的国家持有者.除此之外,我发现静态在一些语言中得到广泛实现,如SmalltalkScala.那么为什么程序员(尤其是Java世界)中普遍存在对静态的压迫呢?

PS:如果我对静力学的假设是错误的,请纠正我.

Jon*_*eet 672

静态变量代表全局状态.这很难推理并且很难测试:如果我创建一个对象的新实例,我可以在测试中推断它的新状态.如果我使用使用静态变量的代码,它可以处于任何状态 - 任何东西都可以修改它.

我可以坚持一段时间,但更大的想法是,事物的范围越紧,理由就越容易.我们擅长思考小事情,但如果没有模块化,就很难推断百万线路系统的状态.顺便说一句,这适用于各种事物 - 不仅仅是静态变量.

  • @M Platvoet:我想说,如果在两个同样有效的设计之间做出选择,那么可测试的设计是优越的.可测试当然不等于精心设计,但我很少遇到不可测试的优秀设计,我认为它们非常罕见,我可以将可测试性作为一般用途*提供良好设计的指标. (144认同)
  • 最近这似乎是一个争论,无论代码是否可测试.这是一个相当有缺陷的推理.论证应该是"好的设计",通常好的设计是可测试的.但不是相反:"我无法测试它,因为它一定是糟糕的设计." 不要误解我的意思,我同意你的帖子. (57认同)
  • M Platvoet - 我不同意你的第一条评论.我认为如果某些东西无法测试,那么它就是糟糕的设计; 因为如果我不能测试它,我不知道它是否有效.如果销售人员告诉你"这个型号的设计是否会阻止它进行测试,那么我会不会买一辆车?所以我不知道它是否实际运行"?可测试性对软件(以及汽车)至关重要,有能力的设计要求包含在内. (12认同)
  • @M Platvoet - 可测试性会影响可维护性和可靠性,我会考虑设计质量的主要因素.当然,它们不是唯一的因素,但恕我直言,任何给定代码的成本都是机器周期,开发人员周期和用户周期的组合.可测试性达到了这三个中的两个. (9认同)
  • @M Platvoet - 可测试性也会影响可重用性,因为解耦类通常更容易重用. (5认同)

Jes*_*own 263

它不是非常面向对象: 一些人认为静态可能被认为是"邪恶"的一个原因是它们违背了面向对象的范式.特别是,它违反了数据被封装在对象中的原则(可以扩展,信息隐藏等).在描述使用它们的方式中,静态主要是将它们用作全局变量,以避免处理范围等问题.然而,全局变量是程序或命令式编程范例的定义特征之一,而不是"好的"面向对象代码的特征.这并不是说程序范例很糟糕,但我得到的印象是你的主管希望你写出"好的面向对象的代码"而你真的想写出"好的程序代码".

当你开始使用并不总是显而易见的静态时,Java中有许多gotchyas.例如,如果您在同一个VM中运行了两个程序副本,它们是否会破坏静态变量的值并混淆彼此的状态?或者当你扩展类时会发生什么,你可以覆盖静态成员吗​​?您的虚拟机是否内存不足,因为您有大量的静态数据,并且无法为其他所需的实例对象回收内存?

对象生存期: 此外,静态的生命周期与程序的整个运行时匹配.这意味着,即使你完成了使用你的类,所有这些静态变量的内存也不能被垃圾收集.例如,如果您将变量设置为非静态,并且在main()函数中,您创建了类的单个实例,然后要求您的类执行10,000次特定函数,一旦完成10,000次调用,并删除对单个实例的引用,可以对所有静态变量进行垃圾回收和重用.

防止某些重用: 此外,静态方法不能用于实现接口,因此静态方法可以防止某些面向对象的功能可用.

其他选项: 如果效率是您的主要关注点,那么可能有其他更好的方法来解决速度问题,而不仅仅考虑调用的优势通常比创建更快.考虑是否需要瞬态或挥发性修饰符.为了保留内联的能力,可以将方法标记为final而不是static.方法参数和其他变量可以标记为final,以允许某些编译器优化基于对可以更改这些变量的内容的假设.实例对象可以多次重用,而不是每次都创建一个新实例.可能存在应该为应用程序打开的compliler优化开关.也许,应该设置设计,以便10,000次运行可以是多线程的,并利用多处理器内核.如果不考虑可移植性,那么本机方法可能会比静态方法更快.

如果由于某种原因你不想要一个对象的多个副本,那么单例设计模式比静态对象有优势,比如线程安全(假设你的单例编码好),允许延迟初始化,保证对象已经正确在使用时初始化,子类化,测试和重构代码的优势,更不用说,如果在某些时候你改变主意只想要一个对象的一个​​实例,那么删除代码以防止重复实例就更容易了而不是重构所有的静态变量代码来使用实例变量.我以前必须这样做,它不是很有趣,你最终还是要编辑更多的课程,这会增加你引入新bug的风险......更好的是第一次设置"正确",即使看起来它有它的缺点.对我来说,如果你需要重新工作,你需要多次复制某些东西,这可能是尽可能不经常使用静力学的最有说服力的理由之一.因此我也不同意你的说法,即静态减少了相互依赖性,我认为如果你有很多可以直接访问的静态,你会得到更多耦合的代码,而不是一个"知道怎么做"的对象一些"本身.

  • +1用于解释静态变量的"对象生存期". (11认同)
  • 我喜欢你的答案,我认为它侧重于权利权衡以考虑静态而不是一些红色鲱鱼,如并发性和范围.对于单身人士+1,一个更好的问题可能是什么时候使用静态变量/方法与单身人士...... (10认同)
  • 而且,静态不是针对OOP范式的.很多OOP狂热分子会告诉你这个类是一个对象,静态方法是类对象的一个​​方法,而不是它的实例.这种现象在Java中不太常见.其他语言(如Python)允许您将类用作变量,您可以访问静态方法作为该对象的方法. (8认同)
  • 如果我没有弄错的话,第三段的最后一行应该是_all****非静态**变量_. (4认同)
  • 即使单例本身可能是线程安全的(例如,通过使用`synchronized`方法),也不意味着调用代码相对于单例状态没有竞争条件。 (2认同)
  • “对象生命周期”,是 @jessica 提到的非常重要的一点。 (2认同)

Pre*_*gha 93

邪恶是一个主观的术语.

您无法在创建和销毁方面控制静态.他们生活在程序装载和卸载的要求下.

由于静态存在于一个空间中,所有希望使用它们的线程必须通过您必须管理的访问控制.这意味着程序更加耦合,这种变化更难以设想和管理(如J Skeet所说).这导致隔离变更影响的问题,从而影响测试的管理方式.

这是我与他们的两个主要问题.

  • 好答案.我想你已经强调了两个要点. (4认同)

irr*_*ble 59

不,全球各州本身并不邪恶.但我们必须查看您的代码,看看您是否正确使用它.新手很可能滥用全球各州; 就像他会滥用所有语言功能一样.

全球国家是绝对必要的.我们无法回避全球各州.我们无法避免对全球国家的推理. - 如果我们想了解我们的应用程序语义.

那些试图摆脱全球国家的人,不可避免地最终会得到一个更加复杂的系统 - 全球各州仍然存在,在许多层面的间接下巧妙地/白痴地伪装; 在揭开所有的背景之后,我们仍然需要对全球国家进行推理.

就像春天的人们在xml中大肆宣布全球状态一样,并且认为它在某种程度上更胜一筹.

@Jon Skeet if I create a new instance of an object现在你有两件事要理由 - 对象中的状态,以及托管对象的环境的状态.

  • "我有两件事要推理".如果我只依赖于对象状态进行测试,那就不行了.哪个更容易,我所拥有的全球状态就越少. (10认同)
  • 依赖注入与全局状态或全局可见性无关,即使容器本身不是全局的。与“常规”代码相比,容器管理的对象可见的唯一额外的事情是容器本身。实际上,DI非常常用于避免单例模式。 (2认同)

ste*_*rnr 31

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

  • 线程安全 - 根据定义,静态资源不是线程安全的
  • Code Implicity - 您不知道何时实例化静态变量以及是否将在另一个静态变量之前实例化它

  • 我没有得到线程安全点,我认为没有什么是线程安全的,除非你这样做.这似乎与静态事物无关,如果我遗漏了什么,请纠正我. (11认同)
  • @sternr我理解你的意思,如果"不同的上下文"不一定等于"不同的线程"那么事件.但是,静态资源通常需要考虑线程安全需求.你应该考虑澄清这句话. (2认同)

JBC*_*BCP 27

如果您使用的是'static'关键字而没有'final'关键字,那么这应该是一个仔细考虑您的设计的信号.即使是"最终"的存在也不是自由传递,因为可变的静态最终对象可能同样危险.

我估计大约85%的时间我看到没有'最终'的'静态',这是错误的.通常,我会找到掩盖或隐藏这些问题的奇怪的解决方法.

请不要创建静态变量.尤其是收藏品.通常,Collections应该在初始化其包含对象时进行初始化,并且应该进行设计,以便在忘记包含对象时重置或忘记它们.

使用静力学可以创建非常微妙的错误,这将导致持续工程师痛苦的日子.我知道,因为我创造并捕获了这些错误.

如果您想了解更多详情,请继续阅读......

为什么不使用静力学?

静态有许多问题,包括编写和执行测试,以及不是很明显的微妙错误.

依赖于静态对象的代码不能轻易地进行单元测试,并且(通常)不能轻易地模拟静态.

如果使用静态,则无法将类的实现交换出来以测试更高级别的组件.例如,假设一个静态CustomerDAO返回它从数据库加载的Customer对象.现在我有一个CustomerFilter类,需要访问一些Customer对象.如果CustomerDAO是静态的,那么在没有首先初始化我的数据库并填充有用信息的情况下,我无法为CustomerFilter编写测试.

数据库填充和初始化需要很长时间.根据我的经验,您的数据库初始化框​​架将随着时间的推移而发生变化,这意味着数据会变形,测试可能会中断 IE,假设客户1曾经是VIP,但数据库初始化框​​架已更改,现在客户1不再是VIP,但您的测试是硬编码加载客户1 ...

更好的方法是实例化CustomerDAO,并在构建CustomerFilter时将其传递给CustomerFilter.(更好的方法是使用Spring或另一个Inversion of Control框架.

完成此操作后,您可以在CustomerFilterTest中快速模拟或删除备用DAO,从而可以更好地控制测试,

如果没有静态DAO,测试将更快(无数据库初始化)并且更可靠(因为当db初始化代码更改时它不会失败).例如,在这种情况下,就测试而言,确保客户1始终是VIP.

执行测试

在一起运行单元测试套件时(例如,使用Continuous Integration服务器),静态会导致真正的问题.想象一下网络套接字对象的静态映射,它从一个测试到另一个测试保持打开状态.第一个测试可能会在端口8080上打开一个Socket,但是当测试被拆除时你忘了清除Map.现在,当第二次测试启动时,它可能会在尝试为端口8080创建新的Socket时崩溃,因为端口仍然处于占用状态.想象一下,静态Collection中的Socket引用不会被删除,并且(WeakHashMap除外)永远不会被垃圾收集,导致内存泄漏.

这是一个过于概括的例子,但在大型系统中,这个问题一直在发生.人们不会想到在同一个JVM中重复启动和停止软件的单元测试,但它是对软件设计的一个很好的测试,如果你对高可用性抱有期望,那么你需要注意这一点.

这些问题通常出现在框架对象中,例如,数据库访问,缓存,消息传递和日志记录层.如果你正在使用Java EE或一些最好的框架,他们可能会为你管理很多这样的东西,但是如果像我一样你正在处理遗留系统,你可能有很多自定义框架来访问这些层.

如果适用于这些框架组件的系统配置在单元测试之间发生更改,并且单元测试框架没有拆除并重建组件,则这些更改将无法生效,并且当测试依赖于这些更改时,它们将失败.

即使是非框架组件也会遇到这个问题.想象一下名为OpenOrders的静态地图.您编写了一个创建一些打开订单的测试,并检查以确保它们都处于正确的状态,然后测试结束.另一个开发人员编写第二个测试,将所需的订单放入OpenOrders映射,然后断言订单的数量是准确的.单独运行,这些测试都会通过,但是当它们在套件中一起运行时,它们将会失败.

更糟糕的是,失败可能基于测试运行的顺序.

在这种情况下,通过避免静态,可以避免跨测试实例持久保存数据的风险,从而确保更好的测试可靠性.

微妙的错误

如果您在高可用性环境中工作,或者在线程可能启动和停止的任何地方工作,那么当您的代码也在生产中运行时,上面提到的与单元测试套件相同的问题也适用.

处理线程时,不是使用静态对象来存储数据,最好使用在线程启动阶段初始化的对象.这样,每次启动线程时,都会创建一个对象的新实例(具有可能的新配置),并且可以避免来自该线程的一个实例的数据流入下一个实例.

线程死亡时,静态对象不会被重置或垃圾回收.想象一下,你有一个名为"EmailCustomers"的线程,当它启动时,它会填充一个带有电子邮件地址列表的静态String集合,然后开始通过电子邮件发送每个地址.让我们说线程被中断或以某种方式取消,因此您的高可用性框架重新启动线程.然后当线程启动时,它会重新加载客户列表.但由于该集合是静态的,因此它可能会保留上一个集合中的电子邮件地址列表.现在有些客户可能会收到重复的电子邮件

旁白:静态决赛

尽管存在技术实现上的差异,但使用"静态最终"实际上是Java等效的C#define.在编译之前,预处理器将AC/C++ #define换出代码.Java"静态final"将结束驻留在堆栈上的内存.这样,它更像C++中的"static const"变量而不是#define.

摘要

我希望这有助于解释为什么静力学有问题的一些基本原因.如果您使用的是Java EE或Spring等现代Java框架,您可能不会遇到很多这样的情况,但如果您正在使用大量遗留代码,它们可能会变得更加频繁.


Jus*_*yes 15

因为没有人提到它:并发.如果您有多个线程读取和写入静态变量,静态变量会让您感到惊讶.这在Web应用程序(例如,ASP.NET)中很常见,并且可能会导致一些令人抓狂的错误.例如,如果您有一个由页面更新的静态变量,并且该页面是由"几乎同时"的两个人请求的,则一个用户可能会得到另一个用户期望的结果,或者更糟.

静态减少了代码其他部分的相互依赖性.他们可以充当完美的国家持有者

我希望你准备好使用锁并处理争用.

*实际上,Preet Sangha提到了它.

  • 与静态相比,实例变量没有线程安全优势,它们都是不受保护的变量.相反,这一切都归结为如何保护访问这些变量的代码. (5认同)
  • 我没有提出这个主张,但为了讨论起见:分离是一种保护形式.线程状态是分开的; 全球状态是*不*.除非在线程之间明确共享,否则实例变量不需要*保护; 静态变量*始终由进程中的所有线程共享. (2认同)

Vir*_*ual 14

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

好处:

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

缺点:

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


Jér*_*nge 13

如果我必须对类中的函数进行10,000次调用,我很乐意将该方法设置为静态并在其上使用简单的class.methodCall()而不是使用10,000个类的实例来混乱内存,对吗?

您必须平衡将数据封装到具有状态的对象的需求,而不是简单地计算某些数据上的函数结果的需要.

此外,静态减少了代码其他部分的相互依赖性.

封装也是如此.在大型应用程序中,静态通常会生成意大利面条代码,并且不容易进行重构或测试.

其他答案也提供了反对过度使用静力学的充分理由.


Jac*_*nds 12

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

话虽这么说,重要的是要区分常规静态变量(通常认为是坏的)和最终的静态变量(AKA常量;不是那么糟糕).

  • "静态变量表示跨类的状态"......我认为你的意思是"静态变量表示跨实例的状态"?+1"最终静态AKA常数,不是那么糟糕".由于该值不能更改,因此在某个时间点依赖于该值的任何内容都不能在以后隐式更改其行为 - 该值是相同的. (4认同)

M P*_*oet 9

在我看来,它几乎不是关于性能,而是关于设计.我不认为静态方法的使用错误,因为使用了静态变量(但我猜你实际上是在谈论方法调用).

它只是关于如何隔离逻辑并给它一个好地方.有时,证明使用静态方法java.lang.Math是一个很好的例子.我想当你为大多数课程命名时,XxxUtil或者Xxxhelper你最好重新考虑你的设计.

  • 纯净的副作用自由静态方法是完美的IMO.但全球可变状态很少,我将OP解释为全球状态. (3认同)

i_a*_*ero 8

我刚刚总结了答案中提出的一些观点.如果您发现任何错误,请随时更正.

缩放:我们每个JVM只有一个静态变量实例.假设我们正在开发一个图书馆管理系统,我们决定将图书名称作为静态变量,因为每本书只有一个.但是,如果系统增长并且我们使用多个JVM,那么我们无法找出我们正在处理哪本书?

线程安全:在多线程环境中使用时,需要控制实例变量和静态变量.但是在实例变量的情况下,它不需要保护,除非它在线程之间显式共享,但是在静态变量的情况下,它总是由进程中的所有线程共享.

测试:虽然可测试设计不等于好的设计,但我们很少会观察到一个不可测试的好设计.由于静态变量代表全局状态,因此测试它们非常困难.

推理状态:如果我创建一个类的新实例,然后我们就可以推论这个实例的状态,但如果是有静态变量它,然后它可以在任何状态.为什么?因为静态变量可能已被某个不同的实例修改,因为静态变量是跨实例共享的.

序列化:序列化也不适用于它们.

创建和销毁:无法控制静态变量的创建和销毁.通常它们是在程序加载和卸载时创建和销毁的.这意味着它们对内存管理不利,并且还会在启动时累计初始化时间.

但是,如果我们真的需要它们呢?

但有时我们可能真的需要它们.如果我们真的觉得需要在应用程序中共享许多静态变量,那么一个选项就是使用具有所有这些变量的Singleton Design模式.或者我们可以创建一些具有这些静态变量的对象,并且可以传递.

此外,如果静态变量标记为final,则它变为常量,并且一旦分配给它,则无法更改.这意味着它将使我们免于因其可变性而面临的所有问题.


Anu*_*tel 6

静态变量最重要的是会产生数据安全性问题(任何时候都会发生变化,任何人都可以更改,无需对象直接访问等)

有关详细信息,请阅读 感谢.


Cha*_*win 6

可能会建议在大多数使用静态变量的情况下,您确实希望使用单例模式.

全局状态的问题在于,有时在更简单的上下文中作为全局变得有意义,在实际上下文中需要更加灵活,这就是单例模式变得有用的地方.


cet*_*nar 6

在我看来,您询问的是静态变量,但您也在示例中指出了静态方法.

静态变量并不是邪恶的 - 它们被采用为全局变量,如大多数情况下常量与最终修饰符相结合,但正如它所说的那样,不要过度使用它们.

静态方法又称实用方法.使用它们通常不是一种不好的做法,但主要担心的是它们可能会妨碍测试.

作为一个很好的java项目的例子,它使用了很多静态并且正确的方式请看Play!框架.在SO中也有关于它的讨论.

结合静态导入的静态变量/方法也广泛用于库中,便于在java中进行声明性编程,例如:make it easyHamcrest.没有很多静态变量和方法就不可能实现.

所以静态变量(和方法)很好但明智地使用它们!


pty*_*tyx 5

另一个原因是:脆弱.

如果你有一个课程,大多数人都希望能够创建它并随意使用它.

你可以记录它不是这种情况,或者防止它(单件/工厂模式) - 但这是额外的工作,因此需要额外的费用.即便如此,在一家大公司,有可能有人会尝试使用你的课程,而没有完全关注所有好评或工厂.

如果你经常使用静态变量,那就会破坏.虫子很贵.

在可能无能的开发人员的性能改进和强大的改进之间,在很多情况下,稳健性是不错的选择.