Объектно-ориентированное программирование в Java

We use cookies. Read the Privacy and Cookie Policy

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

Парадигмы программирования

Первые, даже самые простые программы, написанные в машинных кодах, составляли сотни строк совершенно непонятного текста. Языки ассемблера облегчили чтение программ, но не упростили их. Для упрощения и ускорения программирования придумали языки высокого уровня: FORTRAN, Algol и сотни других, возложив рутинные операции по созданию машинного кода на компилятор. Те же программы, переписанные на языках высокого уровня, стали гораздо понятнее и короче. Но жизнь потребовала решения более сложных задач, и программы снова увеличились в размерах, стали громоздкими и необозримыми.

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

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

Сложность стоящих перед программистами задач проявилась и тут: программы стали содержать сотни процедур и опять оказались необозримыми. "Кирпичики" стали слишком маленькими. Потребовался новый стиль программирования.

В это же время обнаружилось, что удачная или неудачная структура исходных данных может сильно облегчить или усложнить их обработку. Одни исходные данные удобнее объединить в массив, для других больше подходит структура дерева или стека. Появилось множество исследований различных структур данных и рекомендаций по их применению. Никлаус Вирт, создатель языка Pascal, даже назвал одну из своих книг "Алгоритмы + структуры данных = программы".

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

Для того чтобы обеспечить максимальную независимость модулей друг от друга, надо четко отделить процедуры, которые будут использоваться другими модулями, — открытые (public) процедуры, от вспомогательных, которые обрабатывают данные, заключенные в этот модуль, — закрытых (private) процедур. Для этого модуль делится на две части. Открытые процедуры перечисляются в первой части модуля — интерфейсе (interface), вторые участвуют только во второй его части — реализации (implementation) модуля. Данные, занесенные в модуль, тоже делятся на открытые, указанные в интерфейсе и доступные для других модулей, и закрытые, доступные только для процедур того же модуля. В различных языках программирования это деление производится по-разному. В языке Turbo Pascal модуль специально делится на интерфейс и реализацию, в языке С интерфейс выносится в отдельные "головные" (header) файлы. В языке С++, кроме того, для описания интерфейса можно воспользоваться абстрактными классами. В языке Java есть специальная конструкция для описания интерфейсов, которая так и называется — interface, но можно написать и абстрактные классы.

Так возникла идея о скрытии, инкапсуляции (incapsulation) данных и методов их обработки. Подобные идеи периодически возникают в дизайне бытовой техники. То телевизоры испещряются кнопками и топорщатся ручками и движками на радость любознательному телезрителю, господствует так называемый "приборный" стиль, то вдруг все куда-то пропадает, а на панели управления остаются только кнопка включения телевизора и ручка громкости. Любознательный телезритель, привыкший самостоятельно настраивать свой телевизор, берется за отвертку и лезет внутрь.

Инкапсуляция, конечно, производится не для того, чтобы спрятать от другого модуля что-то любопытное. Здесь преследуются две основные цели. Первая — обеспечить безопасность использования модуля, вынести в интерфейс, сделать общедоступными только те методы обработки информации, которые не могут испортить или уничтожить исходные данные. Вторая цель — уменьшить сложность, скрыв от внешнего мира ненужные детали реализации.

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

В самом деле, каждый объект — робот, автомобиль, человек — обладает определенными характеристиками. Ими могут служить: вес, рост, максимальная скорость, угол поворота, грузоподъемность, фамилия, возраст. Объект способен выполнять какие-то действия: перемещаться в пространстве, поворачиваться, поднимать груз, копать котлован или траншею, расти или уменьшаться, есть, пить, рождаться и умирать, изменяя свои первоначальные характеристики. Удобно смоделировать объект в виде модуля. Его характеристики будут данными, постоянными или переменными, а действия — процедурами.

Оказалось удобным сделать и обратное — разбить программу на модули так, чтобы она превратилась в совокупность взаимодействующих объектов. Так возникло объектноориентированное программирование (object-oriented programming), сокращенно ООП (OOP) — современная парадигма программирования.

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

В виде объектов можно представить совсем неожиданные понятия. Например, окно на экране дисплея — это объект, имеющий ширину width и высоту height, определенное расположение на экране, описываемое обычно координатами (x, у) левого верхнего угла окна, а также шрифт, которым в окно выводится текст, скажем, Times New Roman, цвет фона color, несколько кнопок, полосы прокрутки и другие характеристики. Окно может перемещаться по экрану методом, описанным в какой-нибудь процедуре, скажем, move (), увеличиваться или уменьшаться в размерах каким-нибудь методом size(), сворачиваться в ярлык методом iconify(), как-то реагировать на действия мыши и нажатия клавиш. Это полноценный объект! Кнопки, полосы прокрутки и прочие элементы окна — это тоже объекты со своими характеристиками и действиями: размерами, шрифтами, перемещениями.

