如果您在不使用“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等,则可能不会遇到许多这些情况,但如果您正在使用大量的旧代码,则可能会更频繁地遇到这些问题。