在 Java 中处理多个构造函数的最佳方法

Posted

技术标签:

【中文标题】在 Java 中处理多个构造函数的最佳方法【英文标题】:Best way to handle multiple constructors in Java 【发布时间】:2010-10-09 13:46:00 【问题描述】:

我一直想知道在 Java 中处理多个构造函数的最佳(即最干净/最安全/最有效)的方法是什么?尤其是在一个或多个构造函数中没有指定所有字段时:

public class Book


    private String title;
    private String isbn;

    public Book()
    
      //nothing specified!
    

    public Book(String title)
    
      //only title!
    

    ...     


没有指定字段怎么办?到目前为止,我一直在类中使用默认值,以便字段永远不会为空,但这是一种“好”的做事方式吗?

【问题讨论】:

视情况而定,是否需要所有字段都包含值? 我不喜欢人们提出问题然后不接受答案。 【参考方案1】:

稍微简化的答案:

public class Book

    private final String title;

    public Book(String title)
    
      this.title = title;
    

    public Book()
    
      this("Default Title");
    

    ...

【讨论】:

+1 我发现所有构造函数都应该通过一个共同的“阻塞点”是一个很好的经验法则。 另外,总是建议用 this() 或 super() =8-) 开始每个构造函数 +1 用于构造函数链接 - 对于额外的标记,“title”可能被声明为 final,因为它在 Book 对象的生命周期内不太可能改变(不变性非常好!)。 如果 title 在构造函数中为 null,您可能还应该抛出异常,这样您就可以保证它在方法中永远不会为 null(这可能非常有用)。这是一个尽可能做的好习惯(final + not-null)。 小心空检查。 misko.hevery.com/2009/02/09/to-assert-or-not-to-assert【参考方案2】:

考虑使用 Builder 模式。它允许您设置参数的默认值并以清晰简洁的方式进行初始化。例如:


    Book b = new Book.Builder("Catcher in the Rye").Isbn("12345")
       .Weight("5 pounds").build();

编辑:它还消除了对具有不同签名的多个构造函数的需求,并且更具可读性。

【讨论】:

如果您有多个构造函数,这是最好的主意,您可以轻松扩展新类型并且不易出错 @Dinesh - 我同意,我在我的代码中使用了构建器。我喜欢这种模式! 在我看来,它就像一个流畅的界面 (codemonkeyism.com/archives/2007/10/10/…) 但是,如果您想扩展 Builder,它可能会有点麻烦 (***.com/questions/313729/…)。【参考方案3】:

您需要指定什么是类不变量,即对于类的实例始终为真的属性(例如,一本书的标题永远不会为空,或者狗的大小永远是> 0).

这些不变量应该在构造过程中建立,并在对象的生命周期中保留,这意味着方法不应破坏不变量。构造函数可以通过强制参数或设置默认值来设置这些不变量:

class Book 
    private String title; // not nullable
    private String isbn;  // nullable

    // Here we provide a default value, but we could also skip the 
    // parameterless constructor entirely, to force users of the class to
    // provide a title
    public Book()
    
        this("Untitled"); 
    

    public Book(String title) throws IllegalArgumentException
    
        if (title == null) 
            throw new IllegalArgumentException("Book title can't be null");
        this.title = title;
        // leave isbn without value
    
    // Constructor with title and isbn

但是,这些不变量的选择很大程度上取决于您正在编写的类、您将如何使用它等,因此您的问题没有明确的答案。

【讨论】:

你想检查 null 没有被传递给第二个构造函数。【参考方案4】:

另一个考虑,如果一个字段是必需的或有一个有限的范围,在构造函数中执行检查:

public Book(String title)

    if (title==null)
        throw new IllegalArgumentException("title can't be null");
    this.title = title;

【讨论】:

您应该为所有公共方法执行此操作。始终检查参数,它会为您省去一些麻烦。【参考方案5】:

一些通用的构造函数提示:

