ChatGPT解决这个技术问题 Extra ChatGPT

为什么静态变量被认为是邪恶的?

我是一名 Java 程序员,刚接触企业界。最近我使用 Groovy 和 Java 开发了一个应用程序。在我编写的所有代码中,都使用了相当多的静态参数。高级技术人员要求我减少使用的静力学数量。我用谷歌搜索过,我发现很多程序员都反对使用静态变量。

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

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

PS:如果我对静态的假设是错误的,请纠正我。

顺便说一句,Smalltalk 或 Scala 上没有静态变量或方法,正是因为静态方法和变量违反了 OOP 原则。
至少您所做的一个陈述相当好奇:“静态减少了对代码其他部分的相互依赖”。一般来说,它们会加强依赖关系。进行调用的代码与被调用的代码绑定得非常紧密。之间没有抽象,直接依赖。
您的第二段是关于一个完全不同的主题,即静态方法。
函数式编程也对全局状态不屑一顾。如果您曾经(并且应该)有一天进入 FP,请准备好抛弃全局状态的概念。
instead of cluttering the memory with 10,000 instances of the class。如果您的方法可以是静态的,则表明它不依赖于状态。如果它不依赖于状态,为什么需要创建 10000 个对象?为什么不在同一个对象上调用它 10000 次呢?除非你打算从 10000 个不同的位置调用它,在这种情况下你显然需要重构你的代码。

J
Jon Skeet

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

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


最近这似乎是一个争论,无论代码是否可测试。这是一个相当有缺陷的推理。论据应该是“好的设计”,通常好的设计是可测试的。但不是相反:“我无法测试它,因此它一定是糟糕的设计。”不过不要误会我的意思,我总体上同意你的帖子。
@M Platvoet:我会说,如果在两个其他同样有效的设计之间进行选择,可测试的设计会更好。可测试性当然不等于设计得很好,但我很少遇到不可测试的优秀设计,而且我认为它们非常罕见,以至于我可以毫无问题地将可测试性作为对良好设计的通用贡献指标。
@M Platvoet - 可测试性会影响可维护性和可靠性,我会考虑设计质量中的那些主要因素。当然,它们不是唯一的因素,但恕我直言,任何给定代码的成本都是机器周期、开发者周期和用户周期的组合。可测试性命中这三个中的两个。
@M Platvoet - 可测试性也往往会影响可重用性,因为解耦类通常更容易重用。
Platvoet - 我不同意你在这里的第一条评论。我认为如果某些东西无法测试,那么它就是糟糕的设计;因为如果我不能测试它,我就无法知道它是否有效。如果销售人员告诉你“这个车型的设计让它无法进行测试,所以我不知道它是否真的可以运行”,你会买车吗?可测试性对于软件(以及汽车)来说是如此重要,以至于称职的设计要求将其包括在内。
W
William McDowell

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

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

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

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

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

如果出于某种原因您不想要一个对象的多个副本,则 singleton design pattern 比静态对象具有优势,例如线程安全(假设您的单例编码良好),允许延迟初始化,保证对象已正确使用时初始化,子类化,测试和重构代码的优势,更不用说,如果在某些时候你改变主意只想要一个对象的一个实例,那么删除代码以防止重复实例要容易得多而不是重构所有静态变量代码以使用实例变量。我以前必须这样做,这不好玩,而且你最终不得不编辑更多的类,这增加了引入新错误的风险......第一次设置“正确”要好得多,即使它看起来有它的缺点。对我来说,如果你决定在路上需要多份某样东西,那么所需的返工可能是尽可能不经常使用静力学的最令人信服的原因之一。因此,我也不同意您关于静态减少相互依赖性的说法,我认为如果您有很多可以直接访问的静态,而不是“知道如何做的对象”,我认为您最终会得到更加耦合的代码东西”本身。


