Главная » Java » Создание объектов представляющих небесные тела

0

Для первой версии класса Body создание и инициализация объектов, представляющих небесные тела, происходит следующим образом:

Body sun = new Body(); sun.idNum = Body.nextID++; sun.nameFor = “Sol”;

sun.orbits = null; // Солнце является центром Солнечной

// системы

Body earth = new Body(); earth.idNum = Body.nextID++; earth.nameFor = “Earth”; earth.orbits = sun;

Сначала мы объявили две ссылки (sun и earth) на объекты типа Body. Как упоминалось выше, объекты при этом не создаются — лишь объявляются переменные, которые ссылаются на объекты. Первоначальное  значение ссылок равно null, а соответствующие им объекты должны явным образом создаваться в программе.

Объект sun создается посредством оператора new. Конструкция new считается самым распространенным  способом построения объектов (позднее мы рассмотрим и другие возможности). При создании объекта оператором new следует указать тип конструируемого  объекта и необходимые параметры. Runtime-система выделяет область памяти, достаточную для хранения полей объекта, и инициализирует ее в соответствии с рассматриваемыми  ниже правилами. После завершения инициализации runtime-система возвращает ссылку на созданный объект.

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

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

В нашем примере строится модель Солнечной системы. В ее центре находится Солнце, поэтому полю orbits объекта sun присваивается значение null — у Солнца нет объекта, вокруг которого оно бы вращалось. При создании и инициализации объекта earth (Земля) мы присвоили полю orbits значение sun. Для Луны, вращающейся вокруг Земли, поле orbits получило бы значение earth. Если бы мы строили модель Галактики, то Солнце бы также вращалось вокруг “черной дыры”, находящейся где-то в середине Млечного Пути.

Упражнение 2.4

Напишите для класса Vehicle метод main, который создает несколько объектов-автомашин и выводит значения их полей.

Упражнение 2.5

Напишите для класса LinkedList метод main, который создает несколько объектов типа

Vehicle и заносит их в список.

2.5. Конструкторы

Каждый вновь созданный объект обладает некоторым исходным состоянием. Значения полей могут инициализироваться при их объявлении — иногда этого бывает достаточно.

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

\u0000, false или null, в зависимости от типа./ Однако довольно часто для определения

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

Для тех случаев, когда простой инициализации недостаточно, используются

конструкторы. Имя конструктора совпадает с именем класса, который он инициализирует. Конструкторы, подобно методам, могут получать один или несколько параметров, однако они не являются методами и не могут возвращать никакого значения. Параметры конструктора (если они есть) указываются в скобках за именем типа при создании

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

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

В усовершенствованной версии класса Body исходное состояние объекта частично устанавливается посредством инициализации, а частично — в конструкторе:

class Body {

public long idNum;

public String name = “”;

public Body orbits = null;

private static long nextID = 0;

Body() {

idNum = nextID++;

}

}

Конструктор класса Body вызывается без аргументов, однако он выполняет важную функцию, а именно устанавливает во вновь создаваемом объекте правильное значение поля idNum. Простейшая ошибка, возможная при работе со старой версией класса, — скажем, вы забыли присвоить значение полю idNum или не наращивали nextID после его использования — приводила к тому, что в программе возникали разные объекты класса Body с одинаковыми значениями поля idNum. В результате возникали проблемы в той части кода, которая была основана на положении контракта, гласящем: “Все значения idNum должны быть разными”.

Возлагая ответственность за генерацию значений idNum на сам класс Body, мы тем самым предотвращаем ошибки подобного рода. Конструктор Body становится единственным местом в программе, где idNum присваивается значение. Следующим шагом является объявление поля nextID с ключевым словом private, чтобы доступ к нему осуществлялся только из класса. Тем самым мы устраняем еще один возможный источник ошибок для программистов, работающих с классом Body.

Кроме того, такое решение позволит нам свободно изменять способ присвоения значений полю idNums в объектах Body. Например, будущая реализация нашего класса может просматривать базу данных известных астрономических  объектов и присваивать новое значение idNum лишь в том случае, если ранее данному небесному телу не был присвоен другой идентификатор. Такое изменение никак не скажется на существующем коде программы, поскольку он никак не участвует в присвоении значения idNum.