Разумеется, считать, что окно само "умеет" выполнять действия, а мы только даем ему поручения: "Свернись, развернись, передвинься", — это несколько неожиданный взгляд на вещи, но ведь сейчас можно подавать команды не только манипуляцией мышью и нажатием клавиш, но и голосом!

Идея объектно-ориентированного программирования оказалась очень плодотворной и стала активно развиваться. Выяснилось, что удобно ставить задачу сразу в виде совокупности действующих объектов — возник объектно-ориентированный анализ, ООА (object-oriented analysis, OOA). Решили проектировать сложные системы в виде объектов — появилось объектно-ориентированное проектирование, ООП (object-oriented design, OOD).

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

Принципы объектно-ориентированного программирования

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

Абстракция

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

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

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

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

Итак, прежде всего нам надо выбрать уровень абстракции, необходимый для правильного описания реального информационного процесса. Затем следует выделить объекты, участвующие в этом процессе, и установить связи между этими объектами. Как это сделать? Опишите процесс словами и проанализируйте получившиеся фразы. "Завод выпускает автомобили". Здесь два объекта — завод и автомобиль. Производственнотехнические характеристики завода составят набор полей объекта "Завод", а процесс выпуска автомобиля будет описан в виде набора методов объекта "Завод".

Пример из другой области: "Преподаватель читает учебный курс". Полями объекта "Преподаватель" будут его фамилия, имя и отчество, научно-педагогический стаж, квалификация, ученая степень, выпущенные им учебники и методические пособия. Методами "Преподавателя" будут такие действия, как "читать", "писать", "повышать квалификацию", "проводить консультацию", "принимать зачет". Полями объекта "Учебный курс" будут его название, программа, количество часов, перечень учебных пособий. Будет ли объект "Учебный курс" обладать какими-то методами или в этом объекте будут только поля? Какие действия выполняет "Учебный курс"? По-видимому, единственным действием объекта "Учебный курс" будет предоставление своих полей другим объектам, значит, нужны методы доступа к полям объекта.

Таким образом, если в словесном описании процесса вам потребовалось сформулировать какое-то понятие, то оно и будет кандидатом на оформление его в виде объекта. Существительные, описывающие это понятие, будут полями объекта, а глаголы — методами будущего объекта.

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

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

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

Кроме полей и методов в классе можно описать и вложенные классы (nested classes), и вложенные интерфейсы, в которые, в свою очередь, можно вложить классы и интерфейсы. Мы можем создать сложную "матрешку" вложенных классов. Поля, методы и вложенные классы первого уровня называются членами класса (class members). Разные школы объектно-ориентированного программирования предлагают разные термины для описания структуры класса, мы используем терминологию, принятую в технологии Java.

Вот набросок описания автомобиля:

class Automobile{

int maxVelocity; // Поле, содержащее наибольшую скорость автомобиля.

int speed; // Поле, содержащее текущую скорость автомобиля.

int weight; // Поле, содержащее вес автомобиля.

// Прочие поля...

void moveTo(int x, int y){

// Метод, моделирующий перемещение автомобиля в точку (x, y).

// Параметры метода x и y — уже не поля, а локальные переменные. int a = 1; // a — локальная переменная, а не поле.

// Тело метода. Здесь описывается способ перемещения автомобиля / / в точку (x, y)

}

// Прочие методы класса...

}

Знатокам Pascal

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

После того как описание класса закончено, можно создавать конкретные объекты, называемые экземплярами (instances) описанного класса. Создание экземпляров производится в три этапа, подобно описанию массивов (см. главу 1). Сначала объявляются ссылки на объекты: записывается имя класса и после пробела через запятую перечисляются экземпляры класса, точнее, ссылки на них.

Automobile lada2110, fordScorpio, oka;

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

lada2110 = new Automobile(); fordScorpio = new Automobile(); oka = new Automobile();

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

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

lada2110.maxVelocity = 150; fordScorpio.maxVelocity = 180; oka.maxVelocity = 350; // Почему бы и нет?

oka.moveTo(35, 120);

Напомним, что текстовая строка в кавычках понимается в Java как объект класса String. Поэтому можно написать

int strlen = "Это объект класса String".length();