我喜欢你的回答,我认为它侧重于围绕静态考虑的正确权衡,而不是一些像并发和范围这样的红鲱鱼。对于单例,+1,一个更好的问题可能是何时使用静态变量/方法与单例......
尽管单例本身可能是线程安全的(例如,通过使用 synchronized 方法),但这并不意味着调用代码没有关于单例状态的竞争条件。
此外,静态并不违反 OOP 范式。很多OOP狂热者会告诉你,类是对象,静态方法是类对象的方法,而不是实例。这种现象在 Java 中较少出现。其他语言(例如 Python)允许您将类用作变量,并且您可以访问静态方法作为该对象的方法。
如果我没记错的话,第三段的最后一行应该是你所有的非静态变量。
Object Lifetime ,这是@jessica 提到的非常重要的一点。
P
Preet Sangha

邪恶是一个主观的术语。

在创建和破坏方面,您无法控制静态。他们按照程序加载和卸载的要求生活。

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

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


i
irreputable

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

全球状态是绝对必要的。我们无法避免全球状态。我们无法避免对全球状态进行推理。 - 如果我们想了解我们的应用程序语义。

为了它而试图摆脱全球状态的人,不可避免地会得到一个更复杂的系统——而全球状态仍然存在,巧妙地/愚蠢地伪装在多层间接之下;在展开所有间接性之后,我们仍然需要对全局状态进行推理。

就像那些在 xml 中大肆声明全局状态并认为它更优越的 Spring 人一样。

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


“我有两件事要推理”。如果我使我的测试仅依赖于对象状态,则不会。哪个更容易,我拥有的全局状态越少。
依赖注入与全局状态或全局可见性无关——甚至容器本身也不是全局的。与“普通”代码相比,容器管理对象唯一可见的额外内容是容器本身。事实上,DI 非常常用来避免单例模式。
s
sternr

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

线程安全 - 根据定义,静态资源不是线程安全的

代码隐含性 - 您不知道何时实例化静态变量以及它是否会在另一个静态变量之前实例化


我没有得到线程安全点,我认为没有什么是线程安全的,除非你这样做。这似乎根本与静态事物无关,如果我遗漏了什么,请纠正我。
@Zmaster - 虽然线程安全确实不是静态变量独有的问题,因为根据它们的定义,它们将从不同的上下文中调用,它们对它们来说更修剪
@sternr 我明白你的意思,如果“不同的上下文”不一定等于“不同的线程”。但是确实需要经常考虑静态资源的线程安全性。你应该考虑澄清句子。
例如,静态资源有有效的线程安全使用。私有静态最终 Logger LOG = Logger.getLogger(Foo.class);私有静态最终 AtomicInteger x = new AtomicInteger(0);据我了解,类加载器保证像这样的资源的静态分配是线程安全的。 Logger 实例是或不是线程安全的,与您将指针分配给它的位置无关。保持静态状态可能不是一个好主意,但没有理由不应该是线程安全的。
J
JBCP

如果您使用没有“final”关键字的“static”关键字,这应该是一个仔细考虑您的设计的信号。即使“final”的存在也不是免费通行证,因为可变的静态 final 对象可能同样危险。

我估计大约 85% 的时间我看到没有“决赛”的“静态”,这是错误的。通常,我会找到一些奇怪的解决方法来掩盖或隐藏这些问题。

请不要创建静态变量。尤其是收藏。一般来说,集合应该在它们的包含对象被初始化时被初始化,并且应该被设计成当它们的包含对象被忘记时它们被重置或忘记。

使用静力学会产生非常微妙的错误,这将导致工程师持续数天的痛苦。我知道,因为我既创造了又猎杀了这些虫子。

如果您想了解更多详细信息,请继续阅读……

为什么不使用静力学?

静态有很多问题,包括编写和执行测试,以及不是立即显而易见的细微错误。

依赖静态对象的代码不容易进行单元测试,并且静态也不容易被模拟(通常)。

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

