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

Мне было интересно, какой лучший (т.е. самый чистый / самый безопасный / самый эффективный) способ обработки нескольких конструкторов в Java? Особенно, когда в одном или нескольких конструкторах указаны не все поля:

public class Book
{

    private String title;
    private String isbn;

    public Book()
    {
      //nothing specified!
    }

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

    ...     

}

Что делать, если поля не указаны? До сих пор я использовал в классе значения по умолчанию, чтобы поле никогда не было нулевым, но является ли это «хорошим» способом делать что-то?

Ответов (9)

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

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

Немного упрощенный ответ:

public class Book
{
    private final String title;

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

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

    ...
}

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

По вопросу использования конструктора: я всегда стараюсь иметь один базовый конструктор, которому подчиняются все остальные, соединяясь с «пропущенными» параметрами до следующего логического конструктора и заканчивая базовым конструктором. Так:

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

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

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

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

И старайтесь, чтобы количество конструкторов и параметров было небольшим - максимум 5 каждого в качестве ориентира.

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

Я говорю вместо этого , но, очевидно, вы не можете заменить конструктор. Однако вы можете спрятать конструктор за статическим фабричным методом. Таким образом, мы публикуем статический фабричный метод как часть API класса, но в то же время мы скрываем конструктор, делая его закрытым или закрытым для пакета.

Это достаточно простое решение, особенно по сравнению с шаблоном Builder (как показано в Joshua Bloch's Effective Java 2nd Edition - будьте осторожны, шаблоны проектирования Gang of Four определяют совершенно другой шаблон проектирования с тем же именем, так что это может немного сбивать с толку), подразумевает создание вложенного класса, объекта-строителя и т. д.

Такой подход добавляет дополнительный уровень абстракции между вами и вашим клиентом, усиливая инкапсуляцию и упрощая внесение изменений в будущем. Это также дает вам управление экземпляром - поскольку объекты создаются внутри класса, вы, а не клиент, решаете, когда и как эти объекты создаются.

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

Вы можете больше узнать об этом в (уже упомянутом) Эффективном Java 2-е издание Джошуа Блоха - это важный инструмент во всех наборах инструментов разработчика, и неудивительно, что он является предметом первой главы книги. ;-)

Следуя вашему примеру:

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

    ...

}

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

Рассмотрите возможность использования паттерна Строитель. Это позволяет вам установить значения по умолчанию для ваших параметров и инициализировать их в ясной и лаконичной форме. Например:


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

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

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

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

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

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

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

  • Попробуйте сосредоточить всю инициализацию в одном конструкторе и вызвать его из других конструкторов.
    • Это хорошо работает, если существует несколько конструкторов для имитации параметров по умолчанию.
  • Никогда не вызывайте неокончательный метод из конструктора
    • Частные методы окончательны по определению
    • Здесь полиморфизм может вас убить; вы можете вызвать реализацию подкласса до того, как подкласс был инициализирован
    • Если вам нужны «вспомогательные» методы, обязательно сделайте их закрытыми или окончательными.
  • Будьте откровенны в своих вызовах 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());
    }
}

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

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

общедоступная книга класса
{
    частный финальный строковый заголовок;
    закрытая конечная строка isbn;

    публичная книга (последняя строка t, последняя строка i)
    {
        если (t == нуль)
        {
            выбросить новое исключение IllegalArgumentException («t не может быть нулевым»);
        }

        если (я == нуль)
        {
            выбросить новое исключение IllegalArgumentException («я не могу быть нулевым»);
        }

        title = t;
        isbn = i;
    }
}

Я предполагаю, что:

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

Рассмотрим студенческий класс:

общественный класс Студент
{
    частный финальный идентификатор StudentID;
    частная строка firstName;
    private String lastName;

    public Student (final StudentID i,
                   последняя строка сначала,
                   последняя строка последняя)
    {
        если (я == нуль)
        {
            выбросить новое исключение IllegalArgumentException («я не могу быть нулевым»); 
        }

        если (первый == нуль)
        {
            выбросить новое исключение IllegalArgumentException («первое не может быть нулевым»); 
        }

        если (последний == нуль)
        {
            выбросить новое исключение IllegalArgumentException («последнее не может быть нулевым»); 
        }

        id = я;
        firstName = первое;
        lastName = last;
    }
}

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

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

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