Класс Complex

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

зике и технических дисциплинах. Но класс, описывающий комплексные числа, почему-то не включен в стандартную библиотеку Java. Восполним этот пробел.

Листинг 2.4 длинный, но просмотрите его внимательно, при обучении языку программирования очень полезно чтение программ на этом языке. Более того, только программы и стоит читать, пояснения автора лишь мешают вникнуть в смысл действий (шутка).

Листинг 2.4. Класс Complex

class Complex{

private static final double EPs = 1e-12; // Точность вычислений. private double re, im; // Действительная и мнимая части.

// Четыре конструктора:

Complex(double re, double im){ this.re = re; this.im = im;

}

Complex(double re){this(re, 0.0);}

Complex(){this(0.0, 0.0);}

Complex(Complex z){this(z.re, z.im);}

// Методы доступа: public double getRe(){return re;} public double getIm(){return im;}

public Complex getZ(){return new Complex(re, im);} public void setRe(double re){this.re = re;} public void setIm(double im){this.im = im;} public void setZ(Complex z){re = z.re; im = z.im;}

// Модуль и аргумент комплексного числа: public double mod(){return Math.sqrt(re * re + im * im);} public double arg(){return Math.atan2(re, im);}

// Проверка: действительное число? public boolean isReal(){return Math.abs(im) < EPs;} public void pr(){ // Вывод на экран

system.out.println(re + (im < 0.0 ? "" : "+") + im + "i");

}

// Переопределение методов класса Object: public boolean equals(Complex z){ return Math.abs(re — z.re) < EPs &&

Math.abs(im — z.im) < EPs;

}

public string tostring(){

return "Complex: " + re + " " + im;

}

// Методы, реализующие операции +=, -=, *=, /= public void add(Complex z){re += z.re; im += z.im;} public void sub(Complex z){re -= z.re; im -= z.im;} public void mul(Complex z){

double t = re * z.re — im * z.im; im = re * z.im + im * z.re; re = t;

public void div(Complex z){

double m = z.re * z.re + z.im * z.im; double t = re * z.re — im * z.im; im = (im * z.re — re * z.im) / m; re = t / m;

}

// Методы, реализующие операции +, -, *, /

public Complex plus(Complex z){

return new Complex(re + z.re, im + z.im);

}

public Complex minus(Complex z){

return new Complex(re — z.re, im — z.im);

}

public Complex asterisk(Complex z){ return new Complex(

re * z.re — im * z.im, re * z.im + im * z.re);

}

public Complex slash(Complex z){

double m = z.re * z.re + z.im * z.im; return new Complex(

(re * z.re — im * z.im) / m, (im * z.re — re * z.im) / m);

}

}

// Проверим работу класса Complex. public class ComplexTest{

