在爪哇使用静态关键字在方法签名上被认为是什么差的实践?如果一个方法基于一些参数执行一个函数,并且不需要访问非静态的字段,那么您不总是希望这些类型的方法是静态的吗?
在大规模Java应用程序中,您将遇到的两大弊病是:
这些破坏了代码的模块性、可扩展性和可测试性,在某种程度上,我意识到我不可能希望在这个有限的时间和空间内说服您这样做。
*"纯函数"是不修改任何状态的任何方法,其结果只依赖于提供给它的参数。例如,任何执行I/O(直接或间接)的函数都不是纯函数,但是math.sqrt()当然是。
更多关于纯函数(self-link)的废话,以及为什么你要坚持它们。
我强烈建议您支持编程的"依赖注入"风格,可能由Spring或Guice等框架支持(免责声明:我是后者的合著者)。如果您这样做是正确的,那么基本上就不需要可变静态或非纯静态方法。
- 所以你会反对在一个静态方法中做任何I/O,即使它与对象的状态无关,它是一个密封类,并且你正在传入一个对象,在这个对象上作为参数执行IO?我同意它仍然不是纯的,但是我不知道将它作为实例方法在这种情况下有什么帮助。
- (我同意可变静态字段通常是个坏主意,请注意。)
- 另一方面,对其他人来说:表达对凯文的异议就像表达对埃里克·利珀特的异议。我感到紧张,几乎要被一个巨大的咯咯声击中头部。请温柔点,凯文…
- 哦,我不知道这个埃里克·利珀特,所以现在我偏执了,觉得他是个混蛋。:)下面是您假设的静态方法的处理过程,它可以进行I/O:很好。哦,但是调用它的代码呢?那个密码不好。那个代码很难测试。由于它无法控制地执行I/O,所以我的测试需要确保以特定的方式设置程序外部事物的状态。现在我依赖于那个设置,如果我忘记了它,我可能会没事的,这取决于我的测试运行的顺序,如果我以后忘记恢复原始状态…等。
- @凯文:我同意这是任何非纯函数的一般问题,但是仅在实例方法中执行I/O有什么帮助?对于测试,我更喜欢注入流/读卡器/编写器/等等,但这与静态方法不同。我怀疑我遗漏了一点。
- @jon如果您是从静态方法进行I/O的,您将如何将其重定向到其他类型的I/O?如果您决定总是传递I/O类型(比如套接字、日志文件或控制台),那么为什么不将其包含在对象实例中并使其非静态呢?我同意@kevin的观点,一般来说,避开他们是一个好习惯。事实上,我倾向于认为纯函数是不确定设计的一个标志(对于那些您不知道什么业务逻辑将使用它们,但在业务逻辑代码中几乎没有位置的库来说,它们是有用的)
- @比尔:怎么说它自然是物体状态的一部分?您甚至可能不需要有问题的对象的实例——比如您提到的实用方法。实际上,我倾向于积极避免将资源负载类型(通常需要清理)放入对象的状态。这意味着你需要更加注意物体的寿命。我通常在一个方法中获取资源,将它们存储为局部变量,很可能将它们传递给其他(可能是静态的)方法,然后清理它们。我觉得这很简单。
- @jon为什么不扩展(或者更好地包含)您传递给函数的对象,并将函数包含在这个新对象中?生命周期没有区别——除了代码组织得更好,而且不必搜索"log"方法,它就在日志记录器中——而不是在某个无关类中运行的某个实用程序……您还将发现您可能需要这个新对象,并且您认为还有许多其他的东西是分散在您的代码中的独立函数。(至少这是我的经验)。
- @比尔K:首先,因为我没有专门研究这种类型,我只是在使用它。另一方面,如果我希望能够传递任何输入流(例如)并从中加载数据,它就不起作用。例如,我不想强迫人们使用显示为加载联系人的inputstream。我的经验是,当你不是真正专攻行为时使用继承是一个坏主意——我更喜欢构图。
- @乔恩,这就是我建议写作的原因(如"更好但包含内容")——我完全同意。但我不明白为什么你更喜欢将对象传递给函数而不是新对象上的方法,但我想这只是一种观点。我个人认为大多数函数都是糟糕的OO代码味道,除非您正在开发用于多个应用程序的低级通用工具…
- @比尔·K:我们可能是在互相指责。我想我们必须有一个具体的例子才能真正了解。
您不希望它是静态的一个原因是允许它在子类中被重写。换句话说,行为可能不取决于对象内的数据,而是取决于对象的确切类型。例如,您可能有一个常规的集合类型,它具有一个isReadOnly属性,该属性在始终可变的集合中返回false,在始终不变的集合中返回true,并且依赖于其他集合中的实例变量。
然而,在我的经验中,这是非常罕见的——通常应该明确说明。通常我会创建一个不依赖于任何对象状态静态的方法。
- @投反对票的人:想说明原因吗?
- static与访问成员字段无关。它是关于类语义的。如果方法应用于类的实例,则它不能是静态的。在您的例子中,是您的集合实例是只读的,而不是类本身,因此函数必须是非静态的。静态实际上是关于类方法的,用于工厂或实用程序函数。
- 请让我有时间实际输入我的评论;-)
- 如果离开评论需要一段时间,那么在投反对票之前,这可能是值得的:(但感谢评论。)
- 不好意思,这是一个冲动的否决票:)
- 我不确定你的评论到底哪一点与我所说的不一致——只是表达方式不同而已。我仍然认为一个方法应用于类的一个实例是相对罕见的,但不依赖于它的状态。唯一能留下的就是对象的精确类型,也就是继承和重写的地方。
- 我以前就有过这样的问题:我需要在测试或其他场景中使用类,但不能覆盖不需要的行为:-(
- 你敢投乔恩·斯基特的票吗?燃烧异端!J/K:D
- @罗博罗格:在这种情况下,我会以不同的方式进行测试。我很少为了测试而重写具体类中的方法。我将实现接口或从抽象类派生,但不是具体的类。显然,这取决于具体的场景,但我不同意不受控制的继承/重写:)
- 我尽可能使用静态方法。有时我需要四处移动一些东西(比如创建一个实用程序类),静态方法使该任务更容易执行。
- @乔恩:我(希望)不是在提倡意大利面继承,只是发出警告。如果需要"移植"的某个对象引用了很多静态方法,而您无法修复它,并且您没有某种"依赖注入"机制,那么它可能会很糟糕。说了这句话,具有讽刺意味的是,我在上一篇文章发表后就转身,写了几个小时的静态方法来调试显示和登录我正在研究的内容。我真是个伪君子!如果必须的话,我稍后将返回并执行接口/单例包装:—)
一般来说,我更喜欢实例方法,原因如下:
静态方法使测试变得困难,因为它们不能被替换,
静态方法更面向过程。
在我看来,静态方法对于实用程序类(如StringUtils)是可以的,但我更喜欢尽量避免使用它们。
- +1尤其是在提到测试时。我们使用的是JUnit,在模拟对象中重写方法是一项需求。
- 关于2:OO是一个工具,而不是一个目标。
- @埃里克艾伦同意了。但是当我使用OO语言时,我喜欢使用这个工具。
- "静态方法更面向过程(因而更少面向对象)"不确定我是否理解这一点。静态方法仍然连接到它的类,当从它的类外部调用或静态导入时必须限定,如果它不需要访问非静态成员,为什么要授予它?
- 我的意思是这种类型的代码:foo.bar(…);bar.foo(…);…即程序化编程。我去掉了(因此也不那么面向对象),这看起来很混乱。
您所说的有点正确,但是当您想要重写派生类中该方法的行为时会发生什么?如果是静态的,就不能这样做。
例如,考虑以下DAO类型类:
1 2 3 4 5 6 7 8 9
| class CustomerDAO {
public void CreateCustomer ( Connection dbConn, Customer c ) {
// Some implementation, created a prepared statement, inserts the customer record.
}
public Customer GetCustomerByID ( Connection dbConn, int customerId ) {
// Implementation
}
} |
现在,这些方法都不需要任何"状态"。他们所需要的一切都作为参数传递。所以它们很容易是静态的。现在,您需要支持一个不同的数据库(比如Oracle)。
因为这些方法不是静态的,所以您可以创建一个新的DAO类:
1 2 3 4 5 6 7 8 9
| class OracleCustomerDAO : CustomerDAO {
public void CreateCustomer ( Connection dbConn, Customer c ) {
// Oracle specific implementation here.
}
public Customer GetCustomerByID ( Connection dbConn, int customerId ) {
// Oracle specific implementation here.
}
} |
这个新班现在可以代替旧班了。如果您使用的是依赖注入,那么它甚至可能根本不需要更改代码。
但是如果我们将这些方法设置为静态的,这会使事情变得更加复杂,因为我们不能简单地在新类中重写静态方法。
- +1,即使是无状态方法也可能属于一个实例,并且可用于重写。
静态方法通常有两个目的。第一个目的是使用某种全局实用程序方法,类似于java.util.collections中的功能。这些静态方法通常是无害的。第二个目的是控制对象实例化,并通过各种设计模式(如单例和工厂)限制对资源(如数据库连接)的访问。如果实施不当,可能会导致问题。
对于我来说,使用静态方法有两个缺点:
它们使代码模块化程度更低,更难测试/扩展。大多数答案都已经解决了这个问题,所以我不会再讨论它了。
静态方法往往会导致某种形式的全局状态,这通常是引起潜在错误的原因。这可能发生在为上述第二个目的而编写的写得不好的代码中。让我详细说明一下。
例如,考虑一个项目,该项目需要将某些事件记录到数据库中,并且其他状态也依赖于数据库连接。假设通常,首先初始化数据库连接,然后配置日志框架将某些日志事件写入数据库。现在假设开发人员决定从手写的数据库框架转移到现有的数据库框架,如Hibernate。
但是,这个框架可能有自己的日志配置——如果它恰好使用了与您相同的日志框架,那么配置之间很可能会有各种冲突。突然间,切换到不同的数据库框架会导致系统的不同部分出现错误和故障,而这些部分似乎是无关的。发生这种故障的原因是日志配置通过静态方法和变量保持全局状态,并且各种配置属性可以被系统的不同部分覆盖。
为了避免这些问题,开发人员应该避免通过静态方法和变量存储任何状态。相反,他们应该构建干净的API,让用户根据需要管理和隔离状态。BerkeleyDB就是一个很好的例子,它通过环境对象而不是静态调用来封装状态。
- 这是一个非常连贯的反对静态状态的论点,但一般来说不反对静态方法。我编写的大多数静态方法只使用它们的参数。诚然,我确实倾向于使用静态记录器变量,我认为这有问题…
- 好的,这就是我在第一段中试图概述的内容——静态方法通常不会改变参数之外的任何状态,但管理状态的静态方法可能会很麻烦。所以我想作为决定何时编写静态方法的快速经验法则,您应该看看是否有任何状态会受到该方法的影响。
这里有两个问题1)创建对象的静态方法在第一次访问时保持在内存中加载?这不是一个缺点吗?2)使用Java的优点之一是它的垃圾收集功能-我们在使用静态方法时忽略了这一点吗?
静态方法的另一个烦恼是:如果不在函数周围创建包装类,就无法轻松地传递对该函数的引用。例如-类似于:
1
| FunctorInterface f = new FunctorInterface() { public int calc( int x) { return MyClass.calc( x); } }; |
我讨厌这种Java制作的工作。也许Java的后期版本会得到委托或类似的函数指针/过程类型机制?
一个小问题,但还有一件事是不喜欢无缘无故的静态函数,er,方法。
如果要独立于类的任何对象使用类成员,则应将其声明为静态的。如果声明为静态,则可以在没有类对象的现有实例的情况下访问它。静态成员由该特定类的所有对象共享。
这是正确的。事实上,你必须扭曲否则可能是一个合理的设计(有一些不相关的功能与一个类)到Java术语。这就是为什么你看到了捕获所有类,比如fredswingutils和yetanotherioutils。