Java:为什么不声明静态变量而是传递引用?

3

假设我们想制作一个收集宝石的游戏。因此,我们需要一个Gem类、一个GemSpawner类和当然是MainActivity类。

public class MainActivity {
    public static void main(String[] args) {

        List<Gem> gems = new ArrayList<Gem>();

        GemSpawner gs = new GemSpawner(gems);

        //...
    }
}

在这种情况下,我们使用宝石列表创建了一个List,并通过构造函数将其传递给GemSpawner,因此gs可以使用以下方法向列表中添加宝石:
gems.add(new Gem(10, 50, "red")); //should represent Xpos, Ypos, and color.

但是这样做会更好:
public class MainActivity {

    public static List<Gem> gems = new ArrayList<Gem>();

    public static void main(String[] args) {

        GemSpawner gs = new GemSpawner();

        //...
    }
}

现在,GemSpawner (gs) 可以使用以下方式添加宝石:

MainActivity.gems.add(new Gem(10, 50, "red"));

一位朋友向我展示并解释了上述方法,但下面的方法不是更加有效吗?

你的问题有点脱离上下文。gems 数组中的这些宝石在哪里使用?通常情况下,你不应该将经常使用的东西存储在静态上下文中,即使你可以这样做。你也可以将所有代码都写在 static void main 方法中,但同样你永远不会这样做。 - Birb
5个回答

7
效率的论点并不能太站得住脚。只有一个引用,所以在开始时解析32位(如果您使用64位机器,则为64位),因此它没有任何(可测量的)性能影响。
然而,如果你使用了很多静态内容,你的代码设计会变得凌乱。例如,通过将其设置为静态,没有简单的方法来跟踪是什么添加了东西到您的列表中(也许晚上测试一些东西后随机添加一个gem,然后忘记。几个月后,你无法弄清楚为什么这个gem一直出现!)
听起来很傻,但限制对事物的访问确实可以帮助调试。如果您知道唯一可以添加gem是,那么您已经将潜在的基于gem的错误隔离到一个类中。如果可能来自任何地方,那么随着项目的复杂性增长,调试可能会变得更加困难。

1
此外,您不能以这种方式运行多个游戏实例,因为它们都将访问相同的宝石字段。如果我错了,请纠正我,但您将无法使用静态字段使游戏变成多人游戏。 - Shaun Wild

5
静态数据成员引入了我们所谓的“全局状态”概念。通常来说,全局状态会因为以下几个原因而被认为是不好的:
- 它使得读取全局状态的方法的行为变得不可预测。这有两个影响: - 很难推断一个方法的结果是否仅仅依赖于它所属的对象和传递给它的参数。 - 这可能会使得对该方法进行形式化测试的尝试毫无意义(尽管有一些例外)。 - 它做出了可能现在成立但以后不成立的“假设”。例如,如果一个过程修改了全局状态,那么如果它在程序运行时严格只运行一次,那么它可能没问题。但是以后你可能会让这个过程运行多次。在这种情况下: - 按顺序,后续迭代将必须注意“使用”状态。 - 并行地,在没有有效保护措施的情况下,你将打开数据竞争和线程不安全的大门。 另一方面,如果你将所需的状态作为参数传递给每个方法,你就创建了一个安全网,自动使得上述所有问题变得不太可能发生,并威胁到你程序的正确性。
我怀疑选择全局状态而不是传递状态并没有显著的性能优势(或者根本没有任何优势)。如果你最终陷入了过多全局状态验证或更糟糕的情况,即引入锁和障碍(这两种情况都非常昂贵),那么你将取消任何你本来可能获得的好处。
坚持不引入全局状态的良好实践要比其他做法更值得。

4

public static变量本质上是全局变量。

在经验丰富的程序员中,全局变量被认为是非常糟糕的编程风格。

这里有一篇文章描述了它们为什么不好

面向对象的解决方案是将您的gems列表封装为GemManager类中的私有集合,并通过方法公开对gems的任何访问。然后,您将创建一个GemManager实例,并将该实例传递给需要它的每个类或方法。


3
这比你想象的要复杂得多。
许多开始使用Java编写代码的人都会把每个东西都变成 static 的,正是因为不需要传递引用,这使得你的代码更加“简单”。
然而,当你的代码变得更加复杂时,你会遇到很多问题。这些问题主要有以下3个方面:
封装
抽象
测试 封装 这是一个概念,即一个对象不应直接访问其成员,它应该被告知“做事情”,然后在内部完成,而不暴露如何完成的细节。
这背后的理念是尽量避免将类之间耦合得过紧。
这就引出了下一个问题: 抽象 在Java中,这通过 abstract 类和 interface 实现。
其中的理念是,您的 GemSpawner 只是一种生成宝石的定义。如何在内部实现与其无关。
在Java中,您无法真正协调 static 方法和继承的关键面向对象编程思想。
静态方法是可以继承的,但它们只能被隐藏而无法被覆盖,因此不能(容易地)修改它们的行为。
这就引出了下一个问题: 测试 这是一个随着程序变得越来越复杂而越来越常见的主题。
如何测试一个“Hello World”程序?好吧,你运行它,看看它是否打印了“Hello World”,如果是,“Hello Wrld”则有错误。
一旦程序变得更加复杂,您就无法简单地这样做。您需要将程序拆分并测试“单元”。称为单元测试
在这里,您的 static 引用真的会引起问题。因为每个东西都通过直接类引用相互关联,所以您无法将程序分解为离散单元。您也无法模拟 static 方法的行为,因为它们不易被覆盖。
因此,总之,是的;可能将每个东西设为 static 并不传递引用确实更快、更容易。但是,如果您计划编写像游戏这样复杂的东西,那么您应该真正考虑充分发挥Java的潜力。

0

使用静态变量几乎没有任何好处,但有一些缺点:

  • 您无法创建多个游戏副本,无论是同时运行还是作为历史副本 - 您已将其锁定为每个JVM最多一个副本
  • 您之间存在不必要的类依赖关系 - 请参见耦合
  • 您可能希望从另一个类启动游戏,但现在您不能 - 您已将初始化锁定到主类
  • 您已经让实现“泄漏”到另一个类中 - 请参见内聚性

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接