如果你使用“static”关键字而没有使用“final”关键字,这应该是一个信号,提醒你仔细考虑你的设计。即使“final”的存在也不是免费的,因为可变的静态final对象可能同样危险。
我估计在85%的情况下,我看到的是没有“final”的“static”,这是错误的。我经常会发现一些奇怪的方法来掩盖或隐藏这些问题。
请不要创建静态变量。特别是集合。一般来说,集合应该在其包含对象初始化时初始化,并且应该设计为当其包含对象被遗忘时重置或忘记集合。
使用静态方法可能会产生非常细微的错误,这会让工程师痛苦数日。我知道,因为这些虫子都是我创造和猎杀的。
如果你想了解更多细节,请阅读…
为什么不使用静态方法?
静态有很多问题,包括编写和执行测试,以及一些不太明显的细微错误。
依赖于静态对象的代码不容易进行单元测试,而静态对象也不容易被模拟(通常)。
如果使用静态,则不可能为了测试更高级别的组件而交换类的实现。例如,假设一个静态CustomerDAO返回它从数据库加载的Customer对象。现在我有一个类CustomerFilter,它需要访问一些Customer对象。如果CustomerDAO是静态的,那么如果不首先初始化数据库并填充有用的信息,就无法为CustomerFilter编写测试。
数据库填充和初始化需要很长时间。根据我的经验,您的DB初始化框架会随着时间的推移而改变,这意味着数据会发生变化,测试可能会中断。IE,假设客户1曾经是一个VIP,但是DB初始化框架改变了,现在客户1不再是VIP,但是你的测试被硬编码来加载客户1…
更好的方法是实例化CustomerDAO,并在构造CustomerFilter时将其传递给CustomerFilter。(更好的方法是使用Spring或另一个反转控制框架。
一旦你这样做了,你就可以在CustomerFilterTest中快速模拟或剔除一个备用DAO,允许你对测试有更多的控制,
如果没有静态DAO,测试将更快(没有db初始化)并且更可靠(因为当db初始化代码更改时它不会失败)。例如,在本例中,就测试而言,确保客户1始终是VIP。
执行测试
Statics cause a real problem when running suites of unit tests together (for example, with your Continuous Integration server). Imagine a static map of network Socket objects that remains open from one test to another. The first test might open a Socket on port 8080, but you forgot to clear out the Map when the test gets torn down. Now when a second test launches, it is likely to crash when it tries to create a new Socket for port 8080, since the port is still occupied. Imagine also that Socket references in your static Collection are not removed, and (with the exception of WeakHashMap) are never eligible to be garbage collected, causing a memory leak.
这是一个过于一般化的例子,但是在大型系统中,这个问题总是会发生。人们不会认为单元测试在同一个JVM中重复地启动和停止他们的软件,但它是对软件设计的一个很好的测试,如果您希望实现高可用性,那么您需要注意这一点。
这些问题经常出现在框架对象中,例如,您的DB访问、缓存、消息传递和日志记录层。如果您正在使用Java EE或一些最好的框架,它们可能会为您管理很多这方面的工作,但如果您像我一样正在处理遗留系统,则可能会有许多自定义框架来访问这些层。
如果应用于这些框架组件的系统配置在单元测试之间发生了变化,并且单元测试框架没有拆除并重新构建组件,那么这些变化就不能生效,并且当测试依赖于这些变化时,它们就会失败。
甚至非框架组件也会遇到这个问题。想象一个名为OpenOrders的静态映射。您编写了一个测试,该测试创建了几个开放订单,并检查以确保它们都处于正确的状态,然后测试结束。另一个开发人员编写第二个测试,将所需的订单放入OpenOrders映射中,然后断言订单数量是准确的。单独运行时,这些测试都将通过,但在一个套件中一起运行时,它们将失败。
更糟糕的是,失败可能基于运行测试的顺序。
在这种情况下,通过避免静态,您可以避免跨测试实例持久化数据的风险,从而确保更好的测试可靠性。
细微的错误
如果您工作在高可用性环境中,或者线程可能启动和停止的任何地方,那么当您的代码在生产环境中运行时,上面提到的与单元测试套件相同的问题也可以适用。
在处理线程时,与其使用静态对象来存储数据,不如使用在线程启动阶段初始化的对象。这样,每次线程启动时,都会创建对象的一个新实例(可能具有新的配置),并且可以避免线程的一个实例的数据泄漏到下一个实例。
When a thread dies, a static object doesn’t get reset or garbage collected. Imagine you have a thread called “EmailCustomers”, and when it starts it populates a static String collection with a list of email addresses, then begins emailing each of the addresses. Lets say the thread is interrupted or canceled somehow, so your high availability framework restarts the thread. Then when the thread starts up, it reloads the list of customers. But because the collection is static, it might retain the list of email addresses from the previous collection. Now some customers might get duplicate emails.
题外话:静态决赛
“static final”的使用实际上相当于Java中的c#定义,尽管在技术实现上存在差异。C/ c++ #定义在编译前被预处理程序从代码中交换出来。Java的“静态final”将最终驻留在JVM的类内存中,使其(通常)永久存在于ram中。这样,它更类似于c++中的“静态const”变量,而不是#define变量。
总结
我希望这有助于解释为什么静态统计是有问题的几个基本原因。如果您正在使用Java EE或Spring等现代Java框架,您可能不会遇到很多这样的情况,但如果您正在处理大量遗留代码,它们可能会变得更加频繁。