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

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

public class Book
{

    private String title;
    private String isbn;

    public Book()
    {
      //nothing specified!
    }

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

    ...     

}

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


person Community    schedule 24.02.2009    source источник
comment
Это зависит от того, нужно ли вам, чтобы все поля содержали значение?   -  person CodeMonkey    schedule 24.02.2009
comment
Я не люблю, когда люди задают вопросы, а потом не принимают ответы.   -  person Anton Krug    schedule 19.11.2015


Ответы (9)


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

public class Book
{
    private final String title;

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

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

    ...
}
person Craig P. Motlin    schedule 24.02.2009
comment
+1 Я считаю хорошим эмпирическим правилом, что все ваши конструкторы должны проходить через общую точку удушья. - person cletus; 24.02.2009
comment
Кроме того, всегда рекомендуется начинать каждый конструктор с this() или super() =8-) - person Yuval; 24.02.2009
comment
+1 за цепочку конструкторов - для дополнительных оценок заголовок, вероятно, можно объявить окончательным, поскольку он вряд ли изменится в течение жизни объекта Book (неизменяемость - это действительно хорошо!). - person Andrzej Doyle; 24.02.2009
comment
Вероятно, вам также следует генерировать исключение, если заголовок имеет значение null в конструкторе, таким образом вы можете гарантировать, что он НИКОГДА не будет иметь значение null в методе (что может быть чрезвычайно полезным). Это хорошая привычка делать по возможности (final + not-null). - person Bill K; 24.02.2009
comment
Будьте осторожны с нулевыми проверками. misko.hevery.com/2009/02 /09/утверждать или не утверждать - person Craig P. Motlin; 24.02.2009
comment
Пост миско не тот, с которым я бы согласился. Я бы предпочел отказаться от тестирования hardshot, чтобы убедиться, что производственный код лучше. - person TofuBeer; 24.02.2009
comment
Хорошо, но не всегда будет достаточно: у вас может быть два необязательных аргумента для четырех конструкторов, то есть A AB AC ABC. В этом случае вы не можете вернуть все к меньшему конструктору: либо ABC-->AB-->A (и вы оставляете AC), либо ABC-->AC-->A (и вы оставляете AB). - person o0'.; 23.09.2010

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


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

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

person kgrad    schedule 24.02.2009
comment
Это лучшая идея, если у вас есть несколько конструкторов, вы можете легко расширяться для новых типов и менее подвержены ошибкам. - person dinsim; 24.02.2009
comment
@Dinesh - я согласен, я использую Builders во всем своем коде. Я люблю узор! - person kgrad; 24.02.2009
comment
На мой взгляд, это как Fluent Interface (codemonkeyism.com/archives/2007/10/10/) - person alepuzio; 24.02.2009
comment
Однако Builder может быть немного неприятным, если вы хотите его расширить (stackoverflow.com/questions/313729/). - person cdmckay; 24.02.2009

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

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

person Luc Touraille    schedule 24.02.2009
comment
Вы хотите проверить, что null не передается этому второму конструктору. - person Tom Hawtin - tackline; 24.02.2009

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

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

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

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

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

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

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

person Lawrence Dol    schedule 24.02.2009
comment
да, более пяти параметров в вашем конструкторе, и вы должны оценить реализацию объекта компоновщика... более подробную информацию см. в разделе Эффективная Java... - person opensas; 02.09.2011

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

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

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

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

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

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

person Krzysiek Przygudzki    schedule 14.09.2017

Несколько общих советов по конструктору:

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

    1. this(...) if present (just move to another constructor)
    2. вызовите super(...) [если не указано явно, вызовите super() неявно]
    3. (рекурсивно построить суперкласс, используя эти правила)
    4. инициализировать поля через их объявления
    5. запустить тело текущего конструктора
    6. вернуться к предыдущим конструкторам (если вы столкнулись с вызовами this(...) )

Общий поток заканчивается следующим образом:

  • переместиться полностью вверх по иерархии суперкласса к объекту
  • while not done
    • init fields
    • запустить тела конструктора
    • выпадающий в подкласс

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

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

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

person Scott Stanchfield    schedule 24.02.2009
comment
1. Метод init() A никогда не вызывается при создании B (B переопределяет его) - person Scott Stanchfield; 24.02.2009
comment
2. Метод B init() вызывается из конструктора A - person Scott Stanchfield; 24.02.2009
comment
init() вызывается до инициализаторов B!. y присваивается 42 после присваивания в init() - person Scott Stanchfield; 24.02.2009
comment
Никогда не говори никогда - есть места, где вызов защищенных методов из конструктора является правильным решением, чтобы преднамеренно перенести некоторые особенности построения в подклассы. - person Lawrence Dol; 24.02.2009
comment
Нет - я не буду придерживаться никогда. Если вы хотите отложить особенности построения до подклассов, для этого и нужны конструкторы подклассов. Проблема с вызовом non-final из конструкторов заключается в том, что подкласс еще не инициализирован — просто небезопасно! - person Scott Stanchfield; 25.02.2009

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

public Book(String title)
{
    if (title==null)
        throw new IllegalArgumentException("title can't be null");
    this.title = title;
}
person Steve Kuo    schedule 24.02.2009
comment
Вы должны делать это для всех своих общедоступных методов. Всегда проверяйте аргументы, это избавит вас от головной боли в будущем. - person cdmckay; 24.02.2009

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

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) недопустимо иметь книгу без названия и ISBN.

Рассмотрим класс Student:

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 (Effective Java) содержит хорошее обсуждение неизменности. Посмотрите на пункты 12 и 13.

person TofuBeer    schedule 24.02.2009
comment
Почему вы делаете параметры окончательными? - person Steve Kuo; 24.02.2009
comment
Окончательные параметры делают так, что вы не можете случайно сделать, например, t = title. - person TofuBeer; 24.02.2009

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

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

person Craig P. Motlin    schedule 24.02.2009