public static void main(string[] args){ Complex z1 = new Complex(),

z2 = new Complex(1.5),

z3 = new Complex(3.6, -2.2),

z4 = new Complex(z3);

// Оставляем пустую строку. "); z1.pr();

"); z2.pr();

"); z3.pr();

"); z4.pr();

// Работает метод toString().

System.out.println(); system.out.print("z1 system.out.print("z2 system.out.print("z3 system.out.print("z4 System.out.println(z4); z2.add(z3);

'); z2.pr(); '); z2.pr(); '); z2.pr(); '); z3.pr();

system.out.print("z2 + z3 z2.div(z3);

system.out.print("z2 / z3 z2 = z2.plus(z2); system.out.print("z2 + z2 z3 = z2.slash(z1); system.out.print("z2 / z1

}

На рис. 2.3 показан вывод этой программы.

Рис. 2.3. Вывод программы ComplexTest

Метод main()

Всякая программа, оформленная как приложение (application), должна содержать метод с именем main. Он может быть один на все приложение или присутствовать в некоторых классах этого приложения, а может находиться и в каждом классе.

Метод main () записывается как обычный метод, может содержать любые описания и действия, но он обязательно должен быть открытым (public), статическим (static), не иметь возвращаемого значения (void). У него один параметр, которым обязательно должен быть массив строк (string [ ]). По традиции этот массив называют args, хотя имя может быть любым.

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

Часто метод main() заносят в каждый класс с целью отладки. В этом случае в метод main () включают тесты для проверки работы всех методов класса.

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

Все это легко понять на примере листинга 2.5, в котором записана программа, просто выводящая на консоль аргументы, передаваемые в метод main () при запуске.

Листинг 2.5. Передача аргументов в метод main()

class Echo{

public static void main(string[] args){ for (string s: args)

system.out.println("arg = " + s);

}

}

На рис. 2.4 показаны результаты работы этой программы с разными вариантами задания аргументов.

Рис. 2.4. Вывод параметров командной строки

Как видите, имя класса не входит в число аргументов. Оно и так известно в методе

main().

Знатокам C/C++

Поскольку в Java имя файла всегда совпадает с именем класса, содержащего метод main (), оно не заносится в args [0]. Вместо параметра argc используется переменная args. length, имеющаяся в каждом массиве. Доступ к переменным среды разрешен не всегда и осуществляется другим способом. Некоторые значения переменных среды можно просмотреть так:

system.getProperties().list(system.out);

Методы с переменным числом аргументов

Как видно из рис. 2.4, при вызове программы из командной строки мы можем задавать ей разное число аргументов. Исполняющая система Java создает массив этих аргументов и передает его методу main(). Такую же конструкцию можно сделать в своей программе:

class VarArgs{

private static int[] argsl = {1, 2, 3, 4, 5, 6};

private static int[] args2 = {100, 90, 80, 70};

public static int sum(int[] args){ int result = 0;

for (int k: args) result += k; return result;

}

public static void main(string[] args){

System.out.println("Sum1 = " + sum(args1));

System.out.println("Sum2 = " + sum(args2));

}

}

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

public static int sum(int... args){ int result = 0;

for (int k: args) result += k; return result;

}

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

public static void main(string[] args){

System.out.println("Sum1 = " + sum(1, 2, 3, 4, 5, 6));

System.out.println("Sum2 = " + sum(100, 90, 80, 70));

}

Где видны переменные

В языке Java нестатические переменные разрешено объявлять в любом месте кода между операторами. Статические переменные могут быть только полями класса, а значит, не должны объявляться внутри методов и блоков. Какова же область видимости (scope) переменных? Из каких методов мы можем обратиться к той или иной переменной? В каких операторах использовать? Рассмотрим на примере листинга 2.6 разные случаи объявления переменных.

Листинг 2.6. Видимость и инициализация переменных

class ManyVariables{

static int x = 9, y; // Статические переменные — поля класса.

// Они известны во всех методах и блоках класса. // Поле y получает значение 0.

static{ // Блок инициализации статических переменных.

// Выполняется один раз при первой загрузке класса // после инициализаций в объявлениях переменных. x = 99; // Этот оператор выполняется в блоке вне всякого метода!

int a = 1, p; // Нестатические переменные — поля экземпляра.

// Известны во всех методах и блоках класса,

// в которых они не перекрыты другими переменными // с тем же именем.

// Поле p получает значение 0.

{ // Блок инициализации экземпляра.

// Выполняется при создании каждого экземпляра // после инициализаций при объявлениях переменных. p = 999; // Этот оператор выполняется в блоке вне всякого метода!

}

static void f(int b){ // Параметр метода b — локальная переменная,

// известная только внутри метода. int a = 2; // Это вторая переменная с тем же именем "a".

// Она известна только внутри метода f()

// и здесь перекрывает первую "a".

int c; // Локальная переменная, известна только в методе f().

// Не получает никакого начального значения // и должна быть определена перед применением.

{ int c = 555; // Сшибка! Попытка повторного объявления.

int x = 333; // Локальная переменная, известна только в этом блоке.

}

// Здесь переменная x уже неизвестна. for (int d = 0; d < 10; d++){

// Переменная цикла d известна только в цикле. int a = 4; // Ошибка!

int e = 5; // Локальная переменная, известна только в цикле for.

e++; // Инициализируется при каждом выполнении цикла.

System.out.println("e = " + e); // Выводится всегда "e = 6".

}

// Здесь переменные d и e неизвестны.

}

public static void main(string[] args){

int a = 9999; // Локальная переменная, известна только внутри

// метода main().

f (a) ;

}

}

Обратите внимание на то, что переменные класса и экземпляра неявно присваивают нулевые значения. Символы неявно получают значение ’ u0000 ’, логические переменные — значение false, ссылки получают неявно значение null.

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

Внимание!

Поля класса при объявлении обнуляются, локальные переменные автоматически не инициализируются.

В листинге 2.6 появилась еще одна новая конструкция: блок инициализации экземпляра (instance initialization). Это просто блок операторов в фигурных скобках, но записывается он вне всякого метода, прямо в теле класса. Этот блок выполняется при создании каждого экземпляра, после static-блоков и инициализации при объявлении переменных, но до выполнения конструктора. Он играет такую же роль, как и static-блок для статических переменных. Зачем же он нужен, ведь все его содержимое можно написать в начале конструктора? Он применяется в тех случаях, когда конструктор написать нельзя, а именно в безымянных внутренних классах.

Вложенные классы

В этой главе уже несколько раз упоминалось, что в теле класса можно сделать описание другого, вложенного (nested) класса. А во вложенном классе можно снова описать вложенный, внутренний (inner) класс и т. д. Эта "матрешка" кажется вполне естественной, но вы уже поднаторели в написании классов, и у вас возникает масса вопросов.

? Можем ли мы из вложенного класса обратиться к членам внешнего класса? Можем, для того это все и задумывалось.

? А можем ли мы в таком случае определить экземпляр вложенного класса, не определяя экземпляры внешнего класса? Нет, не можем, сначала надо определить хоть один экземпляр внешнего класса, матрешка ведь!

? А если экземпляров внешнего класса несколько, как узнать, с каким экземпляром внешнего класса работает данный экземпляр вложенного класса? Имя экземпляра вложенного класса уточняется именем связанного с ним экземпляра внешнего класса. Более того, при создании вложенного экземпляра операция new тоже уточняется именем внешнего экземпляра.

? А?..

Хватит вопросов, давайте разберем все по порядку.

Все вложенные классы можно разделить на вложенные классы-члены класса (member classes), описанные вне методов, и вложенные локальные классы (local classes), описанные внутри методов и/или блоков. Локальные классы, как и все локальные переменные, не являются членами класса.

Классы-члены могут быть объявлены статическими с помощью модификатора static. Поведение статических классов-членов ничем не отличается от поведения обычных классов, отличается только обращение к таким классам. Поэтому они называются вложенными классами верхнего уровня (nested top-level classes), хотя статические классы-члены можно вкладывать друг в друга. В них можно объявлять статические члены. Используются они обычно для того, чтобы сгруппировать вспомогательные классы вместе с основным классом.

Все нестатические вложенные классы называются внутренними (inner). В них нельзя объявлять статические члены.

Локальные классы, как и все локальные переменные, известны только в блоке, в котором они определены. Они могут быть безымянными (anonymous classes).

В листинге 2.7 рассмотрены все эти случаи.

Листинг 2.7. Вложенные классы

class Nested{

static private int pr; // Переменная pr объявлена статической,

// чтобы к ней был доступ из статических классов A и AB.

String s = "Member of Nested";

// Вкладываем статический класс.

static class A{ // Полное имя этого класса — Nested.A

private int a = pr;

String s = "Member of A";

// Во вложенный класс A вкладываем еще один статический класс. static class AB{ // Полное имя класса — Nested.A.AB

private int ab = pr;

String s = "Member of AB";

}

}

// В класс Nested вкладываем нестатический класс. class B{ // Полное имя этого класса — Nested.B

private int b = pr;

String s = "Member of B";

// В класс B вкладываем еще один класс.

class BC{ // Полное имя класса — Nested.B.BC

private int bc = pr;

String s = "Member of BC";

}

void f(final int i){ // Без слова final переменные i и j

// нельзя использовать в локальном классе D.

final int j = 99;

class D{ // Локальный класс D известен только внутри f().

private int d = pr;

String s = "Meimoer of D"; void pr(){

// Обратите внимание на то, как различаются // переменные с одним и тем же именем "s". System.out.println(s + (i+j)); // "s" эквивалентно "this.s".

System.out.println(B.this.s);

System.out.println(Nested.this.s);

// System.out.println(AB.this.s); // Нет доступа.

// System.out.println(A.this.s); // Нет доступа.

}

}

D d = new D(); // Объект определяется тут же, в методе f().

d.pr(); // Объект известен только в методе f().

}

}

void m(){

new Object(){ // Создается объект безымянного класса,

// указывается конструктор его суперкласса.

private int e = pr; void g(){

System.out.println("From g()");

}

}.g(); // Тут же выполняется метод только что созданного объекта.

}

}

public class NestedClasses{

public static void main(String[] args){

Nested nest = new Nested(); // Последовательно раскрываются

// три матрешки.

Nested.A theA = nest.new A(); // Полное имя класса и уточненная

// операция new. Но конструктор только вложенного класса.

Nested.A.AB theAB = theA.new AB(); // Те же правила.

// Операция new уточняется только одним именем.

Nested.B theB = nest.new B(); // Еще одна матрешка.

Nested.B.BC theBC = theB.new BC();

theB.f(999); // Методы вызываются обычным образом.

nest.m();

}

}

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

Теперь дадим пояснения.

? Как видите, доступ к полям внешнего класса Nested возможен отовсюду, даже к закрытому полю pr. Именно для этого в Java и введены вложенные классы. Остальные конструкции добавлены вынужденно, для того чтобы увязать концы с концами.

? Язык Java позволяет использовать одни и те же имена в разных областях видимости -поэтому пришлось уточнять константу this именем класса: Nested.this, B.this.

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

? Нельзя создать экземпляр вложенного класса, не создав предварительно экземпляр внешнего класса, — поэтому пришлось подстраховать это правило уточнением операции new именем экземпляра внешнего класса nest. new, theA. new, theB. new.

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

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

? Можно ли наследовать вложенные классы? Можно.

? Как из подкласса обратиться к методу суперкласса? Константа super уточняется именем соответствующего суперкласса, подобно константе this.

? А могут ли вложенные классы быть расширениями других классов? Могут.

? А как?.. Помните принцип KISS!!!

Механизм вложенных классов станет понятнее, если посмотреть, какие файлы с байткодами создал компилятор:

? Nested$1$D.class — локальный класс D, вложенный в класс Nested;

? Nested$1.class — безымянный класс;

? Nested$A$AB.class — класс Nested.A.AB;

? Nested$A.class — класс Nested.A;

? Nested$B$BC.class — класс Nested.B.BC;

? Nested$B.class — класс Nested.B;

? Nested.class — внешний класс Nested;

? NestedClasses.class — класс с методом main ().

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

Оказывается, вложенные классы существуют только на уровне исходного кода. Виртуальная машина Java ничего не знает о вложенных классах. Она работает с обычными внешними классами. Для взаимодействия объектов вложенных классов компилятор вставляет в них специальные закрытые поля. Поэтому в локальных классах можно использовать только константы объемлющего метода, т. е. переменные, помеченные словом final. Виртуальная машина просто не догадается передавать изменяющиеся значения переменных в локальный класс. Таким образом, не имеет смысла помечать вложенные классы модификатором private, все равно они выходят на самый внешний уровень.

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

В примере с домашними животными мы сделали объект person класса Master — владелец животного — полем класса Pet. Если класс Master больше нигде не используется, то можно определить его прямо внутри класса Pet, сделав класс Master вложенным (inner) классом. Это выглядит следующим образом:

class Pet{

// В этом классе описываем общие свойства всех домашних любимцев. class Master{

// Хозяин животного. string name; // Фамилия, имя.

// Другие сведения...

void getFood(int food, int drink); // Кормление.

// Прочее...

}

int weight; // Вес животного.

int age; // Возраст животного.

Date eatTime[]; // Массив, содержащий время кормления.

int eat(int food, int drink, Date time){ // Процесс кормления.

// Начальные действия.

if (time == eatTime[i]) person.getFood(food, drink);

// Метод потребления пищи...

}

void voice(); // Звуки, издаваемые животным.

// Прочее.

}

