Лучший способ обработки нескольких конструкторов в Java - PullRequest
71 голосов
/ 24 февраля 2009

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

public class Book
{

    private String title;
    private String isbn;

    public Book()
    {
      //nothing specified!
    }

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

    ...     

}

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

Ответы [ 9 ]

136 голосов
/ 24 февраля 2009

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

public class Book
{
    private final String title;

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

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

    ...
}
34 голосов
/ 24 февраля 2009

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


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

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

19 голосов
/ 24 февраля 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
}

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

9 голосов
/ 24 февраля 2009

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

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

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

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

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

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

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

6 голосов
/ 24 февраля 2009

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

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

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

Общий поток в итоге составит:

  • пройти весь путь вверх по иерархии суперкласса до объекта
  • пока не сделано
    • поля инициализации
    • запустить конструктор тел
    • раскрывающийся подкласс

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

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

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

3 голосов
/ 24 февраля 2009

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

public Book(String title)
{
    if (title==null)
        throw new IllegalArgumentException("title can't be null");
    this.title = title;
}
2 голосов
/ 14 сентября 2017

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

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

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

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

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

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

По вашему примеру:

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

    ...

}

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

0 голосов
/ 24 февраля 2009

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

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

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

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

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.

...