【问题标题】:Best way to handle multiple constructors in Java在 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!
    }

    ...     

}

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

【问题讨论】:

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

标签: java constructor field


【解决方案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)。
【解决方案2】:

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


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

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

【讨论】:

【解决方案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(...))李>
  • 了解构造函数的初始化规则顺序。基本上是:

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

我将添加 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 构造函数,它只是盲目地分配所有值,即使它只是被另一个构造函数使用。

    【讨论】:

      猜你喜欢
      • 2010-12-14
      • 1970-01-01
      • 1970-01-01
      • 2015-07-28
      • 2020-07-02
      • 1970-01-01
      • 2020-02-11
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多