并且数据库填充和初始化需要很长时间。根据我的经验,你的数据库初始化框架会随着时间而改变,这意味着数据会变形,测试可能会中断。 IE,想象一下 Customer 1 以前是 VIP,但是 DB 初始化框架发生了变化,现在 Customer 1 不再是 VIP,但是你的测试是硬编码来加载 Customer 1 的……

更好的方法是实例化 CustomerDAO,并在构造时将其传递给 CustomerFilter。 (更好的方法是使用 Spring 或其他控制反转框架。

完成此操作后,您可以在 CustomerFilterTest 中快速模拟或存根替代 DAO,从而使您可以更好地控制测试,

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

执行测试

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

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

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

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

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

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

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

微妙的错误

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

在处理线程时,与其使用静态对象来存储数据,不如使用在线程启动阶段初始化的对象。这样,每次启动线程时,都会创建一个新的对象实例(具有潜在的新配置),并且您可以避免数据从线程的一个实例流向下一个实例。

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

旁白:静态决赛

“static final”的使用实际上是 C#define 的 Java 等价物,尽管存在技术实现上的差异。 AC/C++ #define 在编译之前由预处理器从代码中换出。 Java“静态final”最终将驻留在JVM的类内存中,使其(通常)永久保存在ram中。这样,它更类似于 C++ 中的“静态 const”变量,而不是 #define。

概括

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


Java“静态最终”最终会驻留在堆栈上的内存吗?我认为静态变量存储在方法区域中。
@wlnirvana - 我的答案是 10 岁,但很好 - 正如这里所指出的,静态变量存储在类内存中:stackoverflow.com/q/6569557/1017787
我的观点是,它被分配在更多“永久”内存中,通常不受应用程序本身的控制(除非您使用的是 OSGi 或其他动态类加载)
c
clearlight

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

优点:

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

缺点:

静态成员始终是内存的一部分,无论它们是否正在使用。您无法控制静态变量的创建和销毁。有用的是,它们是在程序加载时创建的,并在程序卸载时(或 JVM 关闭时)销毁。您可以使用 synchronize 使静态线程安全,但您需要一些额外的努力。如果一个线程更改静态变量的值,可能会破坏其他线程的功能。在使用它之前你必须知道“静态”。您不能覆盖静态方法。序列化不适用于它们。它们不参与运行时多态性。如果使用大量静态变量/方法,则会出现内存问题(在某种程度上,但我猜并不多)。因为在程序结束之前它们不会被垃圾收集。静态方法也很难测试。


缺点 6、7、8 和 10 是所用语言/框架的缺点,而不是一般静态变量的缺点。缺点 1、4 和 5 也存在于其他解决方案中,例如某些框架提供的一些单例模式。 (我没有投票给答案,因为我同意其余的,这是一个不错的收藏。)
@peterh:缺点 #7 是静态字段的基础。如果将静态字段序列化为对象的一部分,则没有明智的方法来处理代码尝试反序列化两个对该静态字段具有不同指定值的实例的情况。
J
Jack Edmonds

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

话虽如此,重要的是要区分常规静态变量(通常被认为是坏的)和最终静态变量(AKA 常量;没那么坏)。


“静态变量代表跨类的状态”......我认为您的意思是“静态变量代表跨实例的状态”? +1 表示“最终静态 AKA 常量,还不错”。由于值不能改变,任何在某个时间点依赖于它的东西都不能在以后隐式地改变它的行为——值是相同的。
“静态变量表示跨实例的状态”是一种更好的表述方式。我已经编辑了我的答案。
C
Community

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

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

我希望你准备好使用锁和处理争用。

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


与静态变量相比,实例变量没有线程安全优势,它们都是不受保护的变量。相反,这一切都取决于您如何保护访问这些变量的代码。
我并没有完全提出这种说法,但为了讨论:分离是一种保护形式。线程状态是分开的;全局状态不是。实例变量不需要保护,除非它在线程之间显式共享;静态变量始终由进程中的所有线程共享。
我希望线程静态变量更像是一个一流的概念,因为它们对于安全地使信息可用于包装的子例程调用非常有用,而无需通过每一层包装传递该信息。例如,如果一个对象具有将其渲染到线程的当前图形上下文的方法,并且有保存/恢复当前图形上下文的方法,则使用这些方法通常比必须通过每个方法调用传递图形上下文更干净。
J
Jérôme Verstrynge

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

您必须平衡将数据封装到具有状态的对象的需求与简单地计算某些数据的函数结果的需求。

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

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

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


M
M Platvoet

在我看来,这几乎与性能无关,而与设计有关。我不认为使用静态方法与使用静态变量相比是错误的(但我猜你实际上是在谈论方法调用)。

它只是关于如何隔离逻辑并给它一个好地方。有时这证明使用静态方法是合理的,java.lang.Math 就是一个很好的例子。我认为当您将大多数类命名为 XxxUtilXxxhelper 时,您最好重新考虑您的设计。


纯无副作用的静态方法非常适合 IMO。但是全局可变状态很少,我将 OP 解释为谈论全局状态。
@CodeInChaos 完全同意。我发现 OP 对静态方法和变量之间的区别并不完全清楚。
C
Community

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

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

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

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

关于状态的推理:如果我创建一个类的新实例,那么我们可以推理这个实例的状态,但如果它有静态变量,那么它可能处于任何状态。为什么?因为静态变量可能已被某些不同的实例修改,因为静态变量是跨实例共享的。

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

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

但是,如果我们真的需要它们怎么办?

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

此外,如果静态变量被标记为 final,它就会变成一个常量,并且一旦分配给它的值就不能更改。这意味着它将使我们摆脱由于其可变性而面临的所有问题。


C
Community

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

静态变量并不是邪恶的——它们在大多数情况下都像常量一样被采用为全局变量,并与 final 修饰符结合使用,但正如它所说,不要过度使用它们。

静态方法又名实用方法。使用它们通常不是一个坏习惯,但主要担心它们可能会obstruct进行测试。

作为一个使用大量静态并以正确方式执行的出色 Java 项目的示例,请查看 Play! framework。 SO中也有关于它的discussion

与静态导入相结合的静态变量/方法也广泛用于促进 Java 中的声明式编程的库中,例如:make it easyHamcrest。如果没有大量静态变量和方法,这是不可能的。

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


A
Anuj Patel

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

如需更多信息,请阅读 this 谢谢。


这有点误导。导致安全问题的不是使用 static 关键字。如果您不将字段声明为静态但将其声明为公开,那将是相同的。我明白你的意思,但经验不足的开发人员可能会从这个答案中得到错误的想法。最好详细说明这一点。
C
Charles Goodwin

可能建议在大多数使用静态变量的情况下,您确实希望使用 singleton pattern

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


p
ptyx

还有一个原因:脆弱。

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

您可以记录情况并非如此,或防止它(单件/工厂模式) - 但这是额外的工作,因此需要额外的成本。即便如此,在一家大公司中,很可能有人会在某个时候尝试使用您的课程,而没有完全注意所有好的评论或工厂。

如果您大量使用静态变量,那将会中断。虫子很贵。

在 0.0001% 的性能提升和潜在无能开发人员更改的稳健性之间,在很多情况下,稳健性是不错的选择。


C
Cygnusx1

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

我明白你的想法,但是一个简单的单例模式也可以做到这一点,而不必实例化 10 000 个对象。

可以使用静态方法,但仅限于与对象域相关且不需要或使用对象内部属性的函数。

前任:

public class WaterContainer {
    private int size;
    private int brand;
    ...etc

    public static int convertToGallon(int liters)...

    public static int convertToLiters(int gallon)...

}

经典的单例(即由 Class.Instance 访问的单例)几乎不比静态变量好。它的可测试性稍高一些,但仍然比您碰巧创建单个实例而不是假设只有一个实例来构建代码的设计差得多。
不知道我理解你的评论!我正在回应 OP 关于他以斜体表示的关于实例化 10 000 个对象的内容。我不明白你为什么要比较单例和静态变量?我从你写的内容中了解到,Singleton 是糟糕的设计......!我想我误解了你,因为 Spring Framework 默认情况下所有的 beans 都是 Singleton ;-)
带有可变状态的经典单例(具有 Class.Instance)是糟糕的设计 IMO。在这种情况下,我非常喜欢这样的设计,我需要将需要使用的单例作为参数传递给使用它们的类(通常在 DI 的帮助下)。逻辑上不可变的经典单例很好 IMO。
@Cygnusx1 如果不清楚为什么类单例(类确保为单个副本的单例)不容易测试,它将类的存在与程序的生命周期紧密结合。要对其进行测试,您必须遵守程序的启动和关闭,这通常会产生对测试类不重要的副作用。如果它有效地作为单例(程序中的一个副本,但不是强制执行),您可以在没有程序的情况下在测试时创建多个副本,验证整个类的行为对于每个测试场景都是应有的。
B
Bryan Agee