尝试将所有初始化集中在一个构造函数中,并从其他构造函数中调用它 如果存在多个构造函数来模拟默认参数,这会很有效 永远不要从构造函数调用非最终方法 根据定义,私有方法是最终的 多态可以杀死你;您最终可以在子类初始化之前调用子类实现 如果您需要“帮助”方法,请务必将它们设为私有或最终方法 在调用 super() 时要明确 您会惊讶于有多少 Java 程序员没有意识到 super() 被调用,即使您没有明确编写它(假设您没有调用 this(...))李>

了解构造函数的初始化规则顺序。基本上是:

    this(...) 如果存在(只是移动到另一个构造函数) 调用 super(...) [如果不显式,则隐式调用 super()] (递归地使用这些规则构造超类) 通过声明初始化字段 当前构造函数的运行体 返回之前的构造函数(如果你遇到过 this(...) 调用)

整个流程最终是:

一直向上移动超类层次结构到 Object 尚未完成时 初始化字段 运行构造函数体 下拉到子类

对于邪恶的一个很好的例子,试着弄清楚下面会打印什么,然后运行它

package com.javadude.sample;

/** THIS IS REALLY EVIL CODE! BEWARE!!! */
class A 
    private int x = 10;
    public A() 
        init();
    
    protected void init() 
        x = 20;
    
    public int getX() 
        return x;
    


class B extends A 
    private int y = 42;
    protected void init() 
        y = getX();
    
    public int getY() 
        return y;
    


public class Test 
    public static void main(String[] args) 
        B b = new B();
        System.out.println("x=" + b.getX());
        System.out.println("y=" + b.getY());
    

我将添加 cmets 来描述为什么上面的工作原理......其中一些可能是显而易见的;有些不是……

【讨论】:

1.创建 B 时永远不会调用 A 的 init() 方法(B 覆盖它) 2. B的init()方法是从A的构造函数中调用的 B 的 init() 被调用 B 的初始化器被调用!。 y 被分配给 42 之后 它在 init() 中分配 永远不要说永远 - 有些地方从构造函数调用受保护的方法是完全正确的做法,以便故意将一些构造细节推迟到子类中。 不——我会坚持“从不”。如果您想将构造细节推迟到子类,那就是子类构造函数的用途。从构造函数调用非final的问题是子类还没有被初始化——只是不安全!【参考方案6】:

我会做以下事情:

公共课本 私有最终字符串标题; 私有最终字符串 isbn; 公共书(最终字符串 t,最终字符串 i) 如果(t == 空) throw new IllegalArgumentException("t 不能为 null"); 如果(我 == 空) throw new IllegalArgumentException("我不能为空"); 标题 = t; isbn = 我;

我在这里假设:

1) 标题永远不会改变(因此标题是最终的) 2) isbn 永远不会改变(因此 isbn 是最终的) 3) 没有书名和 isbn 的书是无效的。

考虑一个学生类:

公开课学生 私人最终学生ID; 私人字符串名; 私人字符串姓氏; 公共学生(最终学生ID i, 最终字符串优先, 最后一个字符串最后) 如果(我 == 空) throw new IllegalArgumentException("我不能为空"); 如果(第一个 == 空) throw new IllegalArgumentException("first cannot be null"); 如果(最后一个 == 空) throw new IllegalArgumentException("last 不能为 null"); 身份证=我; 名字=第一; 姓氏 = 最后一个;

必须创建一个带有 id、名字和姓氏的 Student。学生证永远不会改变,但一个人的姓氏和名字可以改变(结婚、因赌输而改名等......)。

在决定拥有哪些构造函数时,您真正需要考虑拥有什么是有意义的。人们经常添加 set/get 方法,因为他们被教导要这样做 - 但通常这是一个坏主意。

