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

一曲冷凌霜 提交于 2020-01-07 04:22:12

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

我是一位Java程序员,对公司领域来说是新手。 最近,我已经使用Groovy和Java开发了一个应用程序。 我编写的所有代码都使用了大量的静态变量。 高级技术人员要求我减少使用的静电数量。 我已经在谷歌上搜索了相同的内容,并且发现许多程序员都反对使用静态变量。

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

而且,静态函数减少了对代码其他部分的相互依赖。 他们可以充当完美的国家拥有者。 除此之外,我发现在诸如SmalltalkScala的某些语言中广泛实现了静态方法。 那么,为什么在程序员中(尤其是在Java世界中)普遍存在对静态的这种压迫?

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


#1楼

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

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

请不要创建静态可变项。 特别是收藏。 通常,应该在初始化其包含的对象时初始化集合,并且应该对其进行设计,以使它们在忘记包含的对象时被重置或忘记。

使用静电会产生非常细微的错误,这会给工程师带来持续的痛苦。 我知道,因为我已经创建并发现了这些错误。

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

为什么不使用静态?

静态问题很多,包括编写和执行测试,以及一些不明显的错误。

依赖静态对象的代码无法轻松进行单元测试,并且静态对象也无法轻松模拟(通常)。

如果使用静态,则不能为了测试更高级别的组件而交换类的实现。 例如,假设有一个静态的CustomerDAO,它返回从数据库加载的Customer对象。 现在,我有一个CustomerFilter类,该类需要访问一些Customer对象。 如果CustomerDAO是静态的,则必须先初始化数据库并填充有用的信息,然后才能为CustomerFilter编写测试。

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

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

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

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

执行测试

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

这是一个过于笼统的示例,但是在大型系统中,此问题始终存在。 人们不会想到在同一JVM中重复启动和停止其软件的单元测试,但这是对软件设计的很好测试,如果您渴望实现高可用性,则需要注意这一点。

这些问题通常是由框架对象引起的,例如,数据库访问,缓存,消息传递和日志记录层。 如果您使用的是Java EE或某些最佳框架,那么它们可能会为您管理很多此类事务,但是如果像我一样,您正在处理旧系统,则可能会有很多定制框架来访问这些层。

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

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

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

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

细微的错误

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

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

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

旁白:静态决赛

尽管存在技术上的差异,“静态最终”的使用实际上等效于C #define。 在编译之前,预处理器会将AC / C ++ #define换出代码。 Java“静态最终”将最终驻留在堆栈上的内存。 这样,它更类似于C ++中的“静态const”变量,而不是#define。

摘要

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


#2楼

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

首先,关于静态变量。 静态变量完全不必要,可以轻松避免使用它。 通常,在OOP语言中,尤其是在Java中,函数参数是通过引用来粘贴的,这就是说,如果将对象传递给函数,则将指针传递给该对象,因此不需要定义静态变量,因为您可以将指针传递给需要此信息的任何作用域。 即使这意味着您将用指针填充您的内存,但这也不一定表示性能低下,因为实际的内存分配系统已针对此问题进行了优化,并且它们将在内存中保留由您传递给新指针的指针所引用的页面范围; 使用静态变量可能会导致系统在需要访问它们时加载存储页面的内存页面(如果很长时间未访问该页面,就会发生这种情况)。 一个好的做法是将所有静态stuf放在一些小的“配置类”中,这将确保系统将它们全部放在同一内存页中。

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

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

PS:对不起,我的英语。


#3楼

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


#4楼

认为如果您的应用程序有很多用户,并且定义了静态表单,那么每个用户也会修改其他用户的所有其他表单。


#5楼

这里有很多很好的答案,

内存:只要类加载器存在(通常直到VM死机),静态变量就一直存在,但这仅在将大对象/引用存储为静态的情况下。

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

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!