“静态是邪恶的”问题更多是关于全球状态的问题。变量成为静态的适当时间是,如果它不具有多个状态; IE 工具应该可以被整个框架访问,并且总是为相同的方法调用返回相同的结果,它们永远不会像静态变量那样“邪恶”。至于你的评论:

我发现静态变量使用起来更方便。而且我认为它们也很有效

对于永远不会改变的变量/类,静态是理想且有效的选择。

全局状态的问题在于它可能产生的内在不一致。有关单元测试的文档通常会解决这个问题,因为任何时候存在可以被多个不相关对象访问的全局状态,您的单元测试将是不完整的,并且不是“单元”粒度。正如这篇关于 global state and singletons 的文章所提到的,如果对象 A 和 B 不相关(如其中一个没有明确引用另一个),那么 A 应该不能影响 B 的状态。

好的代码中禁止全局状态有一些例外,例如时钟。时间是全球性的,而且——在某种意义上——它改变了对象的状态,而没有编码关系。


“时间是全球性的”——在计算系统中,还有其他方法可以对时间进行建模,而不是让它成为一些隐含的、全球性的东西,它会自行改变。参看。本调查:“计算中的时间建模:分类和比较调查”@ arxiv.org/abs/0807.4132
尽管对于大多数地面应用来说,实际时间是一个全局概念,但在生产应用程序中使用实际时间的许多功能可能需要使用“模拟”时间进行某些类型的测试。如果想要确认时钟应用程序将如何处理夏令时的转换,能够让它使用一个可以独立于机器的实际时钟设置的虚拟时钟可以使测试比使用它更容易。要么弄乱计算机的实际时钟,要么等待夏令时开始或结束。
s
studgeek