При инициализации полям name и orbits присваиваются некоторые разумные начальные значения. Следовательно, после приведенного ниже вызова конструктора все поля нового объекта Body будут инициализированы.  После этого вы можете изменить состояние объекта, присвоив его полям нужные значения:

Body sun = new Body(); // значение idNum равно 0 sun.name = “Sol”;

Body earth = new Body();  // значение idNum равно 1 earth.name = “Earth”;

earth.orbits = sun;

Конструктор Body вызывается при создании нового объекта оператором new, но после того, как полям name и orbits будут присвоены начальные значения. Инициализация поля orbits значением null означает, что sun.orbits в нашей программе не присваивается никакого значения.

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

Body(String bodyName, Body orbitsAround) {

this();

name = bodyName;

orbits = orbitsAround;

}

Как видите, из одного конструктора можно вызывать другой конструктор этого же класса — для этого первым выполняемым оператором должен быть вызов this(). Это называется “явным вызовом конструктора”. Если для вызова конструктора необходимы параметры, они могут передаваться. В нашем случае для присвоения значения полю idNum используется конструктор, вызываемый без аргументов. Теперь создание объектов происходит значительно проще:

Body sun = new Body(“Sol”, null); Body earth = new Body(“Earth”, sun);

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

Для некоторых классов необходимо, чтобы создатель объекта предоставлял некоторые данные. Например, приложение может требовать, чтобы для всех объектов Body было указано их название. Чтобы убедиться, что всем создаваемым объектам Body передается название, необходимо включить соответствующий  параметр во все конструкторы класса Body.

Приведем несколько общепринятых соображений в пользу создания специализированных конструкторов:

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

При конструировании  объектов некоторых видов передача исходного состояния оказывается самым удобным и разумным выходом (примером может служить конструктор Body с двумя аргументами).

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

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

Конструкторы, не получающие при вызове никаких аргументов, встречаются настолько часто, что для них даже появился специальный термин: “безаргументные”  (no-arg) конструкторы.

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

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

class SimpleClass {

/** Эквивалент конструктора по умолчанию */

public SimpleClass() {

}

}

Конструктор по умолчанию имеет атрибут public, если такой же атрибут имеет класс, и не имеет его в противном случае.

Упражнение 2.6

Включите в класс Vehicle два конструктора. Первый из них — безаргументный, а

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

Упражнение 2.7

Какие конструкторы вы бы сочли нужным добавить в класс LinkedList?

2.6. Методы

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

(см. “Проектирование расширяемого класса”). Многие классы обладают функциями,

которые невозможно свести к чтению или изменению некоторой величины — для них необходимы вычисления.

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

reference.method(parameters)

Каждый метод вызывается с определенным количеством параметров. Java не поддерживает методов, у которых допускается переменное число параметров. Каждый параметр имеет строго определенный тип — примитивный или ссылочный. Кроме того, методы обладают типом возвращаемого значения, который указывается перед их именем. Например, приведем метод класса Body, который создает строку типа String с описанием конкретного объекта Body:

public String toString() {

String desc = idNum + “ (” + name + “)”;

if (orbits != null)

desc += “ orbits ” + orbits.toString();

return desc;

}

В этом методе производится конкатенация объектов String с помощью операторов + и +=. Сначала образуется строка, содержащая идентификатор и название объекта. Если данное небесное тело обращается вокруг другого, то к ней присоединяется строка с описанием центра вращения, для чего вызывается метод toString соответствующего  объекта. Последовательность рекурсивных вызовов продолжает строить цепочку тел, обращающихся вокруг друг друга, пока не будет найдено тело, не имеющее центра вращения.

Метод toString не совсем обычен. Если у объекта имеется метод с именем toString, который вызывается без параметров и возвращает значение типа String, то он используется для приведения объекта к типу String, если он участвует в конкатенации строк, выполняемой оператором +. В следующих выражениях:

System.out.println(“Body ” + sun); System.out.println(“Body ” + earth);

происходит косвенный вызов методов toString для объектов sun и earth, приводящий к следующим результатам:

Body 0 (Sol)

Body 1 (Earth) orbits 0 (Sol)

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

