两种方法有什么优势?

示例1:

class A {
    B b = new B();
}


示例2:

class A {
    B b;

    A() {
         b = new B();
    }
}


#1 楼


没有什么区别-实例变量的初始化实际上是由编译器放入构造函数中的。
第一个变量更易于阅读。
第一个变体。

另外还有初始化块,编译器也将其放在构造函数中:

{
    a = new A();
}



检查Sun的解释和建议

来自本教程:


但是,字段声明不是任何方法的一部分,因此无法执行如陈述。相反,Java编译器会自动生成实例字段初始化代码,并将其放入该类的一个或多个构造函数中。初始化代码按照在源代码中出现的顺序插入到构造函数中,这意味着字段初始化器可以使用在其之前声明的字段的初始值。


此外,您可能想要延迟初始化您的字段。如果初始化字段是一项昂贵的操作,则可以在需要时立即对其进行初始化:

ExpensiveObject o;

public ExpensiveObject getExpensiveObject() {
    if (o == null) {
        o = new ExpensiveObject();
    }
    return o;
}


最终(如Bill所指出),对于为了进行依赖性管理,最好避免在类中的任何地方使用new运算符。相反,最好使用Dependency Injection-即让其他人(另一个类/框架)实例化并将依赖项注入您的类中。

评论


@Bozho对象初始化在初始化块之前还是之后进入构造函数?

– Cruncher
2013年9月3日20:34在

我认为之前。但不确定:)

– Bozho
2013年9月4日上午10:55

第一个变体是更具“可读性”的,这是可以讨论的:如果在构造函数中初始化所有字段,则您确切地知道在阅读代码时,只有一个地方可以搜索...

–nbro
15年5月8日在10:42



@Bozho-您能解释一下为什么不能使用第一个变量进行异常处理吗?

– Mario Galea
2015年11月23日14:52

“并且最终(如Bill所指出的那样),为了进行依赖管理,最好避免在类中的任何地方使用new运算符。相反,最好使用Dependency Injection。”至少你说更好。如果热心跟随鲍勃叔叔,可能会引起很多问题(例如工厂爆炸)。新操作符没有什么问题,也不是所有的依赖项都需要注入,特别是如果您对社交测试感到满意的话。

–brumScouse
17年1月13日在11:42

#2 楼

另一个选择是使用依赖注入。

class A{
   B b;

   A(B b) {
      this.b = b;
   }
}


这消除了从B的构造函数创建A对象的责任。从长远来看,这将使您的代码更具可测试性,并且更易于维护。这个想法是要减少两个类AB之间的耦合。这给您带来的好处是,您现在可以将任何扩展了B的对象(或实现为接口的B)传递给A的构造函数,它将起作用。一个缺点是您放弃了B对象的封装,因此它对A构造函数的调用者公开。您必须考虑这些好处是否值得这种折衷,但在许多情况下是值得的。

评论


另一方面,从现在您使A和B之间的链接更加可见的意义上来说,它增加了耦合。以前,使用B是A的内部问题,如果事实证明更好的设计是不使用B,则您的建议很难更改。

– JaakkoK
2010年1月3日,7:51

无论如何,耦合是存在的-A需要B。但是在类中实例化它意味着“ A恰好需要这个B”,而DI允许使用许多不同的B。

– Bozho
2010年1月3日在8:57



A现在在此设计中需要B,我的意思是这种情况是否会改变。

– JaakkoK
2010年1月3日,9:52

@jk:如果使用DI和Factory类将对象的创建与业务逻辑的任何地方(特别是在创建A的地方)分开,则根本就不难更改。它只需要在一个地方更改,即创建A对象的工厂。如果您对此保持一致,则一点都不难掌握。我认为收益大于成本。减少了耦合,整体设计更易于测试和维护。

–比尔蜥蜴
2010年1月3日,14:33

@BilltheLizard即使对于诸如List intList = new ArrayList <>();之类的简单内容,您也会使用此惯用法吗?这可能完全是内部实现细节。将ArrayList传递给构造函数似乎与良好封装完全相反。

–短跑运动员
2015年1月22日23:11



#3 楼

我今天被一种有趣的方式烧死了:

class MyClass extends FooClass {
    String a = null;

    public MyClass() {
        super();     // Superclass calls init();
    }

    @Override
    protected void init() {
        super.init();
        if (something)
            a = getStringYadaYada();
    }
}