我的 $.02 是其中几个答案使问题感到困惑,而不是说“静态不好”,我认为谈论范围和实例更好。

我要说的是,静态是一个“类”变量——它代表一个在该类的所有实例之间共享的值。通常它也应该以这种方式限定范围(受保护或私有的类及其实例)。

如果您打算围绕它放置类级别的行为并将其公开给其他代码,那么单例可能是将来支持更改的更好解决方案(正如@Jessica 建议的那样)。这是因为您可以在实例/单例级别以您无法在类级别使用的方式使用接口 - 特别是继承。

关于为什么我认为其他答案中的某些方面不是问题的核心的一些想法......

静力学不是“全局的”。在 Java 中,作用域是与静态/实例分开控制的。

并发对于静态的危险不亚于实例方法。它仍然是需要保护的状态。当然,您可能有 1000 个实例,每个实例都有一个实例变量,并且只有一个静态变量,但是如果访问其中任何一个的代码不是以线程安全的方式编写的,您仍然会被搞砸 - 您可能需要更长的时间才能意识到它.

管理生命周期是一个有趣的论点,但我认为这是一个不太重要的论点。我不明白为什么管理一对像 init()/clear() 这样的类方法比创建和销毁单例实例更难。事实上,有些人可能会说,由于 GC,单例会稍微复杂一些。