К примеру, предположим, что вам нужно написать метод для возвращения перечня финансовых операций, которые определенное лицо может выполнять с банковским счетом. Таких операций может быть несколько (зачисление и снятие средств со счета и т. д.); следовательно, метод должен возвращать несколько значений. Вот как выглядит объект Permissions, в котором сохраняются логические значения, определяющие допустимость той или иной операции:

class Permissions {

public boolean canDeposit, canWithdraw, canClose;

}

А вот как выглядит метод, заполняющий эти поля:

class Account {

public Permissions permissionsFor(Person who) { Permissions perm = new Permissions(); perm.canDeposit = canDeposit(who); perm.canWithdraw = canWithdraw(who); perm.canClose = canClose(who);

return perm;

}

// … определение метода canDeposit()

}

Если метод не возвращает никакого значения, то на месте возвращаемого типа ставится ключевое слово void. В противном случае каждый возможный путь выполнения его операторов должен возвращать значение, которое может быть присвоено переменной объявленного типа. К примеру, метод permissions For не может возвращать значение типа String, поскольку невозможно присвоить объект типа String переменной типа Permissions. Однако вы можете объявить, что метод permissionsFor возвращает значение String, не изменяя при этом оператор return, поскольку ссылка на объект Permissions может быть присвоена переменной типа Object.

2.6.1. Значения параметров

Все параметры в Java передаются “по значению”. Другими словами, значения переменных-параметров метода являются копиями значений, указанных при его вызове. Если передать методу переменную некоторого типа, то параметр будет представлять

собой копию этой переменной; ее изменение внутри метода никак не повлияет на значение переменной в коде за его пределами. Например:

class PassByValue {

public static void main(String[] args) {

double one = 1.0;

System.out.println(“before: one = ” + one);

halveIt(one);

System.out.println(“after: one = ” + one);

}

public static void halveIt(double arg) {

arg /= 2.0; //

System.out.println(“halved: arg = ” + arg);

}

}

Приведенные ниже результаты показывают, что деление на два переменной arg в методе

halveIt не меняет значения переменной one в методе main:

before: one = 1 halved: arg = 0.5 after:  one = 1

Однако, если параметр метода представляет собой ссылку на объект, то “по значению” передается ссылка, а не сам объект! Следовательно, вы можете изменять в методе тот объект, на который она ссылается, — значение ссылки остается прежним. Можно изменять любые поля объекта или вызывать методы, влияющие на его состояние, — произойдет изменение объекта во всех фрагментах программы, где имеется ссылка на него. Приведенный ниже пример наглядно показывает, чем данный случай отличается от предыдущего:

class PassRef {

public static void main(String[] args) {

Body sirius = new Body(“Sirius”, null);

System.out.println(“before: ” + sirius); commonName(sirius); System.out.println(“after: ” + sirius);

}

public static void commonName(Body bodyRef) {

bodyRef.name = “Dog Star”;

bodyRef = null;

}

}

Результат будет следующим:

before: 0 (Sirius)

after: 0 (Dog Star)

Обратите внимание на то, что название объекта изменилось, тогда как ссылка bodyRef все равно указывает на объект Body (хотя метод commonName присваивал параметру bodyRef значение null).

Приведенная выше диаграмма показывает состояние ссылок непосредственно  после вызова commonName в main. Обе ссылки — sirius (в main) и bodyRef (в commonName) — указывают на один и тот же объект. Когда commonName изменяет значение поля bodyRef.name, то название изменяется в объекте, совместно используемом обоими ссылками. Однако при присвоении null ссылке bodyRef изменяется только ее значение, тогда как значение ссылки на объект sirius остается тем же самым; вспомним, что параметр bodyRef является передаваемой по значению копией sirius. Внутри метода commonName изменяется лишь значение переменной-параметра bodyRef, подобно тому как в методе halveIt изменялось лишь значение параметра-переменной arg. Если бы изменение bodyRef относилось и к значению sirius в main, то в строке, начинающейся с “after: ”, стояло бы “null”. Тем не менее переменные bodyRef в commonName и sirius в main указывают на один и тот же объект, поэтому изменения, вносимые в commonName, отражаются и в том объекте, на который ссылается sirius.

2.6.2. Применение методов для ограничения доступа