看到错误了吗?事实证明,在调用超类构造函数之后,将调用a = null初始化程序。由于超类构造函数调用init(),因此a的初始化之后是a = null的初始化。

评论


这里的教训是永远不要从构造函数中调用可重写的函数! :)有效的Java,第17项对此进行了很好的讨论。

– Mohit Chugh
15年8月26日在6:33

优点。通过在声明时进行初始化,您将无法精确控制何时初始化变量。那可以,但是您可以使用a $$(是的,编译器也确实会更改其实现!)。

– SMBiggs
16年6月10日在6:40

@MohitChugh:的确如此。实际上,如今,如果您从构造函数中调用可重写的方法,则像NetBeans这样的现代Java IDE(当然还有其他人)也会向您发出警告。而这正是Edward Falk遇到的原因。

– GeertVc
18年7月30日在9:01

#4 楼

我个人的“规则”(几乎从未中断过)是:


在一个块的开头声明所有变量
使所有变量最终确定,除非它们
不能
每行声明一个变量
不要在
声明的情况下初始化变量
仅在需要
数据的情况下在
构造函数中初始化
构造函数执行
初始化

,所以我会像这样的代码:

public class X
{
    public static final int USED_AS_A_CASE_LABEL = 1; // only exception - the compiler makes me
    private static final int A;
    private final int b;
    private int c;

    static 
    { 
        A = 42; 
    }

    {
        b = 7;
    }

    public X(final int val)
    {
        c = val;
    }

    public void foo(final boolean f)
    {
        final int d;
        final int e;

        d = 7;

        // I will eat my own eyes before using ?: - personal taste.
        if(f)
        {
            e = 1;
        }
        else
        {
            e = 2;
        }
    }
}


这样,我总是100%确定在哪里查找变量声明(在块的开头),以及它们的赋值(在声明之后有意义的时候)。由于您永远都不会使用未使用的值初始化变量(例如,声明和初始化vars,然后在一半的var需要具有值之前引发异常),因此这也可能使效率更高。您也不会做无意义的初始化(例如int i = 0;然后,在使用“ i”之前,i = 5;。)

我非常重视一致性,所以下面这个“规则”是我一直在做的事情,它使使用代码变得容易得多,因为您不必四处寻找东西。

您的工作量可能会有所不同。

评论


可能是因为“永远不要在声明的位置初始化变量”(尽管不是我)。或“换行符”,它被认为是C / C ++习惯用法。无论如何,我要投票赞成;)

– Bozho
2010年1月4日在6:42

我宁愿人们出于技术原因而不是美学原因({}位置或它们的不必要用法)投下反对票。如果人们投了反对票,他们至少应该说出他们认为答案是错误的...从技术上讲,这没有什么错,这是我过去20年用C / C ++ / Java编码的方式(以及Java 16)因此,我100%确信它会起作用:-)(感谢您的反投票:-)

–豆腐啤酒
2010年1月4日,下午6:47

这是罪恶的丑陋,那就是问题所在。在使用三元运算符之前您会吃掉自己的眼睛,但是相对于OOP合适的构造函数,它更喜欢使用多个静态初始化块,这真是很有趣。您的方式完全打破了依赖注入(从表面上看,是的,编译器本质上是通过将所有内容移至构造函数来为您解决的,但实际上您是在教人们依靠编译器魔术,而不是正确的事情),这种方法难以维护,使我们回到了C ++的糟糕日子。新手读者,请不要这样做。

–有远见的软件解决方案
13年8月6日在6:23



如果您要包含关于使变量可以是final,final的规则。然后,您实际上应该已经包括了使所有变量可以私有的私有的内容。

– Cruncher
2013年9月3日20:40在

@TofuBeer:不用担心。大多数Java开发人员倾向于过于over腐和挑剔。我相信即使Joshua Bloch编写了代码,他们也会选择(假设他们不知道是他)。个人品味就是个人品味;最终,CPU和JRE都不关心语法风格。

–水彩画
2013年9月20日22:38在

#5 楼

示例2的灵活性较差。如果添加另一个构造函数,则还需要记住在该构造函数中实例化该字段。只需直接实例化该字段,或在getter中的某个地方引入延迟加载。

如果实例化不仅需要简单的new,请使用初始化程序块。无论使用哪种构造函数,都将运行它。例如

public class A {
    private Properties properties;