Объект "строка" выполняет метод length(), один из методов своего класса String, подсчитывающий количество символов в строке. В результате получаем значение strlen, равное 24. Подобная странная запись встречается в программах, написанных на языке Java, на каждом шагу.

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

Иерархия

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

Иерархия объектов для их классификации используется давно. Особенно детально она проработана в биологии. Все знакомы с семействами, родами и видами. Мы можем сделать описание своих домашних животных (pets): кошек (cats), собак (dogs), коров (cows) и пр. следующим образом:

class Pet{ // Здесь описываем общие свойства всех домашних любимцев

Master person; // Хозяин животного

int weight, age, eatTime[]; // Вес, возраст, время кормления

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

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

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

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

}

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

// Прочее...

}

Затем создаем классы, описывающие более конкретные объекты, связывая их с общим классом Pet:

class Cat extends Pet{ int mouseCatched; void toMouse();

// Прочие свойства

}

class Dog extends Pet{ void preserve();

// Описываются свойства, присущие только кошкам: // число пойманных мышей // процесс ловли мышей

// Свойства собак:

// охранять

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

Dog tuzik = new Dog(), sharik = new Dog();

После этого определения можно будет написать:

tuzik.age = 3;

int p = sharik.eat(30, 10, 12);

А классификацию можно продолжить так:

class Pointer extends Dog{ ... } // Свойства породы пойнтер

class Setter extends Dog{ ... } // Свойства сеттеров

Заметьте, что на каждом следующем уровне иерархии в класс добавляются новые свойства, но ни одно свойство не пропадает. Поэтому и употребляется слово extends — "расширяет", которое сообщает, что класс Dog — расширение (extension) класса Pet. С другой стороны, количество объектов при этом уменьшается: собак меньше, чем всех домашних животных. Поэтому часто говорят, что класс Dog — подкласс (subclass) класса Pet, а класс Pet — суперкласс (superclass) или надкласс класса Dog.

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

В этой терминологии говорят о наследовании (inheritance) классов, в нашем примере можно сказать, что класс Dog наследует класс Pet.

Мы еще не определили счастливого владельца нашего домашнего зоопарка. Опишем его в классе Master. Сделаем набросок описания:

class Master{ // Хозяин животного

String name; // Фамилия, имя

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

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

}

Ответственность

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

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

В англоязычной литературе подобное обращение описывается словом message. Это понятие переведено на русский язык напрямую ни к чему не обязывающим словом "сообщение”. Лучше было бы использовать слово "послание", "поручение" или даже "распоряжение". Но термин "сообщение" устоялся и нам придется его применять. Почему же не используется словосочетание "вызов метода", ведь говорят: "Вызов процедуры"? Дело в том, что между этими понятиями есть по крайней мере три отличия.

? Сообщение идет к конкретному объекту, знающему метод решения задачи. В примере этот объект — текущее значение переменной person. Объекты одного и того же класса отличаются друг от друга. У каждого объекта свое текущее состояние, свои значения полей класса, и это может повлиять на выполнение метода.

? Способ выполнения поручения, содержащегося в сообщении, зависит от объекта, которому оно послано. В нашем примере этот объект — хозяин животного. Один хозяин поставит миску с Chappi, другой бросит кость, третий выгонит собаку на улицу. Это интересное свойство называется полиморфизмом (polymorphism) и будет обсуждаться далее.

? Обращение к методу произойдет только на этапе выполнения программы, компилятор ничего не знает про метод. Это называется "поздним связыванием" в противовес "раннему связыванию", при котором процедура присоединяется к программе на этапе компоновки.

Итак, объект sharik, выполняя свой метод eat (), посылает сообщение объекту, ссылка на который содержится в переменной person, с просьбой выдать ему определенное количество еды и питья. Сообщение записано в строке person.getFood(food, drink).

Этим сообщением заключается контракт (contract) между объектами, суть которого в том, что объект sharik берет на себя ответственность (responsibility) задать правильные параметры в сообщении, а другой объект — текущее значение экземпляра person — возлагает на себя ответственность применить метод кормления getFood(), каким бы он ни был.

Модульность

Для того чтобы правильно реализовать принцип ответственности, применяется четвертый принцип объектно-ориентированного программирования — модульность (modularity).

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

В языке Java инкапсуляция достигается добавлением модификатора private к описанию члена класса. Например:

private int mouseCatched; private String name; private void preserve();

Эти члены классов становятся закрытыми, ими могут пользоваться только экземпляры того же самого класса, например tuzik может дать поручение sharik.preserve ().

А если в классе Master мы напишем

private void getFood(int food, int drink);

то метод getFood () не будет найден объектами других классов и несчастный sharik не сможет получить пищу.

В противоположность закрытости мы можем объявить некоторые члены класса открытыми, записав вместо слова private модификатор public, например:

public void getFood(int food, int drink);

К таким членам может обратиться любой объект любого класса.

Знатокам C++

В языке Java словами private, public и protected отмечается каждый член класса в отдельности.

Принцип модульности предписывает открывать члены класса только в случае необходимости. Вспомните надпись на железнодорожном переезде: "Нормальное положение шлагбаума — закрытое".

Если же надо обратиться к закрытому полю класса, то рекомендуется включить в класс специальные методы доступа (access methods), отдельно для чтения этого поля (get method) и отдельно для записи в это поле (set method). Имена методов доступа рекомендуется начинать со слов get и set, добавляя к этим словам имя поля. Для классов Java, используемых как компоненты большого приложения (такие классы-компоненты в технологии Java названы JavaBeans), эти рекомендации возведены в ранг закона.

В нашем примере класса Master методы доступа к полю name в самом простом виде могут выглядеть так:

public String getName(){ return name;

}

public void setName(String newName){ name = newName;

}

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

Кроме методов доступа рекомендуется создавать проверочные is-методы, возвращающие логическое значение true или false. Например, в класс Master можно включить метод, проверяющий, задано ли имя хозяина:

public boolean isEmpty(){ return name == null;

}

и использовать этот метод для проверки при доступе к полю name, например: if (master01.isEmpty()) master01.setName("Иванов");

Итак, мы оставляем открытыми только методы, необходимые для взаимодействия объектов. При этом удобно спланировать классы так, чтобы зависимость между ними была наименьшей, как принято говорить в теории ООП, было наименьшее зацепление (low coupling) между классами. Тогда структура программы сильно упрощается. Кроме того, такие классы удобно использовать как строительные блоки для создания других программ.

Напротив, члены класса должны активно взаимодействовать друг с другом, как говорят, иметь тесную функциональную связность (high cohesion). Для этого в класс следует включать все методы, описывающие поведение моделируемого объекта, и только такие методы, ничего лишнего. Одно из правил достижения сильной функциональной связности, введенное Карлом Либерхером (Karl J. Lieberherr), получило название закона Деметра. Закон гласит: "В методе m () класса а следует использовать только методы класса а, методы классов, к которым принадлежат параметры метода m(), и методы классов, экземпляры которых создаются внутри метода m()".

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

Будут ли закрытые члены класса доступны его наследникам? Если в классе Pet написано

private Master person;

то можно ли использовать sharik.person? Разумеется, нет. Ведь в противном случае каждый, интересующийся закрытыми полями класса а, может расширить его классом в и просмотреть закрытые поля класса а через экземпляры класса в.

Когда надо разрешить доступ наследникам класса, но нежелательно открывать его всему миру, тогда в Java используется защищенный (protected) доступ, отмечаемый модификатором protected, например объект sharik может обратиться к полю person родительского класса Pet, если в классе Pet это поле описано так:

protected Master person;

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

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

Но, говоря о принципах ООП, я не могу удержаться от того, чтобы не напомнить основной принцип всякого программирования.

Принцип KISS

Основной, базовый и самый великий принцип программирования на любом языке и при любой парадигме — принцип KISS — не нуждается в переводе. Он расшифровывается так:

"Keep It Simple, Stupid!"

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

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

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

При изучении языка программирования важно усвоить его стиль и следовать этому стилю в своих программах. Удивительно, но правильный стиль программирования очень помогает в написании правильных эффективных программ.

Упражнения

1. Опишите в виде объекта строительный подъемный кран.

2. Опишите в виде объекта игровой автомат.

3. Смоделируйте в виде объекта сотовый телефон.

Как описать класс и подкласс?

Итак, описание класса начинается со слова class, после которого записывается имя класса. Соглашения "Code Conventions" рекомендуют начинать имя класса с заглавной буквы.

Перед словом class можно записать модификаторы класса (class modifiers). Это одно из слов public, abstract, final, strictfp. Перед именем вложенного класса можно поставить также модификаторы protected, private, static. Модификаторы класса мы будем вводить по мере изучения языка.

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

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

Описание поля может начинаться с одного или нескольких необязательных модификаторов public, protected, private, static, final, transient, volatile. Если надо поставить несколько модификаторов, то перечислять их JLS рекомендует в указанном порядке, поскольку некоторые компиляторы требуют определенного порядка записи модификаторов. С модификаторами мы будем знакомиться по мере необходимости.

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

Описание метода может начинаться с модификаторов public, protected, private, abstract,

static, final, synchronized, native, strictfp. Мы будем вводить их по необходимости.

В списке параметров через запятую перечисляются тип и имя каждого параметра. Перед типом какого-либо параметра может стоять модификатор final. Такой параметр нельзя изменять внутри метода. Список параметров может отсутствовать, но скобки сохраняются.

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

В листинге 2.1 показано, как можно оформить метод деления пополам для нахождения корня нелинейного уравнения из листинга 1.5.

Листинг 2.1. Нахождение корня нелинейного уравнения методом бисекции

class Bisection2{

private static double final EPS = 1e-8; // Константа класса.

private double a = 0.0, b = 1.5, root; // Закрытые поля экземпляра.

public double getRoot(){return root;} // Метод доступа к полю root.

private double f(double x){

return x*x*x — 3*x*x + 3; // Можно вернуть и что-нибудь другое.

}

private void bisect(){ // Параметров у метода нет —

// метод работает с полями экземпляра. double y = 0.0; // Локальная переменная — не поле.

do{

root = 0.5 *(a + b); y = f(root);

if (Math.abs(y) < EPS) break;

// Корень найден. Выходим из цикла.

// Если на концах отрезка [a; root] функция имеет разные знаки: if (f(a) * y < 0.0) b = root;

// значит, корень здесь, и мы переносим точку b в точку root.

// В противном случае: else a = root;

// переносим точку a в точку root

// Продолжаем до тех пор, пока [a; b] не станет мал.

} while(Math.abs(b-a) >= EPS);

}

public static void main(String[] args){

Bisection2 b2 = new Bisection2(); b2.bisect();

System.out.println("x = " +

b2.getRoot() + // Обращаемся к корню через метод доступа.

", f() = " +b2.f(b2.getRoot()));

}

}

В описании метода f () сохранен старый процедурный стиль: метод получает аргумент, скопированный в параметр x, обрабатывает его и возвращает результат. Описание метода bisect () выполнено в духе ООП: метод активен, он сам обращается к полям экземпляра b2 и сам заносит результат в нужное поле. Метод bisect () — это внутренний механизм класса Bisection2, поэтому он закрыт (private).

Передача аргументов в метод

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

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

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

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

В языке Java, как и в языке С, реализован только один способ — передача аргументов по значению. Например, выполнив следующую программу:

class Dummy1{

private static void f(int a){ a = 5;

}

public static void main(String[] args){

int x = 7;

System.out.println(,,До: " + x); f(x);

System.out.println("После: " + x);

}

}

вы увидите значение 7 и до и после выполнения метода f(), потому что он менял локальную переменную a, а не переменную-аргумент x.

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

class Dummy2{

private static void f(int[] a){ a[0] = 5;

}

public static void main(String[] args){ int[] x = {7};

System.out.println("До: " + x[0]); f (x);

System.out.println("После: " + x[0]);

}

}

Теперь переменная x — это ссылка на массив, которая копируется в локальную переменную, созданную для параметра a. Ссылка a направляется на тот же массив, что и ссылка x. Она меняет нулевой элемент массива, и мы получаем "До: 7", "После: 5". По-прежнему сделана передача аргумента по значению, но теперь аргумент — это ссылка, и в метод f () передается ссылка, а не объект, на который она направлена.

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

class Dummy3{

private static void f(int[] a){ a = new int[]{5};

}

public static void main(String[] args){ int[] x = {7};

System.out.println("До: " + x[0]); f (x);

System.out.println("После: " + x[0]);

}

}

мы опять оба раза увидим на экране число 7. Хотя теперь в методе f() изменилась ссылка на массив — параметр этого метода, а не сам массив, но изменилась копия a ссылки x, а не она сама. Копия a получила новое значение, она направлена на новый массив {5}, но сама ссылка x осталась прежней, она по-прежнему направлена на массив {7}.

Знатокам Pascal и C++

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

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

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

Замечание

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

Например, в классе Automobile мы записали метод moveTo (int x, int y), обозначив пункт назначения его географическими координатами. Можно определить еще метод moveTo(String destination) для указания географического названия пункта назначения и обращаться к нему так:

oka.moveTo("Москва");

Такое дублирование методов называется их перегрузкой (overloading). Перегрузка методов очень удобна в использовании. Вспомните, в главе 1 мы выводили данные любого типа на экран методом println(), не заботясь о том, данные какого именно типа мы выводим. На самом деле мы использовали разные методы с одним и тем же именем println, даже не задумываясь об этом. Конечно, все эти методы надо тщательно спланировать и заранее описать в классе. Это и сделано в классе Printstream, где представлено около двадцати методов print () и println ().

Переопределение методов

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

class Truck extends Automobile{ void moveTo(int x, int y){

// Какие-то действия...

}

// Что-то еще, содержащееся в классе Truck...

}

то он перекроет метод суперкласса.

Определив экземпляр класса Truck, например:

Truck gazel = new Truck();

и записав gazel.moveTo(25, 150), мы обратимся к методу класса Truck. Произойдет переопределение (overriding) метода.

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

Проверку соответствия сигнатуры переопределяемого метода можно возложить на компилятор, записав перед методом подкласса аннотацию ©Override, как это сделано в листинге 2.2. В этом случае компилятор пошлет на консоль сообщение об ошибке, если сигнатура помеченного метода не будет соответствовать сигнатуре ни одного метода суперкласса с тем же именем.

При переопределении метода права доступа к нему можно только расширить, но не сузить. Открытый метод public должен остаться открытым, защищенный protected может стать открытым, но не может стать закрытым.

Можно ли внутри подкласса обратиться к методу суперкласса? Да, можно, если уточнить имя метода словом super, например super.moveTo(30, 40). Можно уточнить и имя метода, записанного в этом же классе, словом this, например this.moveTo(50, 70), но в данном случае это уже излишне. Таким же образом можно уточнять и совпадающие имена полей, а не только методов.

Данные уточнения подобны тому, как мы говорим про себя "я", а не "Иван Петрович", и говорим "отец", а не "Петр Сидорович".

Реализация полиморфизма в Java

Переопределение методов приводит к интересным результатам. В классе Pet мы описали метод voice (). Переопределим его в подклассах и используем в классе Chorus, как показано в листинге 2.2.

Листинг 2.2. Пример полиморфного метода

abstract class Pet{

abstract void voice();

}

class Dog extends Pet{ int k = 10;

©Override void voice(){

System.out.println("Gav-gav!");

}

}

class Cat extends Pet{

©Override void voice(){

System.out.println("Miaou!");

}

}

class Cow extends Pet{

©Override void voice(){

System.out.println("Mu-u-u!");

}

}

public class Chorus{

public static void main(String[] args){ Pet[] singer = new Pet[3]; singer[0] = new Dog(); singer[1] = new Cat(); singer[2] = new Cow();

for (Pet p: singer) p.voice();

}

}

На рис. 2.1 показан вывод этой программы. Животные поют своими голосами!

Рис. 2.1. Результат выполнения программы Chorus

Все дело здесь в определении поля singer [ ]. Хотя массив ссылок singer [ ] имеет тип Pet, каждый его элемент ссылается на объект своего типа: Dog, Cat, Cow. При выполнении программы вызывается метод конкретного объекта, а не метод класса, которым определялось имя ссылки. Так в Java реализуется полиморфизм.

Знатокам C++

В языке Java все методы являются виртуальными функциями.

Внимательный читатель заметил в описании класса Pet новое слово abstract. Класс Pet и метод voice () являются абстрактными.

Упражнения

4. Опишите в виде класса строительный подъемный кран.

5. Опишите в виде класса игровой автомат.

6. Смоделируйте в виде класса сотовый телефон.

Абстрактные методы и классы

При описании класса Pet мы не можем задать в методе voice () никакой полезный алгоритм, поскольку у всех животных совершенно разные голоса.

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

что необходимо указать компилятору модификатором abstract.

Если класс содержит хоть один абстрактный метод, то создать его экземпляры, а тем более использовать их, не удастся. Такой класс становится абстрактным, что обязательно надо указать модификатором abstract.

Как же использовать абстрактные классы? Только порождая от них подклассы, в которых переопределены абстрактные методы.

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

Хотя элементы массива singer[] ссылаются на подклассы Dog, Cat, Cow, но все-таки это переменные типа Pet и ссылаться они могут только на поля и методы, описанные в суперклассе Pet. Дополнительные поля подкласса для них недоступны. Попробуйте обратиться, например, к полю k класса Dog, написав singer[0] .k. Компилятор "скажет", что он не может найти такое поле. Поэтому метод, который реализуется в нескольких подклассах, приходится выносить в суперкласс, а если там его нельзя реализовать, то объявить абстрактным. Таким образом, абстрактные классы группируются на вершине иерархии классов.

Кстати, можно задать пустую реализацию метода, просто поставив пару фигурных скобок, ничего не написав между ними, например:

void voice(){}

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

Замкнуть же иерархию можно окончательными классами.

Окончательные члены и классы

Пометив метод модификатором final, можно запретить его переопределение в подклассах. Это удобно в целях безопасности. Вы можете быть уверены, что метод выполняет те действия, которые вы задали. Именно так определены математические функции sin ( ), cos ( ) и пр. в классе Math. Мы уверены, что метод Math.cos(x) вычисляет именно косинус числа х. Разумеется, такой метод не может быть абстрактным.

Для полной безопасности поля, обрабатываемые окончательными методами, следует сделать закрытыми (private).

Если пометить модификатором final параметр метода, то его нельзя будет изменить внутри метода.

Если же пометить модификатором final весь класс, то его вообще нельзя будет расширить. Так определен, например, класс Math:

public final class Math{ . . . }

Для переменных модификатор final имеет совершенно другой смысл. Если пометить модификатором final описание переменной, то ее значение (а оно должно быть обязательно задано или здесь же, или в блоке инициализации, или в конструкторе) нельзя изменить ни в подклассах, ни в самом классе. Переменная превращается в константу. Именно так в языке Java определяются константы:

public final int MIN_VALUE = -1, MAX_VALUE = 9999;

По соглашению "Code Conventions" константы записываются прописными буквами, слова в них разделяются знаком подчеркивания.

Класс Object

На самой вершине иерархии классов Java стоит класс Object.

Если при описании класса мы не указываем никакое расширение, т. е. не пишем слово extends и имя класса за ним, как при описании класса Pet, то Java считает этот класс расширением класса Object, и компилятор дописывает это за нас:

class Pet extends Object{ . . . }

Можно записать это расширение и явно.

Сам же класс Obj ect не является ничьим наследником, от него начинается иерархия любых классов Java. В частности, все массивы — прямые наследники класса Object.

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

Object objl = new Dog(), obj2 = new Cat(); if (obj1.equals(obj2)) ...

Оцените объектно-ориентированный дух этой записи: объект obj 1 активен, он сам сравнивает себя с другим объектом. Можно, конечно, записать и obj2.equals(obji), сделав активным объект obj 2, с тем же результатом.

Как указывалось в главе 1, ссылки можно сравнивать на равенство и неравенство:

obj 1 == obj 2; obj 1 ! = obj 2;

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

Метод equals () же сравнивает содержимое объектов в их текущем состоянии, фактически он реализован в классе Object как тождество: объект равен только самому себе. Поэтому его обычно переопределяют в подклассах; более того, правильно спроектированные, "хорошо воспитанные" классы должны переопределить методы класса Obj ect, если их не устраивает стандартная реализация. Например, в классе String метод equals () сравнивает не адреса размещения строк в оперативной памяти, а символы, из которых состоит строка, как мы увидим в главе 5.

Второй метод класса Object, часто требующий переопределения,- метод hashCode( ) —

возвращает целое число, уникальное для каждого объекта данного класса, его идентификатор. Это число позволяет однозначно определить объект. Оно используется многими стандартными классами Java. Реализация метода hashCode (), сделанная в классе Obj ect, может оказаться недостаточной для какого-то подкласса. В таком случае метод hashCode () следует переопределить.

Третий метод класса Object, который следует переопределять в подклассах, — метод tostring (). Это метод без параметров, который выражает содержимое объекта строкой символов и возвращает объект класса string. В классе Object метод tostring() реализован очень скудно — он выдает имя класса и идентификатор объекта, возвращаемый методом hashCode (). Метод tostring() важен потому, что исполняющая система Java обращается к нему каждый раз, когда требуется представить объект в виде строки, например в методе println(). Обычно метод tostring() переопределяют так, чтобы он возвращал информацию о классе объекта и текущие значения его полей, записанные в виде строк символов.

Конструкторы класса

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

Такой "метод" называется конструктором класса (class constructor). Его задача — определение полей создаваемого объекта начальными значениями. Своеобразие конструктора заключается не только в имени. Перечислим особенности конструктора.

? Конструктор имеется в любом классе. Даже если вы его не написали, компилятор Java сам создаст конструктор по умолчанию (default constructor), который, впрочем, пуст, он не делает ничего, кроме вызова аналогичного конструктора по умолчанию суперкласса.

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

? Конструктор не возвращает никакого значения. Поэтому в его описании не пишется даже слово void, но можно задать один из трех модификаторов: public, protected или

private.

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

? Тело конструктора может начинаться:

• с вызова одного из конструкторов суперкласса, для этого записывается слово super () с параметрами конструктора суперкласса в скобках, если они нужны;

• с вызова другого конструктора того же класса, для этого записывается слово this () с параметрами в скобках, если они нужны.

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

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

В классе может быть несколько конструкторов. Поскольку у них одно и то же имя, совпадающее с именем класса, то они должны отличаться типом и/или количеством параметров.

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

В наших примерах мы пока ни разу не рассматривали конструкторы классов, поэтому при создании экземпляров наших классов вызывался конструктор класса Object.

Операция new

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

В первом случае в качестве операнда указывается тип элементов массива и количество его элементов в квадратных скобках, например:

double a[] = new double[100];

Элементы массива обнуляются.

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

Числовые поля класса получают нулевые значения, логические поля — значение false, ссылки — значение null.

Результатом операции new будет ссылка на созданный объект. Эта ссылка может быть присвоена переменной типа "ссылка" на данный тип:

Dog k9 = new Dog();

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

new Dog().voice();

Здесь после создания безымянного объекта сразу выполняется его метод voice (). Такая странная запись встречается в программах, написанных на Java, на каждом шагу. Она возможна потому, что приоритет операции new выше, чем приоритет операции обращения к методу, обозначаемой точкой.

Упражнение

7. Введите конструкторы в классы, определенные в упражнениях 4—6.

Статические члены класса

Разные экземпляры одного класса имеют совершенно независимые друг от друга поля, принимающие различные значения. Изменение поля в одном экземпляре никак не влияет на то же поле в другом экземпляре. В каждом экземпляре для таких полей выделяется своя ячейка памяти. Поэтому такие поля называются переменными экземпляра класса (instance variables) или переменными объекта.

Иногда надо определить поле, общее для всего класса, изменение которого в одном экземпляре повлечет изменение того же поля во всех экземплярах. Например, мы хотим в классе Automobile отмечать порядковый заводской номер автомобиля. Такие поля называются переменными класса (class variables). Для переменных класса выделяется только одна ячейка памяти, общая для всех экземпляров. Переменные класса образуются в Java модификатором static. В листинге 2.3 мы записываем этот модификатор при определении переменной number.

Листинг 2.3. Статическое поле

class Automobile!

private static int number;

Automobile(){ number++;

system.out.println("From Automobile constructor:" + " number = " + number);

}

}

public class AutomobileTest{

public static void main(string[] args){

Automobile lada2105 = new Automobile(), fordscorpio = new Automobile(), oka = new Automobile();

}

}

Получаем результат, показанный на рис. 2.2.

Рис. 2.2. Изменение статического поля

Интересно, что к статическим полям можно обращаться с именем класса, Automobile. number, а не только с именем экземпляра, lada2105. number, причем это можно делать, даже если не создан ни один экземпляр класса. Дело в том, что поля класса определяются при загрузке файла с классом в оперативную память, еще до создания экземпляров класса.

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

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

Такая особенность статических методов приводит к интересному побочному эффекту. Они могут выполняться, даже если не создан ни один экземпляр класса. Достаточно уточнить имя метода именем класса (а не именем объекта), чтобы метод мог работать. Именно так мы пользовались методами класса Math, не создавая его экземпляры, а просто записывая

Math.abs (x), Math. sqrt (x). Точно так же мы использовали метод system.out .println ( ) . Да и методом main () мы пользуемся, вообще не создавая никаких объектов.

Поэтому статические методы называются методами класса (class methods), в отличие от нестатических методов, называемых методами экземпляра (instance methods).

Отсюда вытекают другие особенности статических методов:

? в статическом методе нельзя использовать ссылки this и super;

? статические методы не могут быть абстрактными;

? статические методы переопределяются в подклассах только как статические;

? при переопределении статических методов полиморфизм не действует, ссылки всегда направляются на методы класса, а не объекта.

Именно по этим причинам в листинге 1.5 мы пометили метод f() модификатором static. Но в листинге 2.1 мы работали с экземпляром b2 класса Bisection2, и нам не потребовалось объявлять метод f () статическим.

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

static int[] a = new a[10]; static{

for (int k: a) a[k] = k * k;

}

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

Здесь внимательный читатель, наверное, поймал меня: "А говорил, что все действия выполняются только с помощью методов!" Каюсь: блоки статической инициализации и блоки инициализации экземпляра записываются вне всяких методов и выполняются до начала выполнения не то что метода, но даже конструктора.