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

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

public class Book
{


private String title;
private String isbn;


public Book()
{
//nothing specified!
}


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


...


}

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

233999 次浏览

A slightly simplified answer:

public class Book
{
private final String title;


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


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


...
}

Consider using the Builder pattern. It allows for you to set default values on your parameters and initialize in a clear and concise way. For example:


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

Edit: It also removes the need for multiple constructors with different signatures and is way more readable.

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.

Another consideration, if a field is required or has a limited range, perform the check in the constructor:

public Book(String title)
{
if (title==null)
throw new IllegalArgumentException("title can't be null");
this.title = title;
}

Some general constructor tips:

  • Try to focus all initialization in a single constructor and call it from the other constructors
    • This works well if multiple constructors exist to simulate default parameters
  • Never call a non-final method from a constructor
    • Private methods are final by definition
    • Polymorphism can kill you here; you can end up calling a subclass implementation before the subclass has been initialized
    • If you need "helper" methods, be sure to make them private or final
  • Be explicit in your calls to super()
    • You would be surprised at how many Java programmers don't realize that super() is called even if you don't explicitly write it (assuming you don't have a call to this(...) )
  • Know the order of initialization rules for constructors. It's basically:

    1. this(...) if present (just move to another constructor)
    2. call super(...) [if not explicit, call super() implicitly]
    3. (construct superclass using these rules recursively)
    4. initialize fields via their declarations
    5. run body of current constructor
    6. return to previous constructors (if you had encountered this(...) calls)

The overall flow ends up being:

  • move all the way up the superclass hierarchy to Object
  • while not done
    • init fields
    • run constructor bodies
    • drop down to subclass

For a nice example of evil, try figuring out what the following will print, then run it

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

I'll add comments describing why the above works as it does... Some of it may be obvious; some is not...

I would do the following:

public class Book
{
private final String title;
private final String isbn;


public Book(final String t, final String i)
{
if(t == null)
{
throw new IllegalArgumentException("t cannot be null");
}


if(i == null)
{
throw new IllegalArgumentException("i cannot be null");
}


title = t;
isbn  = i;
}
}

I am making the assumption here that:

1) the title will never change (hence title is final) 2) the isbn will never change (hence isbn is final) 3) that it is not valid to have a book without both a title and an isbn.

Consider a Student class:

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 cannot be null");
}


if(first == null)
{
throw new IllegalArgumentException("first cannot be null");
}


if(last == null)
{
throw new IllegalArgumentException("last cannot be null");
}


id        = i;
firstName = first;
lastName  = last;
}
}

There a Student must be created with an id, a first name, and a last name. The student ID can never change, but a persons last and first name can change (get married, changes name due to losing a bet, etc...).

When deciding what constrructors to have you really need to think about what makes sense to have. All to often people add set/get methods because they are taught to - but very often it is a bad idea.

Immutable classes are much better to have (that is classes with final variables) over mutable ones. This book: http://books.google.com/books?id=ZZOiqZQIbRMC&pg=PA97&sig=JgnunNhNb8MYDcx60Kq4IyHUC58#PPP1,M1 (Effective Java) has a good discussion on immutability. Look at items 12 and 13.

Several people have recommended adding a null check. Sometimes that's the right thing to do, but not always. Check out this excellent article showing why you'd skip it.

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

You should always construct a valid and legitimate object; and if you can't using constructor parms, you should use a builder object to create one, only releasing the object from the builder when the object is complete.

On the question of constructor use: I always try to have one base constructor that all others defer to, chaining through with "omitted" parameters to the next logical constructor and ending at the base constructor. So:

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


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


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

If this is not possible, then I try to have an private init() method that all constructors defer to.

And keep the number of constructors and parameters small - a max of 5 of each as a guideline.

It might be worth considering the use of a static factory method instead of constructor.

I'm saying instead, but obviously you can't replace the constructor. What you can do, though, is hide the constructor behind a static factory method. This way, we publish the static factory method as a part of the class API but at the same time we hide the constructor making it private or package private.

It's a reasonably simple solution, especially in comparison with the Builder pattern (as seen in Joshua Bloch's Effective Java 2nd Edition – beware, Gang of Four's Design Patterns define a completely different design pattern with the same name, so that might be slightly confusing) that implies creating a nested class, a builder object, etc.

This approach adds an extra layer of abstraction between you and your client, strengthening encapsulation and making changes down the road easier. It also gives you instance-control – since the objects are instantiated inside the class, you and not the client decide when and how these objects are created.

Finally, it makes testing easier – providing a dumb constructor, that just assigns the values to the fields, without performing any logic or validation, it allows you to introduce invalid state into your system to test how it behaves and reacts to that. You won't be able to do that if you're validating data in the constructor.

You can read much more about that in (already mentioned) Joshua Bloch's Effective Java 2nd Edition – it's an important tool in all developer's toolboxes and no wonder it's the subject of the 1st chapter of the book. ;-)

Following your example:

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


...

}

Whichever way you choose, it's a good practice to have one main constructor, that just blindly assigns all the values, even if it's just used by another constructors.