    {
        try {
            properties = new Properties();
            properties.load(Thread.currentThread().getContextClassLoader().getResourceAsStream("file.properties"));
        } catch (IOException e) {
            throw new ConfigurationException("Failed to load properties file.", e); // It's a subclass of RuntimeException.
        }
    }

    // ...

}


#6 楼

我认为这几乎只是一个口味问题,只要初始化很简单并且不需要任何逻辑即可。

如果不使用初始化程序块,则构造函数方法会更脆弱,因为如果以后添加第二个构造函数而忘记在那里初始化b,则只会得到null b使用最后一个构造函数时。

有关Java初始化的更多详细信息,以及有关的解释,请参见http://java.sun.com/docs/books/tutorial/java/javaOO/initial.html。初始化块和其他未知的初始化功能)。

评论


那就是为什么你有DI和@Required :)

– Sujoy
2010年1月3日,12:39

是。我只是在描述两个示例之间的差异。

– Vinko Vrsalovic
2010年1月3日,13:12

可能会有大量的构造函数,这意味着您违反了单一职责原则,并且在设计中遇到了更大的问题。

–有远见的软件解决方案
13年8月6日在6:31

#7 楼

最好使用依赖项注入或惰性初始化,如在其他答案中已经详细说明的那样。

当您不希望或不能使用这些模式时,对于原始数据类型,有三种我能想到的令人信服的理由,为什么最好在构造函数之外初始化类属性:



避免重复=如果您有多个构造函数,或者何时使用需要添加更多内容,您将不必一遍又一遍地在所有构造函数主体中重复初始化;

提高了可读性=您可以一目了然地判断哪些变量必须从外部初始化该类;

减少的代码行=对于声明中进行的每次初始化,构造函数中的代码行都将减少。


#8 楼

我在回复中没有看到以下内容:

在声明时进行初始化的可能好处可能是在当今的IDE中,您可以很容易地跳转到变量的声明(通常是
Ctrl-<hover_over_the_variable>-<left_mouse_click>)在代码中的任何位置。然后,您立即看到该变量的值。否则,您必须“搜索”完成初始化的位置(主要是构造函数)。

这个优势当然是所有其他逻辑推理的第二要因,但是对于某些具有“特征”的人而言可能更重要。

#9 楼

两种方法都是可以接受的。请注意,在后一种情况下,如果存在另一个构造函数,则b=new B()可能不会初始化。将构造函数外部的初始化程序代码视为通用构造函数,然后执行该代码。

#10 楼

我认为示例2是可取的。我认为最好的做法是在构造函数外部声明并在构造函数中初始化。

#11 楼

第二个是延迟初始化的示例。第一个是更简单的初始化,它们本质上是相同的。

#12 楼

还有一个微妙的原因可以在构造函数之外进行初始化,这是以前没有人提到的(我必须说的很具体)。如果您使用UML工具从代码中生成类图(逆向工程),我相信大多数工具都会注意到示例1的初始化并将其转移到图上(如果您希望它显示初始值,例如我做)。他们不会从示例2中获取这些初始值。同样,这是一个非常具体的原因-如果您使用的是UML工具,但是一旦我了解到这一点,我便会尝试将所有默认值带到构造函数之外,除非前面提到,存在可能引发异常或复杂逻辑的问题。

#13 楼

第二个选项是可取的,因为允许在ctor中使用不同的逻辑进行类实例化,并使用ctors链接。例如,

class A {
    int b;

    // secondary ctor
    A(String b) {
         this(Integer.valueOf(b));
    }

    // primary ctor
    A(int b) {
         this.b = b;
    }
}


因此第二个选项更加灵活。

#14 楼

实际上,这是完全不同的:

声明在构造之前发生。假设如果在两个地方都初始化了变量(在这种情况下为b),则构造函数的初始化将替换在类级别完成的初始化。

因此在类级别声明变量,然后在构造函数中对其进行初始化。

#15 楼

    class MyClass extends FooClass {
    String a = null;

    public MyClass() {
        super();     // Superclass calls init();
    }

    @Override
    protected void init() {
        super.init();
        if (something)
            a = getStringYadaYada();
    }
}


关于上述内容,

String a = null;


可以避免空初始化,因为无论如何它都是默认的。
但是,如果您是需要另一个默认值
,然后,由于不受控制的初始化顺序,我将按以下步骤修复:

class MyClass extends FooClass 
{
    String a;
    {
        if( a==null ) a="my custom default value";
    }
    ...