Вложение класса удобно тем, что методы внешнего класса могут напрямую обращаться к полям и методам вложенного в него класса. Но ведь того же самого можно было добиться по-другому. Может, следовало расширить класс Master, сделав класс Pet его наследником?

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

Отношения "быть частью" и "являться"

Теперь у нас появились две различные иерархии классов. Одну иерархию образует наследование классов, другую — вложенность классов.

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

Теория ООП советует прежде всего выяснить, в каком отношении находятся объекты классов Master и Pet — в отношении "класс Master является экземпляром класса Pet" или в отношении "класс Master является частью класса Pet". Скажем, "собака является животным" или "собака является частью животного"? Другой пример: "мотор является автомобилем" или "мотор является частью автомобиля"? Ясно, что собака — животное и в этой ситуации надо выбрать наследование, но мотор — часть автомобиля и здесь надо выбрать вложение.

Отношение "класс А является экземпляром класса В" по-английски записывается как "a class A is a class B", поэтому в теории ООП называется отношением "is-a". Отношение же "класс А является частью класса В" по-английски "a class A has a class B", и такое отношение называется отношением "has-a".

Отношение "is-a" — это отношение "обобщение-детализация", отношение большей и меньшей абстракции, и ему соответствует наследование классов.

Отношение "has-a" — это отношение "целое-часть" и ему соответствует вложение классов.

