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

92

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

public class Book
{

    private String title;
    private String isbn;

    public Book()
    {
      //nothing specified!
    }

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

    ...     

}

当未指定字段时,我该怎么办?到目前为止,我一直在类中使用默认值来确保字段永远不会为空,但这是“好”的做法吗?


这要看情况,你需要所有字段都包含一个值吗? - CodeMonkey
11
我不喜欢人们问问题却不接受答案。 - Anton Krug
9个回答

165

稍微简化的答案:

public class Book
{
    private final String title;

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

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

    ...
}

15
我觉得一个很好的经验法则是,所有的构造函数都应该经过一个共同的“瓶颈点”。 - cletus
2
另外,始终建议在每个构造函数中以this()或super()开始=8-) - Yuval
2
+1 用于构造函数链接,对于额外的分数,“title” 可能会被声明为 final,因为在 Book 对象的生命周期内它不太可能改变(不可变性真的很好!)。 - Andrzej Doyle
7
在构造函数中如果标题为null,建议抛出异常,这样可以保证在方法中它永远不会为null(这非常有帮助)。尽可能地养成这种好习惯(final+not-null)。 - Bill K
2
小心处理空值检查。http://misko.hevery.com/2009/02/09/to-assert-or-not-to-assert/ - Craig P. Motlin
显示剩余2条评论

43

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


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

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


如果你有多个构造函数,这是最好的想法,你可以轻松地扩展到新类型,并且错误率较低。 - dinsim
@Dinesh - 我同意,我在我的代码中到处都使用 Builders。我喜欢这种模式! - kgrad
在我看来,它就像是一种流畅接口(Fluent Interface)(http://www.codemonkeyism.com/archives/2007/10/10/fluent-interface-and-reflection-for-object-building-in-java/)。 - alepuzio
5
然而,如果你想扩展它的话,构建器可能会有些麻烦。(https://dev59.com/9HRC5IYBdhLWcg3wYf8p) - cdmckay

25
你需要指定类不变量,即实例的属性始终为真(例如,书的标题永远不会为空,狗的大小始终大于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被传递给了第二个构造函数。 - Tom Hawtin - tackline

14

您应该始终构造一个有效和合法的对象;如果您无法使用构造函数参数进行构造,则应使用构建器对象创建一个对象,在对象完成后才从构建器中释放该对象。

关于构造函数的使用:我总是尝试有一个基本构造函数,所有其他构造函数都通过省略参数来链式调用下一个逻辑构造函数,并以基本构造函数结束。因此:

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

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

SomeClass(String a, String b) {
    myA=a;
    myB=b;
    }
...
}
如果不可能的话,我尝试拥有一个私有的init()方法,所有构造函数都要延迟调用它。同时尽量保持构造函数和参数数量较少,最多5个作为指导方针。

是的,如果你的构造函数有超过五个参数,那么你应该考虑实现一个Builder对象... 更多细节请参阅《Effective Java》... - opensas

13

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

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

这是一个相当简单的解决方案,特别是与建造者模式(如Joshua Bloch的《Effective Java第二版》中所见 - 注意,四人帮的《设计模式》定义了一个完全不同的具有相同名称的设计模式,因此可能会有些混淆)相比。后者需要创建嵌套类、建造者对象等。

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

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

你可以在(已经提到的)Joshua Bloch的《Effective Java第二版》中阅读更多相关内容 - 这是所有开发人员工具箱中的重要工具,难怪它是该书第一章的主题。;-)

按照您的例子:

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);
    }

    ...
无论您选择哪种方式,都最好有一个主构造函数,它会将所有值都盲目地分配,即使只是被其他构造函数使用也是如此。}


7

一些通用的构造函数技巧:

  • 尝试将所有的初始化都集中在一个构造函数中,并从其他构造函数中调用它
    • 如果存在多个构造函数来模拟默认参数,则这样做效果很好
  • 从构造函数中永远不要调用非 final 方法
    • 私有方法在定义上是 final 的
    • 多态性可能会导致问题;您可能在子类初始化之前调用子类实现
    • 如果需要 "辅助" 方法,请确保将它们设置为私有或最终形式
  • 在对 super() 进行调用时要明确
    • 您会惊讶地发现,即使您没有显式编写它(假设您没有调用 this(...)),也会调用 super()
  • 了解构造函数的初始化顺序规则。基本上是:

    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());
    }
}

我将添加注释,描述上述代码为什么会按照这样的方式工作... 其中一些可能很明显;有些则不是...


创建B时,A的init()方法从未被调用(B对其进行了覆盖)。 - Scott Stanchfield
  1. 从A的构造函数调用了B的init()方法。
- Scott Stanchfield
B的init()在B的初始化程序之前被调用!y在init()中赋值后才被赋值为42。 - Scott Stanchfield
永远不要说永远 - 在某些情况下,从构造函数中调用受保护的方法恰恰是正确的做法,以便有意将一些构造细节延迟到子类中。 - Lawrence Dol
不,我会坚持“永远不”。如果你想将构造细节推迟到子类中,那就是子类构造函数的作用。从构造函数调用非final方法的问题在于子类尚未初始化 - 非常不安全! - Scott Stanchfield

3
如果一个字段是必填的或者有限制范围,考虑在构造函数中进行检查。
public Book(String title)
{
    if (title==null)
        throw new IllegalArgumentException("title can't be null");
    this.title = title;
}

1
你应该针对所有公共方法都这样做。始终检查参数,这样可以避免未来的一些麻烦。 - cdmckay

0
我会这样做:
公共类书籍 { private final String title; private final String isbn;
public Book(final String t, final String i) { if(t == null) { throw new IllegalArgumentException("t 不能为空"); }
if(i == null) { throw new IllegalArgumentException("i 不能为空"); }
title = t; isbn = i; } }
我在这里做出以下假设:
1)标题永远不会改变(因此标题是最终的) 2)ISBN 永远不会改变(因此 ISBN 是最终的) 3)没有标题和 ISBN 的书籍是无效的。
考虑一个学生类: public class Student { private final StudentID id; private String firstName; private String lastName;
public Student(final StudentID i, final String first, final String last) { if(i == null) { throw new IllegalArgumentException("i 不能为空"); }
if(first == null) { throw new IllegalArgumentException("first 不能为空"); }
if(last == null) { throw new IllegalArgumentException("last 不能为空"); }
id = i; firstName = first; lastName = last; } }

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

在决定要使用哪些构造函数时,您确实需要考虑什么是有意义的。很多时候,人们添加set/get方法是因为他们被教导这样做,但往往这是一个坏主意。

不可变类(即带有最终变量的类)比可变类更好。这本书:http://books.google.com/books?id=ZZOiqZQIbRMC&pg=PA97&sig=JgnunNhNb8MYDcx60Kq4IyHUC58#PPP1,M1(《Effective Java》)对不可变性进行了很好的讨论。请看第12和13条。


你为什么要将参数设为final? - Steve Kuo
最终参数使得你不能意外地将 t = title 这样的操作。 - TofuBeer

0

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