PS,对于 Smalltalk,它的许多方言确实有类变量,但在 Smalltalk 中,类实际上是 Metaclass 的实例,所以它们实际上是 Metaclass 实例上的变量。不过,我会应用相同的经验法则。如果它们被用于跨实例的共享状态,那么可以。如果他们支持公共功能,您应该查看 Singleton。叹息,我确实很想念 Smalltalk....


j
jrodriguez

您的帖子中有两个主要问题。

首先,关于静态变量。静态变量是完全不必要的,它的使用可以很容易地避免。在一般的 OOP 语言中,特别是在 Java 中,函数参数是通过引用传递的,也就是说,如果你将一个对象传递给一个函数,你传递一个指向对象的指针,所以你不需要定义静态变量,因为您可以将指向对象的指针传递给需要此信息的任何范围。即使这意味着你会用指针填充你的内存,这也不会代表性能不佳,因为实际的内存分页系统已经过优化以处理这个问题,并且它们将在内存中维护你传递给新的指针所引用的页面范围;静态变量的使用可能会导致系统在需要访问时加载存储它们的内存页面(如果页面长时间没有被访问,就会发生这种情况)。一个好的做法是将所有的静态文件放在一些小的“配置类”中,这将确保系统将它们全部放在同一个内存页面中。

第二,关于静态方法。静态方法并没有那么糟糕,但它们会迅速降低性能。例如,考虑一个比较一个类的两个对象并返回一个值来指示哪个对象更大的方法(典型的比较方法),这个方法可以是静态的,也可以不是,但是当调用它时,非静态形式会更有效因为它只需要解决两个引用(每个对象一个)面对三个引用,这三个引用必须解决同一方法的静态版本(一个用于类加上两个,每个对象一个)。但正如我所说,这还不错,如果我们看一下 Math 类,我们会发现很多定义为静态方法的数学函数。这确实比将所有这些方法都放在定义数字的类中更有效,因为它们中的大多数很少使用,并且将它们全部包含在数字类中会导致类非常复杂并不必要地消耗大量资源。

结论:在处理静态或非静态方法时,避免使用静态变量并找到正确的性能平衡。

PS:对不起我的英语。


Z
Zdeněk Pavlas

静态变量本身没有问题。只是Java语法被破坏了。每个 Java 类实际上定义了两个结构——一个封装静态变量的单例对象和一个实例。在同一个源代码块中定义两者是纯粹的邪恶,并导致代码难以阅读。 Scala 做对了。


t
tomasb

一切(可以:)都有其目的,如果您有一堆需要共享/缓存数据的线程以及所有可访问的内存(因此您不会在一个 JVM 中拆分为上下文),那么静态是最佳选择->当然您可以强制只有一个例子,但为什么呢?我发现这个线程中的一些评论是邪恶的,而不是静态的;)


A
Andres

静态变量没有好坏之分。它们代表描述整个类而不是特定实例的属性。如果您需要为某个类的所有实例设置一个计数器,那么静态变量将是保存该值的正确位置。

当您尝试使用静态变量来保存与实例相关的值时,就会出现问题。


u
user unknown

a) 关于程序的原因。

如果您有一个中小型程序,在其中访问静态变量 Global.foo,对它的调用通常来自无处 - 没有路径,因此没有时间线,变量如何到达该位置,它在哪里用来。现在我怎么知道是谁将它设置为实际值?如果我现在修改它,我怎么知道会发生什么?我对整个源代码进行了 grep,以收集所有访问权限,以了解正在发生的事情。

如果你知道怎么用,因为你只是写了代码,问题是看不到的,但是如果你尝试理解外国代码,你就会明白。

b) 你真的只需要一个吗?