不可变类(即具有最终变量的类)比可变类要好得多。这本书:http://books.google.com/books?id=ZZOiqZQIbRMC&pg=PA97&sig=JgnunNhNb8MYDcx60Kq4IyHUC58#PPP1,M1(Effective Java)对不变性有很好的讨论。查看第 12 和 13 项。

【讨论】:

为什么要把参数定为final? 最终参数使您不会意外地做 t = title 例如。【参考方案7】:

一些人建议添加一个空检查。有时这是正确的做法,但并非总是如此。查看这篇出色的文章,了解您为什么要跳过它。

http://misko.hevery.com/2009/02/09/to-assert-or-not-to-assert/

【讨论】:

【参考方案8】:

您应该始终构造一个有效且合法的对象;如果你不能使用构造器参数,你应该使用构建器对象来创建一个,只有在对象完成时才从构建器中释放对象。

关于构造函数的使用问题:我总是尝试使用一个所有其他构造函数都遵循的基本构造函数,通过“省略”参数链接到下一个逻辑构造函数并在基本构造函数处结束。所以:

class SomeClass

SomeClass() 
    this("DefaultA");
    

SomeClass(String a) 
    this(a,"DefaultB");
    

SomeClass(String a, String b) 
    myA=a;
    myB=b;
    
...

如果这是不可能的,那么我尝试拥有一个所有构造函数都遵循的私有 init() 方法。

并保持构造函数和参数的数量较少 - 作为指导,每个最多 5 个。

【讨论】:

是的,您的构造函数中有五个以上的参数,您应该评估实现构建器对象等...有关更多详细信息,请参阅有效 Java...【参考方案9】:

可能值得考虑使用静态工厂方法而不是构造函数。

我说的是而不是,但显然你不能替换构造函数。但是,您可以做的是将构造函数隐藏在静态工厂方法后面。这样,我们将静态工厂方法作为类 API 的一部分发布,但同时隐藏构造函数,使其成为私有或包私有。

这是一个相当简单的解决方案,尤其是与 Builder 模式相比(如 Joshua Bloch 的 Effective Java 2nd Edition 中所见——请注意,Gang of Four 的 Design Patterns 定义了一个具有相同名称的完全不同的设计模式,因此可能会有些混乱)这意味着创建嵌套类、构建器对象等。

这种方法在您和您的客户之间增加了一个额外的抽象层,从而加强了封装性并使更改变得更容易。它还为您提供实例控制——因为对象是在类中实例化的,所以您而不是客户端决定何时以及如何创建这些对象。

最后,它使测试变得更容易——提供了一个愚蠢的构造函数,它只是将值分配给字段,而不执行任何逻辑或验证,它允许您将无效状态引入系统以测试它的行为和反应方式.如果您在构造函数中验证数据,您将无法做到这一点。

您可以在(已经提到的)Joshua Bloch 的 Effective Java 2nd Edition 中阅读更多相关信息——它是所有开发人员工具箱中的重要工具,难怪它是本书第一章的主题. ;-)

按照你的例子:

public class Book 

    private static final String DEFAULT_TITLE = "The Importance of Being Ernest";

    private final String title;
    private final String isbn;

    private Book(String title, String isbn) 
        this.title = title;
        this.isbn = isbn;
    

    public static Book createBook(String title, String isbn) 
        return new Book(title, isbn);
    

    public static Book createBookWithDefaultTitle(String isbn) 
        return new Book(DEFAULT_TITLE, isbn);
    

    ...

无论您选择哪种方式,最好有一个 ma​​in 构造函数,它只是盲目地分配所有值,即使它只是被另一个构造函数使用。

【讨论】:

以上是关于在 Java 中处理多个构造函数的最佳方法的主要内容,如果未能解决你的问题,请参考以下文章

c++ 如何在构造函数中启动一个线程,从命名管道读取数据?

如何在 Java 中从另一个构造函数调用一个构造函数?

重载构造函数的最佳方法是什么?

java 为什么有时一个类有多个构造函数

Java中的构造函数重载 - 最佳实践

Java 构造函数