В лекции 2 мы уже познакомились с первым принципом объектного программирования – инкапсуляцией. Затем научились пользоваться уже готовыми классами. Это – начальная стадия изучения объектного программирования. Для того чтобы овладеть его основными возможностями, требуется научиться создавать собственные классы, изменяющие и усложняющие поведение существующих классов. Важнейшими элементами такого умения является использование наследования и полиморфизма.
Наследование опирается на инкапсуляцию. Оно позволяет строить на основе первоначального класса новые, добавляя в классы новые поля данных и методы. Первоначальный класс называется прародителем (Object. То есть он является базовым для всех классов. Тем не менее, если рассматривается поведение, характерное для объектов какого-то класса и всех потомков этого класса, говорят об иерархии, начинающейся с этого класса.
В этом случае именно он является базовым классом иерархии.
Полиморфизм опирается как на инкапсуляцию, так и на наследование. Как показывает опыт преподавания, это наиболее сложный для понимания принцип. Слово "полиморфизм" в переводе с греческого означает "имеющий много форм". В объектном программировании под полиморфизмом подразумевается наличие кода, написанного для объектов, имеющих тип базового класса иерархии. При этом такой код должен правильно работать для любого объекта, являющегося экземпляром класса из данной иерархии. Независимо от того, где этот класс расположен в иерархии. Такой код и называется полиморфным. При написании полиморфного кода заранее неизвестно, для объектов какого типа он будет работать - один и тот же метод будет исполняться по-разному в зависимости от типа объекта. Пусть, например, у нас имеется класс -"фигура", и в нем заданы методы show() - показать
фигуру на экране, и hide() - скрыть ее. Тогда для переменной типа вызовы и будут показывать или скрывать объект, на который ссылается эта переменная. Причем сам объект "знает", как себя показывать или скрывать, а код пишется на уровне абстракций этих действий.
Основное преимущество объектного программирования по сравнению с процедурным как раз и заключается в возможности написания полиморфного кода. Именно для этого пишется иерархия классов. Полиморфизм позволяет резко увеличить коэффициент повторного использования программного кода и его модифицируемость по сравнению с
В качестве примера того, как строится иерархия, рассмотрим иерархию фигур, отрисовываемых на экране - она показана на рисунке. В ней базовым классом является , от которого наследуются Dot - "точка", - "треугольник" и - "квадрат". От Dot наследуется класс Circle - "окружность", а от Circle унаследуем Ellipse - "эллипс". И, наконец, от унаследуем Rectangle - "прямоугольник".
Отметим, что в иерархии принято рисовать стрелки в направлении от наследника к прародителю. Такое направление называется Generalization - "обобщение", "генерализация". Оно противоположно направлению наследования, которое принято называть Specialization - "специализация". Стрелки символизируют направление в сторону упрощения.
(рис 6.1) Иерархия фигур, отрисовываемых на экранеЧасто класс-прародитель называют суперклассом (
Чем ближе к основанию иерархии лежит класс, тем более общим и универсальным (general) он является. И одновременно - более простым. Класс, который лежит в основе иерархии, называется базовым классом этой иерархии. Базовый класс всегда называют именем, которое характеризует все объекты - экземпляры классов-наследников, и которое выражает наиболее общую абстракцию, применимую к таким объектам. В нашем случае это класс . Любая фигура будет иметь поля данных x и y - координаты фигуры на экране.
Класс Dot ("точка") является наследником , поэтому он будет иметь поля данных x и y, наследуемые от . То есть в самом классе Dot задавать эти поля не надо. От Dot мы наследуем класс Circle ("окружность"), поэтому в нем также имеется поля x и y, наследуемые от . Но появляется дополнительное поле данных. У Circle это поле, соответствующее радиусу. Мы назовем его r. Кроме того, для окружности возможна операция изменения радиуса, поэтому в ней может появиться новый метод, обеспечивающий это действие - назовем его setSize ("установить размер"). Класс Ellipse имеет те же поля данных и обеспечивает то же поведение, что и Circle, но в этом классе появляется дополнительное поле данных r2 - длина второй полуоси эллипса,
и возможность регулировать значение этого поля. Возможен и другой подход, в некотором роде более логичный: считать эллипс сплюснутой или растянутой окружностью. В этом случае необходимо ввести коэффициент растяжения (k. Тогда эллипс будет характеризоваться радиусом r и коэффициентом растяжения k. Метод, обеспечивающий изменение k, назовем stretch ("растянуть"). Обратим внимание, что исходя из выбранной логики действий метод scale должен приводить к изменению поля r и не затрагивать поле k - поэтому эллипс будет масштабироваться без изменения формы.
Каждый из классов этой ветви иерархии фигур можно считать описанием "усложненной точки". При этом важно, что любой объект такого типа можно считать "точкой, которую усложнили". Грубо говоря, считать, что круг или эллипс - это такая "жирная точка". Аналогичным образом Ellipse является "усложненной окружностью"
Аналогично, класс наследует поля x и y, но в нем добавляется поле, соответствующее стороне квадрата. Мы назовем его a. У в качестве новых, не унаследованных полей данных могут выступать координаты вершин треугольника; либо координаты одной из вершин, длины прилегающих к ней сторон и угол между ними, и так далее.
Как располагать классы иерархии, базовый класс внизу, а наследники вверху, образуя ветви дерева наследования, или наоборот, базовый класс вверху а наследники внизу, образуя "корни" дерева наследования - принципиального значения не имеет. По-видимому, на начальном этапе развития объектного программирования применялся первый вариант, почему базовый класс, лежащий в основе иерархии, и получил такое название. Такой вариант выбран в данном учебном пособии, поскольку именно он используется в NetBeans Enterprise Pack. Хотя в настоящее время чаще используют второй вариант, когда базовый класс располагают сверху.
В литературе по объектному программированию часто встречается следующий критерий:"если имеются классы A1 и A2, и можно сказать, что A2 является частным случаем A1, то A2 должен описываться как потомок A1 ". Данный критерий не совсем корректен.
Очень часто встречающийся вариант ошибочных рассуждений, основанный на нем, и приводящий к неправильному построению иерархии, выглядит так:"поскольку Circle является частным случаем Ellipse (при равных длинах полуосей), а Dot является частным случаем Circle (при нулевом радиусе), то класс Ellipse более общий, чем Circle, а Circle - более общий, чем Dot. Поэтому Ellipse должен являться прародителем для Circle, а Circle должен являться прародителем для Dot ". Ошибка заключается в неправильном понимании идей"общности" и"специализации", а также характерной путанице, когда объекты не отличают от классов.
Каждый объект класса-потомка при любых значениях полей должен рассматриваться как экземпляр класса-прародителя, и с тем же поведением на уровне абстракции действий. Но только с некоторыми изменениями на уровне реализации этих действий. В концепции наследования основное внимание уделяется поведению объектов. Объекты с разным поведением имеют другой тип. А значения полей данных характеризуют состояние объекта, но не его тип.
Мы говорим про абстракции поведения как на те характерные действия, которые могут быть описаны на уровне полиморфного кода, безотносительно к конкретной реализации в конкретном классе.
По своему поведению любой объект-эллипс вполне может рассматриваться как экземпляр типа"Окружность" и даже вести себя в точности как окружность. Но не наоборот - объекты типа Окружность не обладает поведением Эллипса. Мы намеренно используем заглавные буквы для того, чтобы не путать классы с объектами. Если для эллипса можно изменить значение aspectRatio ( вызвать метод setAspectRatio ( новое значение ) ), то для окружности такая операция не имеет смысла или запрещена. Аналогично, и для эллипса, и для окружности имеет смысл операция установки нового размера setSize ( новое значение ), а для точки она не имеет смысла или запрещена. И даже если построить неправильную иерархию Ellipse-Circle-Dot и унаследовать от Ellipse эти методы в Circle и Dot, возникнет проблема с их переопределением.
Если setAspectRatio будет менять отношение полуосей нашей"окружности" - она перестанет быть окружностью. Аналогично, если setSize изменит размер точки - та перестанет быть точкой. Если же сделать эти методы ничего не делающими"заглушками" - экземпляры таких потомков не смогут обладать поведением прародителя. Например, мы не сможем вписать окружность в прямоугольник, установив нужное значение aspectRatio - найдутся только три точки, общие для окружности и сторон прямоугольника, а не четыре, как для объекта типа Ellipse. То есть объект типа Circle на уровне абстракции поведения во многих случаях не сможет обладать всеми особенностями поведения объекта типа Ellipse. А значит, Circle не может быть потомком Ellipse.
Можно привести нескончаемое число других примеров того, какие ситуации окажутся нереализуемыми для объектов таких неправильных иерархий. А отдельные хитрости, позволяющие выпутываться из некоторых из таких ситуаций, обычно бывают крайне искусственными, не позволяют решить проблему с очередной внезапно возникшей ситуацией, и только усложняют программу.
Сформулируем критерий того, когда следует использовать наследование, более корректно:" если имеются классы A1 и A2, и можно считать, что A2 является модифицированным (усложненным или измененным) вариантом A1 с сохранением всех особенностей поведения A1 , то A2 должен описываться как потомок A1. - На уровне абстракции, описывающей поведение, объект типа A2 должен вести себя, как объект типа A1 при любых значениях полей данных ".
Специализированный класс, вообще говоря, должен быть устроен более сложно ("расширенно" - extended) по сравнению с прародительским. У него должны иметься дополнительные поля данных и/или дополнительные методы. С этой точки зрения очевидно, что Окружность более специализирована, чем Точка, а Эллипс более специализирован, чем Окружность. Иногда встречаются ситуации, когда потомок отличается от прародителя только своим поведением. У него не добавляется новых полей или методов, а только переопределяется часть методов (возможно, только один). Отметим, что поля или методы, имеющиеся в прародителе, не могут отсутствовать в наследнике - они наследуются из прародителя. Даже если доступ к ним в классе-наследнике закрыт (так бывает в случае, когда поле или метод объявлены с private -"закрытый","частный").
Когда про
Любой наш объект мы можем назвать фигурой. Поэтому то, что базовый класс нашей иерархии называется , естественно и однозначно. И однозначно то, что все классы нашей иерархии должны быть его наследниками. А вот остальные элементы иерархии можно было бы устроить совсем по-другому. Например, так, как показано на следующем рисунке.
(рис 6.2) Альтернативный вариант иерархии фигурВозможно и такое решение: все указанные классы сделать наследниками и расположить на одном уровне наследования.
(рис 6.3) Еще один вариант иерархии фигурВозможны и другие варианты, ничуть не менее логичные. Какой вариант выбрать?
Уже на этом простейшем примере мы убеждаемся, что проектирование иерархии – очень многовариантная задача. И требуется большой опыт, чтобы грамотно построить иерархию. В противном случае при написании кода классов не удается в полной мере обеспечить их функциональность, а код классов становится неуправляемым – внесение исправления в одном месте приводит к возникновению ошибок в совсем других местах. Причем возникает ошибок больше, чем исправляется.
Один из важных принципов при построении таких иерархий – соответствие представлений из предметной области строящейся иерархии. В примере, приведенном на первом рисунке, мы имеем вполне логичную с точки зрения идеологии наследования иерархию, показанную на первом рисунке. С точки зрения общности/специализации такая иерархия безупречна. По этой причине она удобна для написания учебных программ, иллюстрирующих совместимость
Поэтому данная иерархия может вызывать внутренний протест у многих людей. Особенно учитывая сложность различения классов и объектов в обычной речи и при не очень строгих рассуждениях (а можно ли всегда рассуждать абсолютно строго?). Поэтому такое решение может приводить к , во многих случаях предпочтителен. Тем более, что никакого выигрыша при написании программного кода увеличение числа поколений наследования не дает: код, написанный для класса Dot, вряд ли будет использоваться для объектов классов Circle и Ellipse. А ведь наследование само по себе не нужно – это инструмент для написания более экономного полиморфного кода. Более того, увеличение числа поколений приводит к снижению надежности кода. Так что им не следует злоупотреблять.
(Об этом подробнее говорится в одном из параграфов лекции 8).
На выбор варианта иерархии оказывают заметное влияние соображения повторного использования кода – если бы класс Ellipse активно использовал часть кода, написанного для класса Circle, а тот, в свою очередь, активно пользовался кодом класса Dot, выбор первого варианта мог бы стать предпочтительным по сравнению с третьим. Даже несмотря на некоторый конфликт с "обыденными" (не принципиальными!) представлениями предметной области.
Но имеется одна возможность, которую можно реализовать, попытавшись совместить идеи, возникшие при попытках построить предыдущие варианты нашей иерархии. Мы пришли к выводу, что фигуры могут быть масштабируемы (без изменения формы, оставаясь подобными), а также растягиваемы. Поэтому можно ввести классы ScalableFigure ("масштабируемая фигура") и StretchableFigure ("растягиваемая фигура"). Точка Dot не является ни масштабируемой, ни растягиваемой. Очевидно, что любая растягиваемая фигура должна быть масштабируемая. Окружность Circle и квадрат масштабируемы, но не растягиваемы. А прямоугольник Rectangle, эллипс Ellipse и треугольник как масштабируемы, так и растягиваемы. Поэтому наша иерархия будет выглядеть так:
(рис 6.4) Итоговый вариант иерархии фигурОсновное ее преимущество по сравнению с предыдущими – возможность писать полиморфный код для наиболее общих разновидностей фигур. Введение промежуточных уровней наследования, отвечающих соответствующим абстракциям, является характерной чертой объектного программирования. При этом классы , ScalableFigure и StretchableFigure будут абстрактными – экземпляров такого типа создавать не предполагается. Так как не бывает "фигуры", "масштабируемой фигуры" или "растягиваемой фигуры" в общем виде, без указания ее конкретной формы. Точно так же методы show и hide для этих классов также будут абстрактными.
Еще один важный принцип при построении иерархий на первый взгляд может показаться достаточно странным и противоречащим требованию повторного использования кода. Его можно сформулировать так: не использовать код неабстрактных классов для наследования.
Можно заметить, что в приведенной иерархии несколько этапов наследования приходятся именно на Ellipse от Circle, после исправлений в реализации Circle, обеспечивающих правильную работу объектов этого типа, могут возникнуть проблемы при работе объектов типа Ellipse, которые до того работали правильно. Причем речь идет об особенностях реализации конкретного класса, не относящихся к абстракциям поведения.
Продумывание того, как устроены классы, то есть какие в них должны быть поля и методы (без уточнения об конкретной реализации этих методов), и описание того, какая должна быть
В языке Java, к сожалению, отсутствуют адекватные средства для проектирования классов. Более того, в этом отношении он заметно уступает таким языкам как C++ или
Пока в этой среде нет возможности по
Основой создания новых классов является задание полей данных и методов. Но если поля отражают структуру данных, связанных с объектом или классом, то методы задают поведение объектов, а также работу с полями данных объектов и классов.
Формат
Модификаторы Тип Имя(список параметров){
Тело функции
}
Это формат "простой" функции, не содержащей операторов, возбуждающих исключительные ситуации. Про исключительные ситуации и формат
Комбинация элементов декларации метода Модификаторы Тип Имя(список параметров) называется заголовком метода.
Модификаторы – это зарезервированные слова, задающие
В качестве Типа следует указать тип результата, возвращаемого методом. В Java, как мы уже знаем, все методы являются функциями, возвращающими значение какого-либо типа. Если требуется метод, не возвращающий никакого значения (то есть процедура), он объявляется с типом void. Возврат значения осуществляется в теле функции с помощью зарезервированного слова return.
Для выхода без возврата значения требуется написать
return;
Для выхода с возвратом значения требуется написать
return выражение ;
Выражение будет вычислено, после чего полученное значение возвратится как результат работы функции.
Оператор return осуществляет прерывание выполнения подпрограммы, поэтому его обычно используют в ветвях операторов if-else или switch-case в случаях, когда необходимо возвращать тот или иной результат в зависимости от различных условий. Если в подпрограмме-функции в какой-либо из ветвей не использовать оператор return, будет выдана ошибка компиляции с диагностикой "missing
Список параметров – это объявление через запятую переменных, с помощью которых можно передавать значения и объекты в подпрограмму снаружи, "из внешнего мира", и передавать объекты из подпрограммы наружу, "во внешний мир".
Объявление параметров имеет вид
тип1 имя1, тип2 имя2,…, типN имяN
Если список параметров пуст, пишут круглые скобки без параметров.
Тело функции представляет последовательность операторов, реализующую необходимый алгоритм. Эта последовательность может быть пустой, в этом случае говорят о заглушке – то есть заготовке метода, имеющей только имя и список параметров, но с отсутствующей реализацией. Иногда в такой заглушке вместо "правильной" реализации временно пишут операторы служебного вывода в консольное окно или в файл.
Внутри тела функции в произвольном месте могут задаваться переменные. Они доступны только внутри данной подпрограммы и поэтому называются локальными. Переменные, заданные на уровне класса (поля данных класса или объекта), называются глобальными.
Данные (значения или объекты) можно передавать в подпрограмму либо через список параметров, либо через глобальные переменные.
Сначала рассмотрим передачу в подпрограмму через список параметров значений примитивного типа. Предположим, что мы написали в классе MyMath метод mult1 умножения двух чисел, каждое из которых перед умножением увеличивается на 1. Он может выглядеть так:
double mult1(double x, double y){
x++;
y++;
return x*y;
}
Вызов данного метода может выглядеть так:
double a,b,c; … MyMath obj1=new MyMath();//создали объект типа MyMath … c=obj1.mult1(a+0.5,b);
Параметры, указанные в заголовке функции при ее декларации, называются формальными. А те параметры, которые подставляются во время вызова функции, называются фактическими.
В нашем случае x и y являются формальными параметрами, а выражения a+0.5 и b – x будет скопировано значение, получившееся в результате вычисления a+0.5, а в локальную переменную y – значение, хранящееся в переменной b. После чего с локальными переменными происходят все те действия, которые указаны в реализации метода. Соответствие фактических и формальных параметров идет в порядке перечисления. То есть первый фактический параметр соответствует первому формальному, второй фактический – второму формальному, и так далее.
Фактические параметры должны быть совместимы с формальными – при этом действуют все правила, относящиеся к совместимости mult1 можно вместо параметров типа double в качестве фактических использовать значения типа int или float. А если бы float, то использовать фактические параметры типа int было бы можно, а типа double – нельзя.
Влияет ли как-нибудь увеличение переменной y на 1, происходящее благодаря оператору y++, на значение, хранящееся в переменной b? Конечно, нет. Ведь действия происходят с локальной переменной y, в которую при начале вызова было скопировано значение из переменной b. С самой переменной b в результате вызова ничего не происходит.
А можно ли сделать так, чтобы подпрограмма изменяла значение в передаваемой в нее переменной? – Нет, нельзя. В Java значения примитивного типа наружу, к сожалению, передавать нельзя, в отличие от подавляющего большинства других языков программирования. Применяемый в Java способ
Иногда бывает нужно передать в подпрограмму неизменяемую константу. Конечно, можно проверить, нет ли где-нибудь оператора, изменяющего соответствующую переменную. Но надежней проверка на уровне синтаксических конструкций. В этих целях используют модификатор final. Предположим, что увеличивать на 1 надо только первый параметр, а второй должен оставаться неизменным. В этом случае наш метод выглядел бы так:
double mult1(double x, final double y){
x++;
return x*y;
}
А вот при компиляции такого кода будет выдано сообщение об ошибке:
double mult1(double x, final double y){
x++;
y++;
return x*y;
}
Как уже говорилось, данные в подпрограмму могут передаваться через глобальные переменные. Это могут быть поля данных объекта, в методе которого осуществляется вызов, поля данных соответствующего класса, либо поля данных другого объекта или класса. Использование глобальных переменных не рекомендуется по двум причинам.
Конечно, бывают случаи, когда использование глобальных переменных не только желательно, а просто необходимо – иначе их не стали бы вводить как конструкцию языков программирования! Например, при написании метода в каком-либо классе обычно необходимо получать доступ к полям и методам этого класса. В Java такой доступ осуществляется напрямую, без указания имени объекта или класса.
Правила доступа к методам и полям данных (переменным) из других пакетов, классов и объектов задаются с помощью модификаторов private, protected, public. Правила доступа часто называются также правилами видимости, это синонимы. Если переменная или подпрограмма невидимы в некой области программы, доступ к ним запрещен.
private - элемент (поле данных или метод) доступен только в методах данного класса. Доступа из объектов нет! То есть если мы создали объект, у которого имеется поле или метод private, то получить доступ к этому полю или методу из объекта нельзя.
Модификатор не задан - значит, действует доступ по умолчанию – так называемый пакетный, когда соответствующий элемент доступен только из классов своего пакета. Доступа из объектов нет, если они вызываются в операторах, расположенных в классах из других пакетов!
Иногда, по аналогии с C++, этот тип доступа называют "дружественным".
protected - элемент доступен только в методах данного класса, данного пакета, а также классах-наследниках (они могут располагаться в других пакетах).
public - элемент доступен из любых классов и объектов (с квалификацией именем пакета, если соответствующий класс не импортирован).
Например, в классе
class Vis1 {
private int x=10,y=10;
int p1=1;
protected int p2=1;
public int p3=1;
}
заданы переменные x,y,p1,p2,p3. Причем x и y обладают уровнем доступа private, p1 – пакетным, p2 – protected, p3 – public. Перечисление однотипных переменных через запятую позволяет использовать для нескольких переменных однократное задание имени типа и модификаторов, без повторений.
Как уже говорилось, локальные переменные можно вводить в любом месте подпрограммы. Их можно использовать в данном методе только после места, где они заданы. Областью существования и видимости локальной переменной является часть программного кода от места объявления переменной до
А вот переменные, заданные на уровне класса (глобальные переменные), создаются при создании объекта для методов объекта, и при первом вызове класса для
Еще одной важной особенностью локальных переменных является время их существования: под них выделяется память в момент вызова, а высвобождается сразу после окончания вызова. Рассмотрим функцию, вычисляющую сумму чисел от 1 до n:
double sum1(int n){
int i;
double r=0;
for(i=1;i<=n;i++){
r+=i;
};
return r;
}
Вызов данного метода может выглядеть так:
c=obj1.sum1(1000);
При этом переменные i и r существуют только во время вызова obj1.sum1(1000). При следующем аналогичном вызове будет создан, а затем высвобожден из памяти следующий комплект i и r.
Все сказанное про локальные переменные также относится и к объектным переменным. Но не следует путать переменные и объекты: время жизни объектов гораздо больше. Даже если объект создается во время вызова подпрограммы, а после окончания этого вызова связь с ним кончается. Уничтожением неиспользуемых объектов занимается сборщик мусора (
Остановимся на области видимости локальной переменной. Имеются следующие уровни видимости:
{…}, она видна от места декларации до конца блока. Блоки могут быть вложены один в другой с произвольным уровнем вложенности.for. Переменная видна от места декларации в Глобальные переменные видны во всей подпрограмме.
Каждый объект имеет поле данных с именем this ("этот" – данное не слишком удачное обозначение унаследовано из C++), в котором хранится ссылка на сам этот объект. Поэтому доступ в методе объекта к полям и методам этого объекта может осуществляться либо напрямую, либо через ссылку this на этот объект. Например, если у объекта имеется поле x и метод show(), то this.x означает то же, что x, а this.show() – то же, show(). Но в случае перекрытия области видимости, о чем речь пойдет чуть ниже, доступ по короткому имени оказывается невозможен, и приходится использовать доступ по ссылке this. Отметим, что ссылка this позволяет обойтись без использования имени
Ссылка this не может быть использована в методах класса (то есть заданных с модификатором static), поскольку они могут вызываться без существующего объекта.
Встает вопрос о том, что произойдет, если на разных уровнях будет задано две переменных с одним именем. Имеется следующие варианты ситуаций:
this. Например, если имя поля данных x, и имя параметра в методе тоже x, установка значения поля выглядит так:void setX(double x){
this.x=x
}
this.for или внутри какого-нибудь блока, ограниченного фигурными скобками {…}, задается локальная переменная с тем же именем. В Java такая ситуация разрешена. При этом внутри цикла или блока доступна заданная в нем локальная переменная, а глобальная переменная видна через ссылку this.for или внутри какого-нибудь блока, ограниченного фигурными скобками {…}, задается локальная переменная с тем же именем. В Java такая ситуация запрещена. При этом выдается ошибка компиляции с информацией, что переменная с таким именем уже задана ("is already defined").При передаче в подпрограмму ссылочной переменной возникает ряд отличий по сравнению со случаем
Для примера создадим в нашем пакете класс Location. Он будет служить для задания объекта соответствующего типа, который будет передаваться через список параметров в метод m1, вызываемый из нашего приложения.
public class Location {
public int x=0,y=0;
public Location (int x, int y) {
this.x=x;
this.y=y;
}
}
А в
Location locat1=new Location(10,20);
public static void m1(Location obj){
obj.x++;
obj.y++;
}
Мы задали переменную locat1 типа Location, инициализировав ее поля x и y значениями 10 и 20. А в методе m1 происходит увеличение на 1 значения полей x и y объекта, связанного с obj.
Создадим две кнопки с обработчиками событий. Нажатие на первую кнопку будет приводить к выводу информации о значениях полей x и y объекта, связанного с переменной locat1. А нажатие на вторую – к вызову метода m1.
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
System.out.println("locat1.x="+locat1.x);
System.out.println("locat1.y="+locat1.y);
}
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
m1(locat1);
System.out.println("Прошел вызов m1(locat1)");
}
Легко проверить, что вызов m1(locat1) приводит к увеличению значений полей locat1.x и locat1.y.
При передаче в подпрограмму ссылочной переменной имеется особенность, которая часто приводит к ошибкам – потеря связи с первоначальным объектом при изменении ссылки. Модифицируем наш метод m1:
public static void m1(Location obj){
obj.x++;
obj.y++;
obj=new Location(4,4);
obj.x++;
obj.y++;
}
После первых двух строк, которые приводили к инкременту полей передаваемого объекта, появилось создание нового объекта и перещелкивание на него локальной переменной obj, а затем две точно такие же строчки, как в начале метода. Какие значения полей x и y объекта, связанного с переменной locat1 покажет нажатие на кнопку 1 после вызова модифицированного варианта метода? Первоначальный и модифицированный вариант метода дадут одинаковые результаты!
Дело в том, что присваивание obj=new Location(4,4); приводит к тому, что переменная obj становится связанной с новым, только что созданным объектом. И изменение полей данных в операторах obj.x++ и obj.y++ происходит уже для этого объекта. А вовсе не для того объекта, ссылку на который передали через список параметров.
Следует обратить внимание на то, какая терминология используется для описания программы. Говорится "ссылочная переменная" и "объект, связанный со ссылочной переменной". Эти понятия не отождествляются, как часто делают программисты при описании программы. И именно строгая терминология позволяет разобраться в происходящем. Иначе трудно понять, почему оператор obj.x++ в одном месте метода дает совсем не тот эффект, что в другом месте. Поскольку если бы мы сказали "изменение поля x объекта obj", было бы невозможно понять, что объекты-то разные! А правильная фраза "изменение поля x объекта, связанного со ссылочной переменной obj " подталкивает к мысли, что эти объекты в разных местах программы могут быть разными.
Способ передачи данных (ячейки памяти) в подпрограмму, позволяющий изменять содержимое внешней ячейки памяти благодаря использованию ссылки на эту ячейку, называется передачей по ссылке. И хотя в Java объект передается по ссылке,
Рассмотрим теперь нетривиальные ситуации, которые часто возникают при передаче ссылочных переменных в качестве параметров.
Мы уже упоминали о проблемах, возникающих при работе со строками. Рассмотрим подпрограмму, которая, по идее, должна бы возвращать с помощью переменной s3 сумму строк, хранящихся в переменных s1 и s2:
void strAdd1(String s1,s2,s3){
s3=s1+s2;
}
Строки в Java являются объектами, и строковые переменные являются ссылочными. Поэтому можно было бы предполагать возврат измененного состояния строкового объекта, с которым связана переменная s3. Но все обстоит совсем не так: при вызове
obj1.strAdd1(t1,t2,t3);
значение строковой переменной t3 не изменится. Дело в том, что в Java строки типа String являются неизменяемыми объектами, и вместо изменения состояния прежнего объекта в результате вычисления выражения s1+s2 создается новый объект. Поэтому присваивание s3=s1+s2 приводит к перещелкиванию ссылки s3 на этот новый объект. А мы уже знаем, что это ведет к тому, что новый объект оказывается недоступен вне подпрограммы – "внешняя" переменная t3 будет ссылаться на прежний объект-строку. В данном случае, конечно, лучше сделать функцию strAdd1 строковой, и возвращать получившийся строковый объект как результат вычисления этой функции.
Еще пример: пусть нам необходимо внутри подпрограммы обработать некоторую строку и вернуть измененное значение. Допустим, в качестве
Напишем в классе нашего приложения такой код:
String componentName="myComponent";
int count=0;
public void calcName1(String name) {
count++;
name+=count;
System.out.println("Новое значение="+name);
}
Создадим в нашем приложении кнопку, при нажатии на которую срабатывает следующий обработчик события:
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
calcName1(componentName);
System.out.println("componentName="+componentName);
}
Многие начинающие программисты считают, что раз строки являются объектами, то при первом нажатии на кнопку значение componentName станет "myComponent1", при втором – "myComponent2", и так далее. Но значение myComponent остается неизменным, хотя в методе calcName1 новое значение выводится именно таким, как надо. В чем причина такого поведения программы, и каким образом добиться правильного результата?
Если мы меняем в подпрограмме значение полей у объекта, а ссылка на объект не меняется, то изменение значения полей оказывается наблюдаемым с помощью доступа к тому же объекту через внешнюю переменную. А вот присваивание строковой переменной внутри подпрограммы нового значения приводит к созданию нового объекта-строки и перещелкивания на него ссылки, хранящейся в локальной переменной name. Причем глобальная переменная componentName остается связанной с первоначальным объектом-строкой "myComponent".
Как бороться с данной проблемой? Существует несколько вариантов решения.
Во-первых, в данном случае наиболее разумно вместо подпрограммы-процедуры, не возвращающей никакого значения, написать подпрограмму-функцию, возвращающую значение типа String:
public String calcName2(String name) {
count++;
name+=count;
return name;
}
В этом случае не возникает никаких проблем с возвратом значения, и следующий обработчик нажатия на кнопку это демонстрирует:
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
componentName=calcName2(componentName);
System.out.println("componentName="+componentName);
}
К сожалению, если требуется возвращать более одного значения, данный способ решения проблемы не подходит. А ведь часто из подпрограммы требуется возвращать два или более измененных или вычисленных значения.
Во-вторых, можно воспользоваться глобальной строковой переменной – но это плохой стиль программирования. Даже использование глобальной переменной count в предыдущем примере не очень хорошо – но мы это сделали для того, чтобы не
В-третьих, возможно создание оболочечного объекта ( ), у которого имеется поле
В-четвертых, имеется возможность использовать классы StringBuffer или StringBuilder. Это наиболее адекватный способ при необходимости возврата более чем одного значения, поскольку в этой ситуации является и самым простым, и весьма эффективным по быстродействию и используемым ресурсам. Рассмотрим соответствующий код.
public void calcName3(StringBuffer name) {
count++;
name.append(count);
System.out.println("Новое значение="+name);
}
StringBuffer sbComponentName=new StringBuffer();
{sbComponentName.append("myComponent");}
private void jButton8ActionPerformed(java.awt.event.ActionEvent evt){
calcName3(sbComponentName);
System.out.println("sbComponentName="+sbComponentName);
}
Вместо строкового поля componentName мы теперь используем поле sbComponentName типа StringBuffer. Почему-то разработчики этого класса не догадались сделать в нем конструктор с параметром sbComponentName присваивается нетривиальное начальное значение. В остальном код очевиден. Принципиальное отличие от использования переменной типа String – то, что изменение значения строки, хранящейся в переменной StringBuffer, не приводит к созданию нового объекта, связанного с этой переменной.
Вообще говоря, с этой точки зрения для работы со строками переменные типа StringBuffer и StringBuilder подходят гораздо лучше, чем переменные типа String. Но метода toStringBuffer() в классах не предусмотрено. Поэтому при использовании переменных типа StringBuffer обычно приходится пользоваться конструкциями вида sb.append ( выражение ). В методы append и insert можно передавать выражения произвольных примитивных или
int[] a=new int[]{10,11,12};
System.out.println("a="+a);
был получен следующий результат:
a=[I@15fea60
И выводимое значение не зависело ни от значений элементов массива, ни от их числа.
Наличие автоматической упаковки-распаковки также приводит к проблемам. Пусть у нас имеется случай, когда в списке параметров указана
void m1(Double d){
d++;
}
Несмотря на то, что переменная d объектная, изменение значения d внутри подпрограммы не приведет к изменению снаружи подпрограммы по той же причине, что и для переменных типа String. При инкременте сначала производится распаковка в тип double, для которого выполняется оператор "++". После чего выполняется упаковка в новый объект типа Double, с которым становится связана переменная d.
Приведем еще один аналогичный пример:
public void proc1(Double d1,Double d2,Double d3){
d3=d1+sin(d2);
}
Надежда на то, что в объект, передаваемый через параметр d3, возвратится вычисленное значение d3=d1+sin(d2), является ошибочной, так как при упаковке вычисленного результата создается новый объект.
Таким образом, объекты стандартных оболочечных числовых классов не позволяют возвращать измененное числовое значение из подпрограмм, что во многих случаях вызывает проблемы. Для этих целей приходится писать собственные оболочечные классы. Например:
public class UsableDouble{
Double value=0;
UsableDouble(Double value){
this.value=value;
}
}
Объект UsableDouble d можно передавать в подпрограмму по ссылке и без проблем получать возвращенное измененное значение. Аналогичного рода оболочные классы легко написать для всех
Если бы в стандартных оболочечных классах были методы, позволяющие изменить числовое значение, связанное с объектом, без изменения адреса объекта, в такого рода деятельности не было бы необходимости.
Заканчивая разговор о проблемах
В объектном программировании принято использовать имеющиеся классы в качестве "заготовок" для создания новых классов, которые на них похожи, но обладают более сложной структурой и/или отличающимся поведением. Такие "заготовки" называются прародителями (
В C++ и Java вместо терминов "прародители" и "потомки" чаще используют неудачные названия "
При задании класса-потомка сначала идут модификаторы, затем после ключевого слова class идет имя декларируемого класса, затем идет зарезервированное слово extends ("расширяет"), после чего требуется указать имя класса-родителя (непосредственного прародителя). Если не указывается, от какого класса идет наследование, родителем считается класс Object. Сам
В синтаксисе Java словом extends подчеркивается, что потомок расширяет то, что задано в прародителе – добавляет новые поля, методы, усложняет поведение. (Но все это делает класс более специализированным, менее общим).
Далее в фигурных скобках идет реализация класса – описание его полей и методов. При этом поля данных и методы, имеющиеся в прародителе, в потомке описывать не надо – они наследуются. Однако в случае, если реализация прародительского метода нас не устраивает, в классе-потомке его можно реализовать по другому. В этом случае метод необходимо продекларировать и реализовать в классе-потомке. Кроме того, в потомке можно задавать новые поля данных и методы, отсутствующие в прародителях.
Модификаторы, которые можно использовать:
final ) , то есть что у него не может быть потомков.Таким образом, задание класса-наследника имеет следующий формат:
Модификаторы class ИмяКласса extends ИмяРодителя {
Задание полей;
Задание подпрограмм - методов класса, методов объекта, конструкторов
}
Данный формат относится к классам, не реализующим интерфейсы ( interfaces ). Работе с интерфейсами будет посвящен отдельный раздел.
Рассмотрим в качестве примера наследование для классов описанной ранее иерархии фигур. Для простоты выберем вариант, в котором - это класс-прародитель иерархии, Dot его потомок, а Circle - потомок Dot (то есть является "жирной точкой"). Напомним, что имена классов принято начинать с заглавной буквы.
Класс опишем как абстрактный – объектов такого типа создавать не предполагается, так как фигура без указания конкретного вида – это, действительно, чистая абстракция. По той же причине методы show ("показать") и hide ("скрыть") объявлены как абстрактные. Напомним также, что если в классе хоть один метод является абстрактным, это класс обязан быть объявлен как абстрактный.
public abstract class Figure { //это абстрактный класс
int x=0;
int y=0;
java.awt.Color color;
java.awt.Graphics graphics;
java.awt.Color bgColor;
public abstract void show(); //это абстрактный метод
public abstract void hide(); //это абстрактный метод
public void moveTo(int x, int y){
hide();
this.x= x;
this.y= y;
show();
};
}
Поля x и y задают координаты фигуры, а color – ее цвет. Соответствующий тип задан в пакете java.. Поле graphics задает ссылку на графическую поверхность, по которой будет идти отрисовка фигуры. Соответствующий тип также задан в пакете java.. В отличии от полей x, y и color для этого поля при написании класса невозможно задать начальное значение, и оно будет присвоено при создании объекта. То же относится к полю bgColor (от "hide для того, чтобы она перестала показываться на экране. Это не самый лучший, но зато самый простой способ скрыть фигуру. В дальнейшем при желании реализацию метода можно изменить – это никак не коснется остальных частей программы.
В параграфе, посвященном конструкторам, в классе FilledCircle мы применим более совершенный способ отрисовки и "скрывания" фигур, основанный на использовании режима рисования XOR ("исключающее или"). Установка этого режима производится методом setXORMode. Такой режим можно использовать для всех наших фигур.
Метод moveTo имеет реализацию несмотря на то, что класс абстрактный, и в этой реализации используются имена show и hide. Этот вопрос будет подробно обсуждаться в следующем параграфе, посвященном полиморфизму.
Рассмотрим теперь, как задается потомок класса – класс Dot ("Точка"). Для Dot классы Object и будут являться прародителями ( будет непосредственным прародителем. Соответственно, для них класс Dot будет являться потомком ( – непосредственным потомком. Класс Dot расширяет (extends) функциональность класса : хотя в нем и не появляется новых полей, зато пишется реализация для методов show и hide, которые в прародительском классе были абстрактными. В классе мы использовали классы пакета java. без импорта этого пакета. В классе Dot используется импорт – обычно это удобнее, так как не надо много раз писать длинные имена.
package java_gui_example;
import java.awt.*;
/**
* @author В.В.Монахов
*/
public class Dot extends Figure{
/** Создает новый экземпляр типа Dot */
public Dot(Graphics graphics,Color bgColor) {
this.graphics=graphics;
this.bgColor=bgColor;
}
public void show(){
Color oldC=graphics.getColor();
graphics.setColor(Color.BLACK);
graphics.drawLine(x,y,x,y);
graphics.setColor(oldC);
}
public void hide(){
Color oldC=graphics.getColor();
graphics.setColor(bgColor);
graphics.drawLine(x,y,x,y);
graphics.setColor(oldC);
;
}
}
Отметим, что в классе Dot не задаются поля x, y, graphics и метод moveTo – они наследуются из класса . А методы show и hide переопределяются (override) – для них пишется реализация, соответствующая тому, каким именно образом точка появляется и скрывается на экране.
Конструктор Dot(Graphics graphics, Color bgColor) занимается созданием объекта типа Dot и инициализацией его полей. В методах show и hide используются методы объекта graphics. В методе show сначала во временной переменной oldC сохраняется информация о текущем цвете рисования. Затем в качестве текущего цвета устанавливается черный цвет (константа java. ). Затем вызывается метод, рисующий точку, в качестве него используется рисование линии с совпадающими началом и концом. После чего восстанавливается первоначальный цвет рисования. Это необходимо для того, чтобы не повлиять на поведение других объектов, пользующихся для каких-либо целей текущим цветом. Такого рода действия являются очень характерными при пользовании разделяемыми (shared) ресурсами.
Если вам при работе какого-либо метода требуется изменить состояние разделяемых внешних данных, сначала требуется сохранить информацию о текущем состоянии, а в конце вызова восстановить это состояние.
Термин override ("переопределить") на русский язык часто переводят как "перекрыть". Это может вводить в заблуждение, так как имеется еще одно понятие – перекрытие области видимости ( hiding ). Такое перекрытие возникает в случае, когда в классе-потомке задается поле с тем же именем, что и в прародителе (но, возможно, другого типа). Для методов совпадение имен разрешено, в том числе с именами глобальных и локальных переменных.
Имя метода в сочетании с числом параметров и их типами называется его сигнатурой. А
Если контракт задаваемого метода совпадает с контрактом прародительского метода, говорят, что метод переопределен. Если у двух методов имена совпадают, но сигнатуры различны – говорят, что производится перегрузка (
В классе нашего приложения создадим на background. Затем зададим переменную dot, которой назначим объект в обработчике нажатия на кнопку:
Dot dot=new Dot(jPanel1.getGraphics(),jPanel1.getBackground());
После создания объекта-точки с помощью переменной dot можно вызывать методы show и hide:
dot.show(); dot.hide();
Создадим на форме пункты ввода/редактирования текста jTextField1 и jTextField2. В этом случае становится можно вызывать метод moveTo, следующим образом задавая координаты, куда должна перемещаться точка:
int newX=Integer.parseInt(jTextField1.getText()); int newY=Integer.parseInt(jTextField2.getText()); dot.moveTo(newX,newY);
Наш пример оказывается достаточно функциональным для того, чтобы увидеть работу с простейшим объектом.
Рассмотрим теперь класс ScalableFigure ("Масштабируемая фигура"), . Он очень прост.
package java_gui_example;
public abstract class ScalableFigure extends Figure{
int size;
public void resize(int size) {
hide();
this.size=size;
show();
}
}
Класс ScalableFigure является абстрактным – объектов такого типа создавать не предполагается, так как масштабируемая фигура без указания конкретного вида – это абстракция. По этой же причине в классе не заданы реализации методов show и hide.
Зато появилось поле size ("размер"), и метод ("изменить размер"), расширяющий этот класс по сравнению с прародителем. Для того, чтобы изменить размер фигуры, отрисовываемой на экране, надо не только присвоить полю size новое значение, но и правильно перерисовать фигуру. Сначала надо ее скрыть, затем изменить значение size, после чего показать на экране – уже нового размера. Следует обратить внимание, что мы пишем данный код на уровне абстракций, для нас не имеет значения, какого типа будет фигура – главное, чтобы она была масштабируемая, то есть являлась экземпляром класса-потомка ScalableFigure. О механизме, позволяющем такому коду правильно работать, будет рассказано далее в параграфе, посвященном полиморфизму.
Опишем класс Circle ("Окружность"), ScalableFigure.
package java_gui_example;
import java.awt.*;
public class Circle extends ScalableFigure {
Circle(Graphics g,Color bgColor, int r){ //это конструктор
graphics=g;
this.bgColor=bgColor;
size=r;
}
public void show(){
Color oldC=graphics.getColor();
graphics.setColor(Color.BLACK);
graphics.drawOval(x,y,size,size);
graphics.setColor(oldC);
}
public void hide(){
Color oldC=graphics.getColor();
graphics.setColor(bgColor);
graphics.drawOval(x,y,size,size);
graphics.setColor(oldC);
}
};
В классе Circle не задается новых полей – в качестве радиуса окружности используется поле size, унаследованное от класса ScalableFigure. Зато введен конструктор, позволяющий задавать радиус при создании окружности.
Кроме того, написаны новые реализации для методов show и hide, поскольку окружность показывается, скрывается и движется по экрану не так, как точка.
Таким образом, усложнение структуры Circle по сравнением со ScalableFigure в основном связано с появлением реализации у методов, которые до этого были абстрактными. Очевидно, класс Circle является более специализированным по сравнению со ScalableFigure, не говоря уж о .
Поля x, y, color, bgColor, graphics и метод moveTo наследуется в Circle из класса . А из ScalableFigure наследуются поле size и метод .
Следует особо подчеркнуть, что наследование относится к классам, а не к объектам. Можно говорить, что один класс является наследником другого. Но категорически нельзя – что один объект является наследником другого объекта. Иногда говорят фразы вроде "объект circle является наследником ". Это не страшно, если подразумевается, что "объект circle является экземпляром класса-наследника ". Слишком долго произносить правильную фразу. Но следует четко понимать, что имеется в виду, и злоупотреблять такими оборотами не следует. Класс Circle является непосредственным (прямым) потомком ScalableFigure, а ScalableFigure – непосредственным (прямым) прародителем класса Circle. То есть для ScalableFigure класс Circle является Circle класс ScalableFigure является ScalableFigure, и Circle. А для Circle ScalableFigure, и .
Поскольку в Java все классы — потомки класса Object, то Object является прародителем и для , и для ScalableFigure, и для Circle. Но непосредственным прародителем он будет только для .
В данном параграфе рассматривается ряд нетривиальных ситуаций, связанных с правилами видимости при наследовании.
Поля и методы, помеченные как private ("закрытый, частный") наследуются, но в классах-наследниках недоступны. Это сделано в целях обеспечения безопасности. Пусть, например, некий класс Password1 обеспечивает проверку правильности пароля, и у него имеется строковое поле password ("пароль"), в котором держится пароль и с которым сравнивается введенный пользователем пароль. Если оно имеет тип public, такое поле общедоступно, и сохранить его в тайне мы не сможем. При отсутствии protected на первый взгляд имеется необходимое ограничение доступа. Но если мы напишем класс Password2, являющийся наследником от Password1, в нем легко написать метод, "вскрывающий" пароль:
public String getPass(){
return password;
};
Если же поставить модификатор private, то в потомке до прародительского поля password не добраться!
То, что private -поля наследуются, проверить достаточно просто: зададим класс
public class TestPrivate1 {
private String s="Значение поля private";
public String get_s(){
return s;
}
}
и его потомок, который просто имеет другое имя, но больше ничего не делает:
public class TestPrivate2 extends TestPrivate1 {
}
Если из объекта, являющегося экземпляром TestPrivate2, вызвать метод get_s(), мы получим строку ="Значение поля private":
TestPrivate2 tst=new TestPrivate2(); System.out.println(tst.get_s());
Таким образом, поле s наследуется. Но если в классе, где оно задано, не предусмотрен доступ к нему с помощью каких-либо методов, доступных в наследнике, извлечь информацию из этого поля оказывается невозможным.
Модификатор protected предназначен для использования соответствующих полей и методов разработчиками классов-наследников. Он дает несколько большую открытость, чем пакетный вид доступа (по умолчанию, без модификатора), поскольку в дополнении к видимости из текущего пакета позволяет обеспечить доступ к таким членам в классах-наследниках, находящихся в других пакетах. Модификатором protected полезно помечать различного рода служебные методы, ненужные пользователям класса, но необходимые для функциональности этого класса.
Существует "правило хорошего тона": поля данных принято помечать модификатором private, а доступ к этим полям обеспечивать с помощью методов с тем же именем, но префиксом get ("получить" - доступ по чтению) и set ("установить" - доступ по записи). Эти методы называют "геттерами" и "сеттерами". Такие правила основаны на том, что прямой доступ по записи к полям данных может разрушить целостность объекта.
Рассмотрим следующий пример: пусть у нас имеется фигура, отрисовываемая на экране. Изменение ее координат должно сопровождаться отрисовкой на новом месте. Но если мы напрямую изменили поле x или y, фигура останется на прежнем месте, хотя поля имеют новые значения! Если же доступ к полю осуществляется через методы setX и , кроме изменения значений полей будут вызваны необходимые методы, обеспечивающие перерисовку фигуры в новом месте. Также можно обеспечить проверку вводимых значений на допустимость.
Возможен и гораздо худший случай доступа к полям напрямую: пусть у нас имеется объект-прямоугольник, у которого заданы поля x1,y1 - координаты левого верхнего угла, x2,y2 - координаты правого нижнего угла, w - ширина, h – высота, s - площадь прямоугольника. Они не являются независимыми: w=x2-x1, h=y2-y1, s=w*h. Поэтому изменение какого-либо из этих полей должно приводить к изменению других. Если же, скажем, изменить только x2, без изменения w и s, части объекта станут несогласованными. Предсказать, как поведет себя в таких случаях программа, окажется невозможно!
Еще хуже обстоит дело при наличии наследования в тех случаях, когда в потомке задано поле с тем же именем, что и в прародителе, имеющее совместимый с прародительским полем тип. Так как для полей данных полиморфизм не работает, возможны очень неприятные ошибки.
Указанные выше правила хорошего тона программирования нашли выражение в среде NetBeans при установленном пакете NetBeans Enterprise Pack. В ней при разработке private и созданию двух public-методов с тем же именем, но префиксами get и set. Эти типы видимости в дальнейшем, конечно, можно менять, как и удалять ненужные методы.
Иногда возникает необходимость вызвать поле или метод из прародительского класса. Обычно это бывает в случаях, когда в классе-потомке задано поле с таким же именем (но, обычно, другим типом) или . имяПоля или . имяМетода(список параметров). Слово . имя не разрешены.
Использовать вызовы с помощью слова static ) вызовы с помощью ссылки
Данный параграф, несмотря на краткость, является очень важным – практически все профессиональное программирование в Java основано на использовании полиморфизма. В то же время эта тема является одной из наиболее сложных для понимания учащимися. Поэтому рекомендуется внимательно перечитать этот параграф несколько раз.
Методы классов помечаются модификатором static не случайно – для них при компиляции программного кода действует статическое связывание. Это значит, что в контексте какого класса указано имя метода в исходном коде, на метод того класса в скомпилированном коде и ставится ссылка. То есть осуществляется связывание имени метода в месте вызова с исполняемым кодом этого метода. Иногда final ("финальный", "окончательный").
Методы объектов в Java являются динамическими, то есть для них действует динамическое связывание. Оно происходит на этапе выполнения программы непосредственно во время вызова метода, причем на этапе написания данного метода заранее неизвестно, из какого класса будет проведен вызов. Это определяется типом объекта, для которого работает данный код - какому классу принадлежит объект, из того класса вызывается метод. Такое связывание происходит гораздо позже того, как был скомпилирован код метода. Поэтому такой тип связывания часто называют поздним связыванием.
Программный код, основанный на вызове
Для пояснения этих не очень понятных при первом чтении слов рассмотрим пример из предыдущего параграфа – работу метода moveTo. Неопытным программистам кажется, что этот метод следует переопределять в каждом классе-наследнике. Это действительно можно сделать, и все будет правильно работать. Но такой код будет крайне избыточным – ведь реализация метода будет во всех классах-наследниках совершенно одинаковой:
public void moveTo(int x, int y){
hide();
this.x=x;
this.y=y;
show();
};
Кроме того, в этом случае не используются преимущества полиморфизма. Поэтому мы не будем так делать.
Еще часто вызывает недоумение, зачем в писать реализацию данного метода. Ведь используемые в нем вызовы методов hide и show, на первый взгляд, должны быть вызовами
Но методы hide и show являются динамическими, а это, как мы уже знаем, означает, что связывание имени метода и его исполняемого кода производится на этапе выполнения программы. Поэтому то, что данные методы указаны в контексте класса , вовсе не означает, что они будут вызываться из класса ! Более того, можно гарантировать, что методы hide и show никогда не будут вызываться из этого класса. Пусть у нас имеются переменные dot1 типа Dot и circle1 типа Circle, и им назначены ссылки на объекты соответствующих типов. Рассмотрим, как поведут себя вызовы dot1.moveTo(x1,y1) и circle1.moveTo(x2,y2).
При вызове dot1.moveTo(x1,y1) происходит вызов из класса метода moveTo. Действительно, этот метод в классе Dot не переопределен, а значит, он наследуется из . В методе moveTo первый оператор – вызов динамического метода hide. Реализация этого метода берется из того класса, экземпляром которого является объект dot1, вызывающий данный метод. То есть из класса Dot. Таким образом, скрывается точка. Затем идет изменение координат объекта, после чего вызывается динамический метод show. Реализация этого метода берется из того класса, экземпляром которого является объект dot1, вызывающий данный метод. То есть из класса Dot. Таким образом, на новом месте показывается точка.
Для вызова circle1.moveTo(x2,y2) все абсолютно аналогично – динамические методы hide и show вызываются из того класса, экземпляром которого является объект circle1, то есть из класса Circle. Таким образом, скрывается на старом месте и показывается на новом именно окружность.
То есть если объект является точкой, перемещается точка. А если объект является окружностью - перемещается окружность. Более того, если когда-нибудь кто-нибудь напишет, например, класс Ellipse, являющийся наследником Circle, и создаст объект Ellipse ellipse=new Ellipse(…), то вызов ellipse.moveTo(…) приведет к перемещению на новое место эллипса. И происходить это будет в соответствии с тем, каким образом в классе Ellipse реализуют методы hide и show. Заметим, что работать будет давным-давно скомпилированный полиморфный код класса . Полиморфизм обеспечивается тем, что ссылки на эти методы в код метода moveTo в момент компиляции не ставятся – они настраиваются на методы с такими именами из класса вызывающего объекта непосредственно в момент вызова метода moveTo.
В объектно-ориентированных языках программирования различают две разновидности
Может показаться, что вызов
Класс Object является базовым для всех классов Java. Поэтому все его поля и методы наследуются и содержатся во всех классах. В классе Object содержатся следующие методы:
true в случае, когда равны значения объекта, из которого вызывается метод, и объекта, передаваемого через ссылку obj в списке параметров. Если объекты не равны, возвращается false. В классе Object равенство рассматривается как равенство ссылок и эквивалентно оператору сравнения "==". Но в потомках этот метод может быть переопределен, и может сравнивать объекты по их содержимому. Например, так происходит для объектов оболочечных числовых классов. Это легко проверить с помощью такого кода:Double d1=1.0,d2=1.0;
System.out.println("d1==d2 ="+(d1==d2));
System.out.println("d1.equals(d2) ="+(d1.equals(d2)));
Первая строка вывода даст d1==d2 =false, а вторая d1.
Object его обязательно надо переопределить, а также указать, что класс реализует интерфейс Clonable. Попытка вызова метода из объекта, не поддерживающего CloneNotSupportedException ("Клонирование не поддерживается"). Про интерфейсы и исключительные ситуации будет рассказано в дальнейшем.Различают два вида shallow ), когда в клон один к одному копируются значения полей оригинального объекта, и глубокое ( deep ), при котором для полей
Object, в которых требуется совершать какие-либо вспомогательные действия перед уничтожением объекта (закрыть файл, вывести сообщение, отрисовать что-либо на экране, и т.п.). Подробнее об этом методе говорится в соответствующем параграфе.Object этот метод реализует выдачу в строку полного имени объекта (с именем пакета), после которого следует символ '@', а затем в шестнадцатеричном виде Object obj=new Object();
System.out.println(" obj.toString() дает "+obj.toString());
Double d=new Double(1.0);
System.out.println(" d.toString()дает "+d.toString());
Character c='A';
System.out.println("c.toString() дает "+c.toString());
обеспечит вывод
obj.toString() дает java.lang.Object@fa9cf d.toString()дает 1.0 c.toString()дает A
Также имеются методы notify(), notifyAll(), и несколько перегруженных вариантов метода wait, предназначенные для работы с потоками (threads). О них говорится в разделе, посвященном потокам.
Как уже говорилось, объекты в Java создаются с помощью зарезервированного слова new, после которого идет конструктор – специальная подпрограмма, занимающаяся созданием объекта и инициализацией полей создаваемого объекта. Для него не указывается тип возвращаемого значения, и он не является ни методом объекта (вызывается через имя класса когда объекта еще нет), ни методом класса (в конструкторе доступен объект и его поля через ссылку this ). На самом деле конструктор в сочетании с оператором new возвращает ссылку на создаваемый объект и может считаться особым видом методов, соединяющим в себе черты методов класса и методов объекта.
Если в объекте при создании не нужна никакая дополнительная инициализация, можно использовать конструктор, который по умолчанию присутствует для каждого класса. Это имя класса, после которого ставятся пустые круглые скобки – без списка параметров. Такой конструктор при разработке класса задавать не надо, он присутствует автоматически.
Если требуется инициализация, обычно применяют конструкторы со списком параметров. Примеры таких конструкторов рассматривались нами для классов Dot и Circle. Классы Dot и Circle были унаследованы от (от "FilledCircle - наследник от Circle, экземпляр которого будет отрисовываться как цветной круг.
package java_gui_example;
import java.awt.*;
public class FilledCircle extends Circle{
/** Creates a new instance of FilledCircle */
public FilledCircle(Graphics g,Color bgColor, int r,Color color) {
super(g,bgColor,r);
this.color=color;
}
public void show(){
Color oldC=graphics.getColor();
graphics.setColor(color);
graphics.setXORMode(bgColor);
graphics.fillOval(x,y,size,size);
graphics.setColor(oldC);
graphics.setPaintMode();
}
public void hide(){
Color oldC=graphics.getColor();
graphics.setColor(color);
graphics.setXORMode(bgColor);
graphics.fillOval(x,y,size,size);
graphics.setColor(oldC);
graphics.setPaintMode();
}}
Вообще, логика создания сложно устроенных объектов: родительская часть объекта создается и инициализируется первой, начиная от части, доставшейся от класса Object, и далее по иерархии, заканчивая частью, относящейся к самому классу. Именно поэтому обычно первым оператором конструктора является вызов прародительского конструктора
В данном классе мы применяем более совершенный способ отрисовки и "скрывания" фигур по сравнению с предыдущими классами. Он основан на использовании режима рисования XOR ("исключающее или"). Установка этого режима производится методом setXORMode. При этом повторный вывод фигуры на то же место приводит к восстановлению первоначального изображения в области вывода. Переход в обычный режим рисования осуществляется методом setPaintMode.
В конструкторах очень часто используют зарезервированное слово this для доступа к полям объекта, видимость имен которых перекрыта переменными из списка параметров конструктора. Но в конструкторах оно имеет еще одно применение - для обращения из одного варианта конструктора к другому, имеющему другой список параметров. Напомним, что наличие таких вариантов называется перегрузкой конструкторов. Например, пусть мы первоначально задали в классе Circle конструктор, в котором значение полей x, y и r задается случайным образом:
Circle(Graphics g, Color bgColor){
graphics=g;
this.bgColor=bgColor;
size=(int)Math.round(Math.random()*40);
}
Тогда конструктор, в котором случайным образом задаются значения полей x и y, а значение size задается через список параметров конструктора, можно написать так:
Circle(Graphics g, Color bgColor, int r){
this(g, bgColor);
size=r;
}
При вызове конструктора с помощью слова this требуется, чтобы вызов this был первым оператором в реализации вызывающего конструктора.
В отличие от языка C++ в Java не разрешается использование имени конструктора, отличающегося от имени класса.
Порядок вызовов при создании объекта некого класса (будем называть его дочерним классом):
Object.Знание данного порядка важно в случаях, когда в конструкторе вызываются какие-либо методы объекта, и надо быть уверенным, что к моменту вызова этих методов объект получит правильные значения полей данных.
Как правило, для инициализации полей сложно устроенных объектов используют конструкторы. Но кроме них в Java, в отличие от большинства других языков программирования, для этих целей могут также служить блоки инициализации класса и блоки инициализации объекта. Синтаксис задания классов с блоками инициализации следующий:
Модификаторы class ИмяКласса extends ИмяРодителя {
Задание полей;
static {
тело блока инициализации класса
}
{
тело блока инициализации объекта
}
Задание подпрограмм - методов класса, методов объекта, конструкторов
}
Блоков инициализации класса и блоков инициализации объекта может быть несколько.
Порядок выполнения операторов при наличии блоков инициализации главного main ):
main ;Для других классов порядок аналогичен, но без вызова метода main:
Чем лучше пользоваться, блоками инициализации или конструкторами? Ответ, конечно, неоднозначен: в одних ситуациях – конструкторами, в других – блоками инициализации. Для придания начальных значений
Как мы знаем, конструктор занимается созданием и рядом дополнительных действий, связанных с
Например, у нас имеется список фигур, отрисовываемых на экране, и мы хотим удалить из этого списка какую-нибудь фигуру. Перед уничтожением фигура должна исключить себя из списка, затем дать команду списку заново отрисовать содержащиеся в нем фигуры, и только после этого "умереть". Именно такого рода действия характерны для
В Java имеется метод finalize(). Если в классе, который производит завершающие действия перед уничтожением объекта сборщиком мусора, переопределить этот метод, он, как может показаться, может служить некой заменой finalize не может служить реальной заменой деструктору. Даже явный вызов сборщика мусора System.gk() сразу после вызова метода finalize() не слишком удачное решение, так как и в этом случае нет гарантии правильности порядка высвобождения ресурсов. Кроме того, сборщик мусора потребляет много ресурсов и в ряде случаев может приостановить работу программы на заметное время.
Гораздо более простым и правильным решением будет написать в базовом классе разрабатываемой вами иерархии метод - "уничтожить, разрушить", который будет заниматься выполнением всех необходимых вспомогательных действий (можно назвать метод dispose() – "избавиться, отделаться", можно free() – "освободить"). Причем при необходимости надо будет переопределять этот метод в классах-наследниках. В случае, когда надо вызывать прародительский деструктор, следует делать вызов . При этом желательно, чтобы он был последним оператором в
Логика разрушения объектов является обратной той, что используется при их создании: сначала разрушается часть, относящаяся к самому классу. Затем разрушается часть, относящаяся к непосредственному прародителю, и далее по иерархии, заканчивая частью, относящейся к базовому классу. Поэтому последним оператором .
Напомним, что имя функции в сочетании с числом параметров и их типами называется сигнатурой функции. Тип возвращаемого значения и имена параметров в сигнатуру не входят. Понятие сигнатуры важно при задании подпрограмм с одинаковыми именами, но разными списками параметров – перегрузке (
Чаще всего перегружают конструкторы при желании иметь разные их варианты, так как имя конструктора определяется именем класса. Например, рассмотренные ранее конструкторы
Circle(Graphics g, Color bgColor){
…
}
и
Circle(Graphics g, Color bgColor, int r){
…
}
отличаются числом параметров, поэтому перегрузка разрешена.
Вызов
Напишем класс Math1, в котором имеется подпрограмма-функция product, вычисляющая произведение двух чисел, у которой имеются варианты с разными целыми типами параметров. Пример полезен как для иллюстрации проблем, связанных с вызовом
public class Math1 {
public static byte product(byte x, byte y){
return x*y;
}
public static short product(short x, short y){
return x*y;
}
public static int product(int x, int y){
return x*y;
}
public static char product(char x, char y){
return x*y;
}
public static long product(long x, long y){
return x*y;
}
}
Такое задание методов разрешено, так как сигнатуры перегружаемых вариантов различны. Обратим внимание на типы возвращаемых значений – они могут задаваться по желанию программиста. Подпрограммы заданы как методы класса ( static ) для того, чтобы при их использовании не пришлось создавать объект.
Если бы мы попытались задать такие варианты методов:
public static byte product(byte x, byte y){
return x*y;
}
public static int product(byte a, byte b){
return a*b;
}
то компилятор выдал бы сообщение об ошибке, так как у данных вариантов одинаковая сигнатура. - Ни тип возвращаемого значения, ни имена параметров на сигнатуру не влияют.
Если при вызове метода product параметры имеют типы, совпадающие с заданными в одном из перегруженных вариантов, все просто. Но что произойдет в случае, когда в качестве параметра будут переданы значения типов byte и int? Какой вариант будет вызван? Проверка идет при компиляции программы, при этом перебираются все допустимые варианты. В нашем случае это product(int x, int y) и product(long x, long y). Остальные варианты не подходят из-за типа второго параметра – тип подставляемого значения должен иметь диапазон значений, "вписывающийся" в диапазон вызываемого метода. Из допустимых вариантов выбирается тот, который ближе по типу параметров, то есть в нашем случае product(int x, int y).
Если среди Math2
public class Math2 {
public static int product(int x, byte y){
return x*y;
}
public static int product(byte x, int y){
return x*y;
}
}
и в каком-нибудь другом классе имели переменные byte b1, b2 и сделали вызов Math2.product(b1,b2). Оба варианта перегруженного метода подходят, и выбрать более подходящий невозможно. Отметим, что класс Math2 при этом компилируется без проблем – в нем самом ошибок нет. Проблема в том классе, который его использует.
Самая неприятная особенность перегрузки – вызов не того варианта метода, на который рассчитывал программист. Особо
Мы уже говорили, что полиморфный код обеспечивает основные преимущества объектного программирования. Но как им воспользоваться? Ведь тип объектных переменных задается на этапе компиляции. Решением проблемы является следующее правило:
переменной некоторого объектного типа можно присваивать выражение, имеющее тот же тип или тип класса-наследника.
Аналогичное правило действует при передаче
В качестве фактического параметра вместо формального параметра некоторого объектного типа можно подставлять выражение, имеющее тот же тип или тип класса-наследника.
В качестве выражения может выступать переменная new, за которым следует конструктор), функция
Поэтому если мы создадим переменную базового типа, для которой можно писать полиморфный код, этой переменной можно назначить ссылку на объект, имеющий тип любого из классов-потомков. В том числе – еще не написанных на момент компиляции базового класса. Пусть, например, мы хотим написать подпрограмму, позволяющую перемещать фигуры из нашей иерархии не в точку с новыми координатами, как метод moveTo, а на необходимую величину dx и dy по соответствующим осям. При этом у нас отсутствуют исходные коды базового класса нашей иерархии (либо их запрещено менять). Для этих целей создадим класс FiguresUtil (сокращение от Utilities – утилиты, служебные программы), а в нем зададим метод moveFigureBy ("переместить фигуру на").
public class FiguresUtil{
public static void moveFigureBy(Figure figure,int dx, int dy){
figure.moveTo(figure.x+dx, figure.y+dy);
}
}
В качестве можно подставлять выражение, имеющее тип любого класса из иерархии фигур. Пусть, например, новая фигура создается по нажатию на кнопку в зависимости от того, какой выбор сделал пользователь во время работы программы: если в радиогруппе отмечен пункт "Точка", создается объект типа Dot. Если в радиогруппе отмечен пункт "Окружность", создается объект типа Circle. Если же отмечен пункт "Круг", создается объект типа FilledCircle. Отметим также, что класс FilledCircle был написан уже после компиляции классов , Dot и Circle.
Фрагмент кода для класса нашего приложения будет выглядеть так:
Figure figure;
java.awt.Graphics g=jPanel1.getGraphics();
//обработчик кнопки создания фигуры
private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {
if(jRadioButton1.isSelected() )
figure=new Dot(g,jPanel1.getBackground());
if(jRadioButton2.isSelected())
figure=new Circle(g,jPanel1.getBackground());
if(jRadioButton3.isSelected())
figure=new FilledCircle(g,jPanel1.getBackground(),20,
java.awt.Color.BLUE);
figure.show();
}
//обработчик кнопки передвижения фигуры
private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {
int dx= Integer.parseInt(jTextField1.getText());
int dy= Integer.parseInt(jTextField2.getText());
FiguresUtil.moveFigureBy(figure,dx,dy);
}
При написании программы неизвестно, ни какого типа будет передвигаемый объект, ни насколько его передвинут – все зависит от решения пользователя во время работы программы. Именно возможность назначения ссылки на объект класса-потомка обеспечивает возможность использования полиморфного кода.
Следует обратить внимание на еще один момент – стиль написания вызова
FiguresUtil.moveFigureBy(figure,dx,dy);
Можно было бы написать его так:
FiguresUtil.moveFigureBy( figure, Integer.parseInt(jTextField1.getText()), Integer.parseInt(jTextField2.getText()) );
При этом экономились бы две локальные переменные (аж целых 8 байт памяти!), но читаемость, понимаемость и отлаживаемость кода стали бы гораздо меньше.
Часто встречающаяся ошибка: пытаются присвоить переменной типа "наследник" выражение типа "прародитель". Например,
Figure figure; Circle circle; … figure =new Circle (); //так можно … circle= figure; - Так нельзя! Выдастся ошибка компиляции.
Несмотря на то, что переменной назначен объект типа Circle – ведь проверка на допустимость присваивания делается на этапе компиляции, а не динамически.
Если программист не уверен, что объект имеет тип класса-потомка, в таких случаях надо использовать приведение типа. Для приведения типа перед выражением или именем переменной в круглых скобках ставят имя того типа, к которому надо осуществить приведение:
Figure figure; Circle circle; Dot dot; … figure =new Circle (); //так можно … circle= (Circle)figure; //так можно! dot=(Dot) figure; //так тоже можно!
Отметим, что приведение типа принципиально отличается от преобразования типа, хотя синтаксически записывается так же. Преобразование типа приводит к изменению содержимого ячейки памяти и может приводить к изменению ее размера. А вот приведение типа не меняет ни размера, ни содержимого никаких ячеек памяти – оно меняет только тип, сопоставляемый ячейке памяти. В Java приведение типа применяется к
Приводить тип можно как в сторону генерализации, так и в сторону специализации.
Приведение в сторону генерализации является безопасным, так как объект класса-потомка всегда является экземпляром прародителя, хоть и усложненным. А вот приведение в сторону специализации является опасным – вполне допустимо, что во время выполнения программы окажется, что объект, назначенный переменной, не является экземпляром нужного класса. Например, при приведении (Circle) может оказаться, что переменной назначен объект типа Dot, который не может быть приведен к типу Circle. В этом случае возникает исключительная ситуация приведения типа ( typecast ).
Возможна программная проверка того, что объект является экземпляром заданного класса:
if(figure instanceof Circle)
System.out.println("figure instanceof Circle");
Иногда вместо работы с самими классами бывает удобно использовать ссылки на класс. Они получаются с помощью доступа к полю .class из любого класса.
Возможно создание переменных типа "ссылка на класс":
Class c=Circle.class;
Их можно использовать для обращения к newInstance():
Circle circle=(Circle)c.newInstance();
Возможна программная проверка соответствия объекта нужному типу с помощью ссылки на класс:
if(figure.getClass()==Circle.class)
circle= (Circle)figure;
…;
Но следует учитывать, что при такой проверке идет сравнение на точное равенство классов, а не на допустимость приведения типов. А вот оператор isInstance позволяет проверять, является ли объект экземпляром класса, на который ссылается c:
if(c.isInstance(figure))
System.out.println("figure isInstance of Circle");
Одним из важных элементов современного программирования является рефакторинг – изменение структуры существующего проекта без изменения его функциональности.
Приведем три наиболее часто встречающихся примера рефакторинга.
В сложных проектах, конечно, возникают и другие варианты рефакторинга (например, выделение части кода в отдельный метод – ", но с упомянутыми приходится встречаться постоянно. Поэтому рассмотрим эти три случая подробнее.
Первый случай - переименование элементов программы.
Для того, чтобы в среде NetBeans переименовать элемент, следует щелкнуть по его имени правой кнопкой мыши. Это можно сделать в исходном коде программы, а можно и в окне Projects или Navigator. В появившемся всплывающем меню следует выбрать Refactor/Rename… После чего ввести новое имя и нажать кнопку "Next>".

(рис 6.6) Переименование класса. Шаг 1(рис 6.5) Переименование класса. Шаг 2Если галочка "Preview All Changes" ("Предварительный просмотр всех изменений") не снята, в самом нижнем окне, Output ("Вывод"), появится дерево со списком мест, где будут проведены исправления. В случае необходимости галочки можно снять, и в этих местах переименование проводиться не будет. При нажатии на кнопку "Do Refactoring" ("Провести рефакторинг") проводится операция переименования в выбранных местах программы. В отличие от обычных
(рис 6.7) Переименование класса. Шаг 3Требуется быть внимательными: довольно часто начинающие программисты не замечают появления в окне Output списка изменений и кнопки "Do Refactoring". Особенно если высота этого окна сделана очень малой. Если в диалоге переименования (шаг 2) флажок "Preview all Changes" снят, при нажатии на кнопку "Next>" сразу происходит рефакторинг.
Следует также отметить, что после проведения рефакторинга возможен возврат к первоначальному состоянию ("откат", операция undo ). Обычно такая операция осуществляется с помощью главного меню проекта (кнопка Undo или пункт меню Edit/Undo ), но в случае рефакторинга требуется правой клавишей мыши вызвать всплывающее окно и выбрать пункт Refactor/Undo. Откат может быть на несколько шагов назад путем повторения данного действия. При необходимости отказа от отката в меню рефакторинга следует выбрать пункт Redo.
Второй случай - перемещение элементов программы с одного места на другое.
Например, мы хотим переместить класс из одного пакета в другой. Для выполнения этого действия достаточно перетащить мышью в окне Projects узел, связанный с данным классом, в соответствующий пакет. При таком перемещении там, где это необходимо, автоматически добавляются операторы импорта.
Если при перемещении возникают проблемы, о них выдается сообщение. Как правило, проблемы бывают связаны с неправильными уровнями видимости. Например, если указан пакетный уровень видимости метода, он доступен другим классам этого пакета. А при переносе класса в другой пакет в месте исходного кода, где осуществляется такой доступ, в новом варианте кода возникает ошибка доступа. Перенос класса в отдельный пакет, отличающийся от пакета приложения – хороший способ проверить правильности выбранных уровней доступа для членов класса.
Аналогичным образом перемещаются пакеты. При этом все пакеты в дереве элементов показываются на одном уровне вложенности, но у вложенных пакетов имена квалифицируются именем родительского пакета.
Третий случай - инкапсуляция полей данных.
Напрямую давать доступ к полю данных – дурной тон программирования. Поэтому рекомендуется давать полям уровень видимости private, а доступ к ним по чтению и записи осуществлять с помощью методов get ИмяПоля и set ИмяПоля - получить и установить значение этого поля. Такие методы в Java называют геттерами (
Но при введении в класс новых полей на первом этапе часто бывает удобнее задать поля с модификатором public и обеспечивать чтение значения полей напрямую, а изменение значения – путем присваивания полям новых значений. А затем можно исправить данный недостаток программы с помощью инкапсуляции полей данных. Это делается просто: в дереве элементов программы окна Projects в разделе Fields ("поля") щелкнем правой кнопкой мыши по имени поля и выберем в появившемся всплывающем меню Refactor/Encapsulate Fields… ("Провести рефакторинг"/ "Инкапсулировать поля…").В появившемся диалоге нажмем на кнопку "Next>" и проведем рефакторинг. При этом каждое поле приобретет private, а во всех местах программы, где напрямую шел доступ к этому полю, в коде будет проведена замена на вызовы геттеров и сеттеров.
Более подробную информацию по идеологии и методах рефакторинга проектов, написанных на языке Java, можно найти в монографии [7]. Правда, эта книга уже несколько устарела – среда NetBeans позволяет делать в автоматическом режиме многие из описанных в [7] действий.
Среда NetBeans при установленном пакете NetBeans Enterprise Pack позволяет по имеющемуся исходному коду построить
"Reverse Engineer…"
(рис 6.8) Кнопка "Reverse Engineering"Появится диалоговая форма задания параметров создаваемого проекта, в которой следует изменить название проекта на осмысленное, по которому легко можно будет определить, к какому проекту Java он относится.
(рис 6.9) Диалоговая форма задания параметров создаваемого UML-проектаВ нашем случае UMLProject7 мы заменим на UML_Figure. После нажатия на кнопку Finish ("Закончить") будет выдана форма с ненужной вспомогательной информацией, и в ней следует нажать кнопку Done ("Сделано"). В результате чего мы получим новый
(рис 6.10) Параметры UML-проекта, относящиеся к классу CircleДля класса показываются конструкторы и обычные методы (узел Operations ), а также ).
В
(рис 6.11) Всплывающее меню действий с классом в UML-проектеЕсли выбрать пункт "Create Diagram From Selected Elements" ("Создать диаграмму из выбранных элементов"), и далее выбрать тип диаграммы "Class Diagram",
(рис 6.12) Выбор типа создаваемой диаграмыможно получить диаграмму такого вида:
(рис 6.13) Диаграмма для класса CircleПри этом лучше заменить имя создаваемой диаграммы, например, на Circle . Переименование можно сделать и позже, щелкнув правой кнопкой мыши по имени диаграммы и выбрав в появившемся всплывающем меню пункт Rename… ("Переименовать…").
Если же выделить Circle,Dot,
(рис 6.14) Диаграмма для классов Circle,Dot,Figure, ScalableFigureЕсли для класса Circle во всплывающем меню выбрать пункт "Generate Dependency Diagram" ("Сгенерировать диаграмму зависимостей"), получим следующую диаграмму :
(рис 6.15) Диаграмма зависимостей для класса CircleПункт всплывающего меню Navigate to Source позволяет вместо диаграмм показывать редактор исходного кода.
На диаграммах можно добавлять в классы или удалять из них поля и методы, проводить переименования, менять модификаторы. Причем изменения, сделанные на любой из диаграмм, автоматически отражаются как на других диаграммах
В настоящее время работа с
general ) он является. Чем дальше от базового класса иерархии стоит класс, тем более специализированным ( specialized ) он является.private, protected и public.this обеспечивает ссылку на объект из метода объекта. Чаще всего она используется при перекрытии области видимости имени поля объекта overloaded ) варианты методов, отличающиеся сигнатурой. В сигнатуру входит только часть заголовка метода – имя функции, а также число, порядок и тип ее параметров.instanceof: if(figure instanceof Circle)...if(figure .getClass()==Circle.class)... При этом для экземпляра класса-наследника Circle сравнение даст false, в отличие от экземпляра Circle.isInstance позволяет проверять, является ли тип объекта совместимым с классом, на который задана ссылка c: if(c.isInstance(figure ))... При этом если класс, экземпляром которого является объект figure , является наследником класса c или совпадает с ним, сравнение даст true.Object object присвоена ссылка на объект типа Dot, а пытаются сделать приведение (Circle) object. Такая ошибка на этапе компиляции не может быть распознана, и возникает исключительная ситуация неправильного приведения типа ( invalid typecast ) во время выполнения программы.MathUtil написать подпрограмму вычисления факториала public static double factorial(int n)Модификатор static помечает подпрограмму как метод класса. То есть позволяет вызывать метод через имя класса без создания объекта.
Напомним, что факториал натурального числа n – это произведение всех натуральных чисел от 1 до n :
$$n!=1\cdot 2\cdot\ldots\cdot(n-1)\cdot n$$ Кроме того, 0! считается равным 1. Обозначение факториала в виде n! математическое, в Java символ "!" зарезервирован для других целей. Также написать подпрограммы вычисления факториала с другими типами возвращаемых значений:public static long factorial_long(int n) и public static int factorial_int(int n)Сравнить работу подпрограмм при n=0,1,5,10,20,50,100. Объяснить результаты.
show и hide. Вместо показа на экране эти методы должны выводить в консольное окно вывода имя класса фигуры и слово show или hide, а также координаты x и y фигуры.figure . и dot., где эти переменные заданы как Figure figure и Dot dot.Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.