89

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

public class Book
{

    private String title;
    private String isbn;

    public Book()
    {
      //nothing specified!
    }

    public Book(String title)
    {
      //only title!
    }

    ...     

}

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

4

9 回答 9

160

一个稍微简化的答案:

public class Book
{
    private final String title;

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

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

    ...
}
于 2009-02-24T14:14:03.467 回答
42

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


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

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

于 2009-02-24T14:19:05.187 回答
23

You need to specify what are the class invariants, i.e. properties which will always be true for an instance of the class (for example, the title of a book will never be null, or the size of a dog will always be > 0).

These invariants should be established during construction, and be preserved along the lifetime of the object, which means that methods shall not break the invariants. The constructors can set these invariants either by having compulsory arguments, or by setting default values:

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
}

However, the choice of these invariants highly depends on the class you're writing, how you'll use it, etc., so there's no definitive answer to your question.

于 2009-02-24T14:36:18.217 回答
14

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

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

class SomeClass
{
SomeClass() {
    this("DefaultA");
    }

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

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

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

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

于 2009-02-24T17:57:18.950 回答
10

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

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

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

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

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

您可以在(已经提到的)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);
    }

    ...

}

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

于 2017-09-14T16:19:55.307 回答
7

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

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

    1. this(...) 如果存在(只需移动到另一个构造函数)
    2. 调用 super(...) [如果不显式,则隐式调用 super()]
    3. (递归地使用这些规则构造超类)
    4. 通过声明初始化字段
    5. 运行当前构造函数的主体
    6. 返回之前的构造函数(如果你遇到过 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());
    }
}

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

于 2009-02-24T15:56:55.527 回答
3

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

public Book(String title)
{
    if (title==null)
        throw new IllegalArgumentException("title can't be null");
    this.title = title;
}
于 2009-02-24T15:32:42.917 回答
0

我会做以下事情:

公共课本
{
    私有最终字符串标题;
    私有最终字符串 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 项。

于 2009-02-24T17:51:49.230 回答
0

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

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

于 2009-02-24T17:53:50.957 回答