Вернемся к нашим животным и их хозяевам и постараемся ответить на вопрос: "класс Master является экземпляром класса Pet" или "класс Master является частью класса Pet"? Ясно, что не верно ни то, ни другое. Классы Master и Pet не связаны ни тем, ни другим образом. Поэтому мы и сделали объект класса Master полем класса Pet.

Заключение

После прочтения этой главы вы получили представление о современной парадигме программирования — объектно-ориентированном программировании и реализации этой парадигмы в языке Java. Если вас заинтересовало ООП, обратитесь к специальной литературе [3—6].

Не беда, если вы не усвоили сразу принципы ООП. Для выработки "объектного" взгляда на программирование нужны время и практика. Части II и III книги как раз и дадут вам эту практику. Но сначала необходимо ознакомиться с важными понятиями языка Java — пакетами и интерфейсами.

Вопросы для самопроверки

1. Какие парадигмы возникали в программировании по мере его развития?

2. Какова современная парадигма программирования?

3. Что такое объектно-ориентированное программирование?

4. Что понимается под объектом в ООП?

5. Каковы основные принципы ООП?

6. Что такое класс в ООП?

7. Какая разница между объектом и экземпляром класса?

8. Что входит в класс Java?

9. Что такое конструктор класса?

10. Какая операция выделяет оперативную память для объекта?

11. Что такое суперкласс и подкласс?

12. Как реализуется полиморфизм в Java?

13. Для чего нужны статические поля и методы класса?

14. Какую роль играют абстрактные методы и классы?

15. Можно ли записать конструктор в абстрактном классе?

16. Почему метод main() должен быть статическим?

17. Почему метод main() должен быть открытым?

ГЛАВА 3