Лучший способ обработки нескольких конструкторов в Java

Мне было интересно, какой лучший (то есть самый чистый/безопасный/самый эффективный) способ обработки нескольких конструкторов в 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");
    }

    ...
}

Ответ 2

Рассмотрим использование шаблона Builder. Он позволяет вам устанавливать значения по умолчанию для ваших параметров и инициализировать четким и сжатым способом. Например:


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

Изменить: он также устраняет необходимость в нескольких конструкторах с различными сигнатурами и является более читаемым.

Ответ 3

Вам нужно указать, что такое инварианты класса, то есть свойства, которые всегда будут истинны для экземпляра класса (например, название книги никогда не будет равно null или размер собаки всегда будет > 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
}

Однако выбор этих инвариантов сильно зависит от класса, который вы пишете, как вы его используете и т.д., поэтому нет окончательного ответа на ваш вопрос.

Ответ 4

Вы всегда должны создавать действительный и законный объект; и если вы не можете использовать конструкторы-конструкторы, вы должны использовать объект-конструктор для его создания, только освобождая объект от строителя, когда объект будет завершен.

В вопросе о использовании конструктора: я всегда стараюсь иметь один базовый конструктор, который все остальные откладывают, цепляясь с "опущенными" параметрами следующему логическому конструктору и заканчивая базовым конструктором. Итак:

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

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

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

Если это невозможно, я пытаюсь получить частный метод init(), который все конструкторы откладывают.

И сохраните количество конструкторов и параметров малым - максимум 5 из них в качестве ориентира.

Ответ 5

Некоторые общие советы конструктора:

  • Попробуйте сфокусировать всю инициализацию в одном конструкторе и вызвать его из других конструкторов
    • Это хорошо работает, если существуют несколько конструкторов для имитации параметров по умолчанию
  • Никогда не вызывать неконтактный метод из конструктора
    • Частные методы окончательны по определению
    • Полиморфизм может убить вас здесь; вы можете в конечном итоге вызвать реализацию подкласса до инициализации подкласса
    • Если вам нужны "вспомогательные" методы, обязательно сделайте их закрытыми или окончательными.
  • Быть явным в ваших вызовах super()
    • Вы были бы удивлены тем, сколько программистов Java не понимают, что вызов super() вызывается, даже если вы явно не записываете его (если у вас нет вызова этого (...))
  • Знайте порядок правил инициализации для конструкторов. Это в основном:

    • это (...), если присутствует (просто перейдите к другому конструктору)
    • вызов super (...) [если не явный, вызов super() неявно]
    • (постройте суперкласс, используя эти правила рекурсивно)
    • инициализировать поля через свои объявления
    • запустить тело текущего конструктора
    • вернуться к предыдущим конструкторам (если вы столкнулись с этими (...) вызовами)

Общий поток заканчивается:

  • переместить все иерархию суперкласса в 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());
    }
}

Я добавлю комментарии, описывающие, почему это работает так, как есть. Некоторые из них могут быть очевидны; некоторые не...

Ответ 6

Другое соображение, если поле требуется или имеет ограниченный диапазон, выполните проверку конструктора:

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

Ответ 7

Я бы сделал следующее:

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

Я делаю здесь предположение, что:

1) название никогда не изменится (следовательно, название окончательно) 2) isbn никогда не изменится (следовательно, isbn является окончательным) 3), что недопустимо иметь книгу без названия и изобна.

Рассмотрим класс Стьюдента:

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

Там Студент должен быть создан с идентификатором, именем и фамилией. Идентификатор студента никогда не может измениться, но фамилия и имя человека могут измениться (жениться, изменить имя из-за потери ставки и т.д.).

Когда вы решаете, какие конструкторы должны иметь вам действительно нужно думать о том, что имеет смысл иметь. Все часто люди добавляют методы set/get, потому что их учат - но очень часто это плохая идея.

Неизменяемые классы намного лучше иметь (то есть классы с конечными переменными) над изменчивыми. Эта книга: http://books.google.com/books?id=ZZOiqZQIbRMC&pg=PA97&sig=JgnunNhNb8MYDcx60Kq4IyHUC58#PPP1,M1 (Эффективная Java) имеет хорошее обсуждение неизменности. Посмотрите на пункты 12 и 13.

Ответ 8

Несколько человек рекомендовали добавить нулевую проверку. Иногда это правильно, но не всегда. Ознакомьтесь с этой замечательной статьей, в которой показано, почему вы пропустили ее.

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

Ответ 9

Возможно, стоит рассмотреть использование статического метода factory вместо конструктора.

Я говорю вместо этого, но, очевидно, вы не можете заменить конструктор. Однако вы можете скрыть конструктор за статическим factory методом. Таким образом, мы публикуем статический метод factory как часть 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);
    }

    ...

}

Какой бы способ вы ни выбрали, хорошая практика состоит в том, чтобы иметь конструктор main, который просто слепо присваивает все значения, даже если он просто используется другими конструкторами.