Формирование объектов

From AsIsWiki
Jump to: navigation, search
Форум

Назад | Оглавление | Дальше


Contents

Перегрузка

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

В классе GregorianCalendar предусмотрено несколько конструкторов:

GregorianCalendar today = new GregorianCalendar();

GregorianCalendar deadline = new GregorianCalendar(2099, Calendar.DECEMBER, 31);

...

Такая возможность называется overloading - перегрузка. О перегрузке говорят в том случае, если несколько методов (или конструкторов) имеют одинаковое имя, но разные параметры.

Сравнивая типы параметров, описанных в заголовках методов, с типами значений, указанных в вызове, компилятор решает, какой метод вызвать. Если ни один метод не соответствует вызову, или вызову соответствует несколько вариантов, возникает ошибка компиляции. Этот процесс называется overloading resolution - разрешение перегрузки.

В Java можно перегрузить не только конструкторы, но и любой метод. Чтобы полностью описать метод, нужно указать его имя и типы параметров. Подобное написание называется signature - сигнатура метода. Например, в классе String есть четыре общедоступных метода indexOf(). Они имеют следующие сигнатуры:

indexOf(int)

indexOf(int, int)

indexOf(String)

indexOf(String, int)

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


Инициализация полей по умолчанию

Если значение поля в конструкторе не задано явно, ему автоматически присваивается значение по умолчанию: числам - нули, логическим переменным - false, а ссылкам на объект - null. Однако полагаться на действия по умолчанию считается плохим стилем. Если поля инициализируются неявно, программа становится менее понятной.

Между полями и локальными переменными есть существенная разница. Локальные переменные всегда должны явно инициализроваться в методе.

В качестве примера рассмотрим класс Employee. Допустим, что в конструкторе значения некоторых полей не заданы. По умолчанию, поле salary инициализируется нулем, а поля name и hireDay - ссылкой null. Однако, при вызове методов getName() или getHireDay() ссылка null может оказаться совершенно нежелательной:

Date d = harry.getHireDay();

calendar.setTime(h);  // если h = null, генерируется исключение


Конструктор по умолчанию

Default constructor (конструктор по умолчанию) не имеет параметров. Например, конструктор по умолчанию для класса Employee имеет следующий вид:

public Employee() {
    name = "";
    salary = 0;
    hireDay = new Date();
}

Если в классе не определены конструкторы, то вызывается конструктор по умолчанию. Этот конструктор присваивает всем полям экземпляра значения по умолчанию: числовые поля обнулятся, логические поля получат значение false, а ссылки на объекты - null.

Если в классе есть конструктор с параметрами, например:

Employee(String name, double salary, int y, int m, int d)

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

Employee e = new Employee();

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

public ClassName() {
    Filed1 = "some expression";
    Filed2 = 10;
    Field3 = true;
}

Если значения по умолчанию устраивают, то можно упростить конструктор:

public ClassName() {
}


Явная инициализация полей

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

Каждому полю можно присвоить значение в определении класса:

class Employee {
    ...
    private String name = "";
}

Поле инициализируется до вызова конструктора. Это особенно важно, когда требуется, чтобы поле имело конкретное значение независимо от вызова конструктора.

Для инициализации поля можно использовать метод:

class Employee {
  
    static int assignId() {
        int r = nextId;
        nextId++;
        return r;
    }
    ...
    private int id = assignId();
}

В C++ нельзя инициализировать поля экземпляра непосредственно в описании класса. Значения всех полей должны задаваться в конструкторе. Но в C++ есть синтаксическая конструкция, называемая initializer list (список инициализации):

Employee::Employee(String n, double s, int y, int m, int d)  // C++
: name(n),
  salary(s),
  hireDay(y, m, d) {
}

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


Имена параметров

Обычно в качестве имен параметров используются отдельные буквы.

public Employee(String n, double s) {
    name = n;
    salary = s;
}

Лучше использовать осмысленные имена параметров с некоторым префиксом:

public Employee(String aName, double aSalary) {
    name = aName;
    salary = aSalary;
}

Можно использовать способность параметров маскировать (shadow) поля экземпляра с одинаковыми именами. Например, если вызвать метод с параметром salary, то переменная будет ссылаться на параметр, а не на поле экземпляра. Для доступа к полю используется выражение this.salary. Ключевое слово this - неявный параметр, т.е. создаваемый объект:

public Employee(String name, double salary) {
    this.name = name;
    this.salary = salary;
}

В C++ к именам полей обычно добавляют префиксы, например: _salary, mSalary, xSalary. Разработчики на Java обычно так не поступают.


Вызов одного конструктора из другого

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

public Employee(double s) {
    // Вызов конструктора Employee(String, double)
    this("Employee " + nextId, s);
    nextId++;
}

Если используется выражение new Employee(60000), то конструктор Employee(double) вызывает конструктор Employee(String, double).

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

Объект this в Java идентичен указателю this в C++. Однако в C++ невозможно вызвать один конструктор из другого. Для реализации общего кода инициализации объекта в C++, создается отдельный метод.


Инициализационные блоки

Ранее рассматривалось два способа инициализации поля:

  • Инициализация значения поля в конструкторе.
  • Присвоение значения поля при объявлении.

Кроме того, можно использовать initialization block - инициализационный блок. Такой блок выполняется каждый раз, когда создается объект данного класса:

class Employee {

    public Employee(String n, double s) {
        name = n;
        salary = s;
    }
  
