【问题标题】:Choosing when to instantiate classes选择何时实例化类
【发布时间】:2008-11-15 09:54:46
【问题描述】:

我最近为一个作业编写了一个类,我必须将名称存储在一个 ArrayList(在 java 中)中。我将 ArrayList 初始化为实例变量private ArrayList<String> names。后来当我对照解决方案检查我的工作时,我注意到他们已经在 run() 方法中初始化了他们的 ArrayList。

我想了一会儿,我觉得这可能是一个品味问题,但一般来说,在这种情况下如何选择?是占用内存少还是什么?

PS 我喜欢 Ruby 中以 @ 符号开头的实例变量:它们更可爱。

(元问题:这个问题的更好标题是什么?)

【问题讨论】:

  • 可能的元答案:“选择何时初始化字段”“何时使用惰性字段初始化”

标签: java variables performance


【解决方案1】:

用伟大的 Knuth 的话来说“过早的优化是万恶之源”。

只需担心您的程序运行正常并且没有错误。这比以后难以调试的晦涩优化要重要得多。

但要回答您的问题 - 如果您在类成员中进行初始化,则将在代码中首次提及您的类时分配内存(即当您从中调用方法时)。如果您在一个方法中进行初始化,则内存分配会在稍后调用此特定方法时发生。

所以只是后期初始化的问题……这在业界称为惰性初始化。

【讨论】:

  • 我不确定是延迟初始化还是使用实例变量而不是局部变量。
【解决方案2】:

初始化

根据经验,请尝试在声明变量时对其进行初始化。

如果变量的值永远不会改变,请使用 final 关键字明确表示。这可以帮助您推断代码的正确性,虽然我不知道可以识别 final 关键字的编译器或 JVM 优化,但它们肯定是可能的。

当然,这条规则也有例外。例如,可以在 if-else 或 switch 中分配变量。在这种情况下,“空白”声明(没有初始化的声明)优于保证在读取虚拟值之前被覆盖的初始化。

/* DON'T DO THIS! */
Color color = null;
switch(colorCode) {
  case RED: color = new Color("crimson"); break;
  case GREEN: color = new Color("lime"); break;
  case BLUE: color = new Color("azure"); break;
}
color.fill(widget);

如果出现无法识别的颜色代码,您现在有一个NullPointerException。最好不要分配无意义的null。编译器会在调用color.fill() 时产生错误,因为它会检测到您可能没有初始化color

为了在这种情况下回答您的问题,我必须查看相关代码。如果解决方案在 run() 方法中对其进行了初始化,则它必须被用作临时存储,或者作为“返回”任务结果的一种方式。

如果集合用作临时存储,并且在方法之外无法访问,则应将其声明为局部变量,而不是实例变量,并且很可能应在方法中声明的位置对其进行初始化。

并发问题

对于初级编程课程,您的讲师可能不会试图让您面对并发编程的复杂性——尽管如果是这样的话,我不确定您为什么使用Thread。但是,随着 CPU 设计的当前趋势,任何学习编程的人都需要牢牢掌握并发性。我会在这里尝试更深入地研究。

从线程的run 方法返回结果有点棘手。这个方法就是Runnable接口,没有什么能阻止多个线程执行单个实例的run方法。由此产生的并发问题是 Java 5 中引入的Callable 接口背后的动机的一部分。它很像Runnable,但可以以线程安全的方式返回结果,如果任务可以则抛出Exception不会被执行。

有点题外话,但如果你好奇,可以考虑以下示例:

class Oops extends Thread { /* Note that thread implements "Runnable" */

  private int counter = 0;

  private Collection<Integer> state = ...;

  public void run() {
    state.add(counter);
    counter++;
  }

  public static void main(String... argv) throws Exception {
    Oops oops = new Oops();
    oops.start();
    Thread t2 = new Thread(oops); /* Now pass the same Runnable to a new Thread. */
    t2.start(); /* Execute the "run" method of the same instance again. */
    ...
  }
}

main 方法结束时,您几乎不知道Collection 的“状态”是什么。两个线程同时处理它,我们还没有指定集合是否可以安全地并发使用。如果我们在线程内部初始化它,至少我们可以说最终state会包含一个元素,但我们不能说它是0还是1。

【讨论】:

  • 不确定发帖人是否真的在使用线程。他使用的方法可能恰好被称为“运行”?
【解决方案3】:

来自wikibooks

Java 中有三种基本的变量作用域:

  • 局部变量,在类的方法中声明,在方法执行期间有效(并且仅占用存储空间)。每次调用该方法时,都会使用一个新的变量副本。

  • 实例变量,在类中声明但在任何方法之外。只要对应的对象在内存中就有效并占用存储;一个程序可以实例化该类的多个对象,每个对象都有自己的所有实例变量的副本。这是面向对象编程的基本数据结构规则;类被定义为在给定系统中保存特定于“对象类”的数据,并且每个实例都保存自己的数据。

  • 静态变量,在类中声明为静态,在任何方法之外。无论从该类实例化多少对象,此类变量都只有一个副本。

所以是的,内存消耗是一个问题,特别是如果 run() 中的 ArrayList 是本地的。

【讨论】:

    【解决方案4】:

    我不完全理解你的全部问题。

    但据我目前了解,性能/内存优势将相当小。因此,我肯定会支持易用性方面。

    所以做最适合你的事情。仅在需要时解决性能/内存优化问题。

    【讨论】:

      【解决方案5】:

      对于实例变量,我个人的经验法则是初始化它们,至少使用默认值,或者:

      1. 在声明时间,即

        private ArrayList&lt;String&gt; myStrings = new ArrayList&lt;String&gt;();

      2. 在构造函数中

      如果它确实是一个实例变量,并且表示对象的状态,那么它会在构造函数退出时完全初始化。否则,您可能会尝试在变量有值之前访问它。当然,这不适用于您将自动获得默认值的原语。

      对于静态(类级)变量,在声明或静态初始化程序中初始化它们。如果我进行了计算或其他工作来获取值,我会使用静态初始化程序。如果您只是调用 new Foo() 或将变量设置为已知值,请在声明中初始化。

      【讨论】:

        【解决方案6】:

        您必须避免延迟初始化。这会导致以后出现问题。
        但是如果你因为初始化太重而必须这样做,你必须这样做:

        静态字段:

        // Lazy initialization holder class idiom for static fields
        private static class FieldHolder {
           static final FieldType field = computeFieldValue();
        }
        static FieldType getField() { return FieldHolder.field; }
        

        实例字段:

        // Double-check idiom for lazy initialization of instance fields
        private volatile FieldType field;
        FieldType getField() {
           FieldType result = field;
           if (result == null) { // First check (no locking)
              synchronized(this) {
                 result = field;
                 if (result == null) // Second check (with locking)
                    field = result = computeFieldValue();
              }
           }
           return result;
        }
        

        根据 Joshua Bolch 的“Effective Java™ 第二版”(ISBN-13:978-0-321-35668-0):
        “明智地使用延迟初始化”

        【讨论】:

        • en.wikipedia.org/wiki/Double-checked_locking 支持我的“不太线程安全”声明。
        • 我修正了身份。关于声明:所以 Joshua Bolch 错了?在维基百科中它说:“从 J2SE 5.0 开始,这个问题已得到修复。volatile 关键字现在可确保多个线程正确处理单例实例”
        • Re: damian: 其实你是对的,一旦你使用了 volatile 就可以了。道歉。
        猜你喜欢
        • 2020-03-31
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-03-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多