Пользоваться классом Body с несколькими конструкторами стало значительно удобнее, чем его старым вариантом, состоявшим из одних данных; кроме того, мы обеспечили правильное автоматическое присвоение значений idNum. И все же программист может все испортить, изменяя значение поля idNum после конструирования  объекта, — ведь данное поле объявлено как public и открыто для любых действий. Необходимо, чтобы поле idNum содержало данные, доступные только для чтения. Подобные данные в объектах встречаются довольно часто, но в языке Java не существует ключевого слова, которое сделало бы поле за пределами класса доступным только для чтения.

Чтобы сделать поле доступным только для чтения, мы должны скрыть его. Для этого поле idNum объявляется с ключевым словом private, а в класс добавляется новый метод, с помощью которого код за пределами класса может получить значение этого поля:

class Body {

private long idNum; // поле стало private

public String name = “”; public Body orbits = null; private static long nextID = 0;

Body() {

}

idNum = nextID++;

public long id() {

return idNum;

}

// …

}

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

Методы, регулирующие доступ к внутренним данным класса, иногда называются методами доступа (accessor methods). С их помощью также можно (и, наверное, нужно) защитить поля name и orbits.

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

поля и что происходит в программе при их изменении. По этим причинам в дальнейших примерах этой книги поля public встречаются очень редко.

Упражнение 2.8

Объявите поля класса Vehicle с ключевым словом private и опишите соответствующие методы доступа. Для каких полей следует предусмотреть методы, изменяющие их значения, а для каких — нет?

Упражнение 2.9

Объявите поля класса LinkedList с ключевым словом private и включите в класс соответствующие  методы доступа. Для каких полей следует предусмотреть методы, изменяющие их значения, а для каких — нет?

Упражнение 2.10

Включите в класс Vehicle метод changeSpeed для задания текущей скорости машины в соответствии с передаваемым значением и метод stop для обнуления скорости.

Упражнение 2.11

Включите в класс LinkedList метод для подсчета количества элементов в списке.

2.7. Ссылка this

Мы уже видели (на стр. ), как в начале работы конструктора происходит явный вызов другого конструктора класса. Специальная ссылка на объект this может также применяться внутри нестатических методов; она указывает на текущий объект, для

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

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

Service.add(this);

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

class Name {

public String str; Name() {

str = “”;

}

}

равносильно следующему:

this.str = “”;

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

class Moose {

String hairdresser;

Moose(String hairdresser) {

this.hairdresser = hairdresser;

}

}

Поле hairdresser внутри конструктора скрывается присутствием одноименного параметра. Чтобы обратиться к полю hairdresser, а не к параметру, мы ставим перед именем ссылку this, указывая тем самым, что поле принадлежит к текущему объекту. Намеренное скрытие идентификаторов,  осуществляемое  подобным образом, может быть отнесено к хорошему стилю программирования  лишь при идиоматическом  использовании в конструкторах и методах доступа.

Помимо ссылки this, может также применяться ссылка super, с помощью которой осуществляется доступ к скрытым полям и вызов переопределенных  методов суперкласса. Ключевое слово super подробно рассматривается  в разделе “Переопределение  методов и скрытие полей”.

2.8. Перегрузка методов

В языке Java каждый метод обладает определенной сигнатурой, которая представляет собой совокупность имени с количеством и типом параметров. Два метода могут иметь одинаковые имена, если их сигнатуры отличаются по количеству или типам параметров. Это называется перегрузкой (overloading), поскольку простое имя метода “перегружается” несколькими значениями. Когда программист вызывает метод, компилятор по количеству

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

нашего класса Body:

public Body orbitsAround() {

return orbits;

}

public void orbitsAround(Body around) {

orbits = around;

}

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

Вопрос о том, как язык выбирает среди нескольких перегруженных методов тот, что нужен для данного вызова, подробно рассматривается  в разделе “Доступ к членам” на стр. .

Упражнение 2.12

Включите в класс Vehicle два новых метода: один в качестве параметра получает количество градусов, на которое поворачивает машина, а другой — одну из констант Vehicle.TURN_LEFT  или Vehicle.TURN_RIGHT.

Источник: Арнольд К., Гослинг Д. – Язык программирования Java (1997)

По теме:

  • Комментарии