    public Employee() {
        name = "";
        salary = 0;
    }
    ...
    private static int nextId;
  
    private int id;
    private String name;
    private double salary;
    ...
    // initialization block
    {
        id = nextId;
        nextId++;
    }
}

Инициализационный блок всегда выполняется первым, а тело конструктора - после него. Поэтому, начальное значение поля id задается в инициализационном блоке объекта.

Этот механизм используется крайне редко. Гораздо чаще применяются более понятные способы задания начальных значений полей.

В инициализационном блоке допустимо обращение к полям, определения которых следуют после него. Несмотря на это, поступать так не рекомендуется во избежание возникновения циклических определений. Конкретные правила изложены в разделе 8.3.2.3 спецификации Java. Поскольку правила достаточно сложны и учесть их в реализации компилятора крайне трудно, рекомендуется помещать инициализационные блоки после определения полей.

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

  1. Все поля инициализируются значениями по умолчанию (0, false, null).
  2. Инициализаторы полей и инициализационные блоки выполняются в порядке их следования в объявлении класса.
  3. Если в первой строке конструктора указан вызов другого конструктора, то выполняется вызов.
  4. Выполняется тело конструктора.

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

Статические поля инициализируются заданием начального значения:

static int nextId = 1;

Если статическое поле инициализируется сложным кодом, можно использовать статический инициализационный блок - static initialization block. Пример кода, где идентификационные номера сотрудников должны начинаться со случайного числа, не превышающего 10000:

static {
    Random generator = new Random();
    nextId = generator.nextInt(10000);
}

Статическая инициализация выполняется при первой загрузке класса. Аналогично полям экземпляра, статические поля принимают значения 0, false, null (если их значения не заданы явно). Все операторы, задающие начальные значения статических полей, и статические блоки инициализации, выполняются в порядке их перечисления в объявлении класса.

Можно создать программу "Hello, World", не используя метод main():

public class Hello {

    static {
        System.out.println("Hello, World!");
    }
}

После загрузки класса Hello, статический блок инициализации выводит строку "Hello, World!", и лишь затем появляется сообщение, что метод main() не определен. Сообщение можно подавить с помощью вызова метода System.exit(0) в конце static-блока.

Следующая программа иллюстрирует многие из рассмотренных вопросов:

  • Перегрузка конструкторов.
  • Вызов конструктора с помощью выражения this( ... )
  • Использование конструктора по умолчанию.
  • Использование инициализационного блока.
  • Использование статического инициализационного блока.
  • Инициализация поля экземпляра.
import java.util.*;

public class ConstructorTest {

    public static void main(String[] args) {

        // fill the staff array with three Employee objects
        Employee[] staff = new Employee[3];

        staff[0] = new Employee("Harry", 40000);
        staff[1] = new Employee(60000);
        staff[2] = new Employee();

        // print out information about all Employee objects
        for (Employee e : staff) {
            System.out.println("name = " + e.getName()
                    + ", id = " + e.getId()
                    + ", salary = " + e.getSalary());
        }
    }
}

class Employee {
    // three overloaded constructors

    public Employee(String n, double s) {
        name = n;
        salary = s;
    }

    public Employee(double s) {
        // calls the Employee(String, double) constructor
        this("Employee #" + nextId, s);
    }

    // the default constructor
    public Employee() {
        // name initialized to ""--see below
        // salary not explicitly set--initialized to 0
        // id initialized in initialization block
    }

    public String getName() {
        return name;
    }

    public double getSalary() {
        return salary;
    }

    public int getId() {
        return id;
    }

    private static int nextId;
    private int id;
    private String name = "";  // instance field initialization
    private double salary;

    // static initialization block
    static {
        Random generator = new Random();
        // set nextId to a random number between 0 and 9999
        nextId = generator.nextInt(10000);
    }

    // object initialization block
    {
        id = nextId;
        nextId++;
    }
}


Уничтожение объекта и метод finalize()

В некоторых объектно-ориентированных языках, например в C++, есть явные деструкторы, предназначенные для уничтожения объектов. Их основная задача - освобождение памяти, занятой объектами. Поскольку в Java есть механизм автоматической сборки мусора, освобождать память вручную нет необходимости. Поэтому в Java деструкторов нету.

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

В любой класс можно добавить метод finalize(). Этот метод будет вызван перед тем, как система "сборки мусора" уничтожит объект. На практике, если вам нужно возобновить ресурсы и сразу использовать их повторно, нельзя полагаться на метод finalize(), - неизвестно, когда именно этот метод будет вызван.

Существует метод System.runFinalizerOnExit(true), гарантирующий, что метод finalize() будет вызван до того, как программа завершит свою работу. Однако, этот метод крайне ненадежен и не рекомендован к использованию.

В качестве альтернативы можно применить метод Runtime.addShutdownHook(). Дополнительную информацию о нем можно найти в документации по API.

Если ресурс должен быть освобожден сразу после его использования, нужно самостоятельно написать соответствующий код. Добавьте метод dispose() или close(), который нужно явно вызвать для очистки памяти. Если класс имеет метод dispose(), нужно вызвать данный метод по завершении работы с объектом этого класса.


Справочник

java.util.Random

Random()  // создает новый генератор случайных чисел

int nextInt(int n)  // возвращает случайное число в интервале от 0 до n - 1



Форум

Назад | Оглавление | Дальше

Personal tools
Namespaces

Variants
Actions
Navigation
Tools