静态变量通常会阻止多个相同类型的程序以不同的值在同一个 JVM 中运行。您通常不会预见到您的程序的多个实例有用的用法,但是如果它发展了,或者如果它对其他人有用,他们可能会遇到这样的情况,他们希望启动您的程序的多个实例.

只有或多或少无用的代码不会被许多人在较长时间内以密集的方式使用,可能会与静态变量很好地配合。


b
blockhead

上面的所有答案都说明了为什么静力学不好。它们是邪恶的原因是因为它给人一种错误的印象,即您正在编写面向对象的代码,而实际上您并非如此。那简直是邪恶的。


但是,严格地考虑你的代码是否遵循一些任意的标准范式实际上会使代码更好,或者我们是否在抱怨避免只编写有效的代码?
是的,它确实让它变得更好,因为它使它在未来更易于管理、更容易理解和更明确。
为什么不写面向对象代码是邪恶的?为什么 Bjarne Stroustrup 不同意你的看法?仅举一个...
我并没有说不写 OO 代码是邪恶的。我说过认为你正在编写 OO 代码是邪恶的,而你只是在静态方法和属性后面伪装全局变量。请重新阅读我写的内容。
S
Sankarganesh Eswaran

这里有很多很好的答案,补充一下,

内存:只要类加载器存在,静态变量就会存在[通常直到 VM 死亡],但这仅适用于存储为静态的批量对象/引用。

模块化:考虑 IOC、dependencyInjection、代理等概念。所有这些都完全反对紧密耦合/静态实现。

其他缺点:线程安全、可测试性


B
Bill K

我玩过很多静力学,我可以给你一个稍微不同的答案——或者一个稍微不同的看待它的方式吗?

当我在一个类中使用静态(成员和方法)时,我最终开始注意到我的类实际上是两个共享责任的类——有一个“静态”部分,它的行为很像一个单例,还有一个非-static 部分(普通类)。据我所知,您始终可以通过选择一个类的所有静态变量和另一个类的非静态变量来完全分离这两个类。

当我在一个类中拥有一个包含该类实例的静态集合和一些管理该集合的静态方法时,这种情况经常发生。一旦你仔细想想,很明显你的班级不是在做“只做一件事”,而是一个集合,做一些完全不同的事情。

现在,让我们稍微重构一下这个问题:如果你把你的类分成一个所有东西都是静态的类,另一个只是一个“普通类”而忘记了“普通类”,那么你的问题就变成了纯静态类 vs 单例类长度为 here(可能还有十几个其他问题)。


J
Java Carzy

静态字段是事实上的 GC 根(参见本章前面的垃圾收集如何工作部分),这意味着它们永远不会被垃圾收集!仅为了方便起见,静态字段和集合通常用于保存缓存或跨线程共享状态。可变静态字段需要显式清理。如果开发人员没有考虑所有可能性(几乎可以肯定),则不会进行清理,从而导致内存泄漏。这种粗心的编程意味着静态字段和集合已成为内存泄漏的最常见原因!

简而言之,永远不要使用可变的静态字段——只使用常量。如果您认为需要可变静态字段,请再考虑一下,然后再考虑!总有更合适的技术。


R
Rahul Anand

我认为使用静态关键字过度使用全局变量也会导致应用程序中某个实例点的内存泄漏


q
qiAlex

在我看来,static 变量应该只是只读数据按约定创建的变量。

例如,我们有一个项目的 ui,我们有一个国家、语言、用户角色等的列表。我们有一个类来组织这些数据。我们绝对确定如果没有此列表,该应用程序将无法运行。所以我们在 app init 上做的第一件事是检查这个列表是否有更新,并从 api 获取这个列表(如果需要)。因此,我们同意这些数据“始终”存在于应用程序中。它实际上是只读数据,所以我们不需要关心它的状态——考虑到这种情况,我们真的不希望有很多这些数据的实例——这种情况看起来是静态的完美候选。