Смекни!
smekni.com

Общие представления о языке Java 5 (стр. 35 из 68)

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

Можно заметить, что в приведённой иерархии несколько этапов наследования приходятся именно на абстрактные классы, и ни один из классов, имеющих экземпляры (объекты), не имеет наследников. Причина такого требования проста: изменение реализации одного класса, проводимое не на уровне абстракции, а относящееся только к одному конкретному классу, не должна влиять на поведение другого класса. Иначе возможны неотслеживаемые труднопонимаемые ошибки в работе иерархии классов. Например, если мы попробуем унаследовать класс Ellipse от Circle, после исправлений в реализации Circle, обеспечивающих правильную работу объектов этого типа, могут возникнуть проблемы при работе объектов типа Ellipse, которые до того работали правильно. Причём речь идёт об особенностях реализации конкретного класса, не относящихся к абстракциям поведения.

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

В языке Java, к сожалению, отсутствуют адекватные средства для проектирования классов. Более того, в этом отношении он заметно уступает таким языкам как C++ или Object PASCAL, поскольку в Java отсутствует разделение декларации класса (описание полей и заголовков методов) и реализации методов. Но в Sun Java Studio и NetBeans Enterprise Pack имеется средство решения этой проблемы – создание UML-диаграмм. UML расшифровывается как Universal Modeling Language – Универсальный Язык Моделирования. Он предназначен для моделирования на уровне абстракций классов и связей их друг с другом – то есть для задач Объектно-Ориентированного Проектирования (OOA – Object-Oriented Architecture). Приведённые выше рисунки иерархий классов – это UML-диаграммы, сделанные с помощью NetBeans Enterprise Pack.

Пока в этой среде пока нет возможности по UML-диаграммам создавать заготовки классов Java, как это делается в некоторых других средах UML-проектирования. Но если создать пустые заготовки классов, то далее можно разрабатывать соответствующие им UML-диаграммы, и внесённые изменения на диаграммах будут сразу отображаться в исходном коде. Как это делается будет подробно описано в последнем параграфе данной главы, где будет обсуждаться технология Reverse Engineering.

Функции. Модификаторы. Передача примитивных типов в функции

Основой создания новых классов является задание полей данных и методов. Но если поля отражают структуру данных, связанных с объектом или классом, то методы задают поведение объектов, а также работу с полями данных объектов и классов.

Формат объявления функции следующий:

Модификаторы Тип Имя(список параметров){

Тело функции

}

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

Комбинация элементов декларации метода

Модификаторы Тип Имя(список параметров)

называется заголовком метода.

Модификаторы – это зарезервированные слова, задающие

- Правила доступа к методу (private, protected, public). Если модификатор не задан, действует доступ по умолчанию – так называемый пакетный.

- Принадлежность к методам класса (static). Если модификатор не задан, считается, что это метод объекта.

- Невозможность переопределения метода в потомках (final). Если модификатор не задан, считается, что это метод можно переопределять в классах-потомках.

- Способ реализации (native – заданный во внешней библиотеке DLL, написанной на другом языке программирования; abstract – абстрактный, не имеющий реализации). Если модификатор не задан, считается, что это обычный метод.

- Синхронизацию при работе с потоками (synchronized) .

В качестве Типа следует указать тип результата, возвращаемого методом. В Java, как мы уже знаем, все методы являются функциями, возвращающими значение какого-либо типа. Если требуется метод, не возвращающий никакого значения (то есть процедура), он объявляется с типом void. Возврат значения осуществляется в теле функции с помощью зарезервированного слова return.

Для выхода без возврата значения требуется написать

return;

Для выхода с возвратом значения требуется написать

return выражение;

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

Оператор return осуществляет прерывание выполнения подпрограммы, поэтому его обычно используют в ветвях операторов if-else или switch-case в случаях, когда необходимо возвращать тот или иной результат в зависимости от различных условий. Если в подпрограмме-функции в какой-либо из ветвей не использовать оператор return, будет выдана ошибка компиляции с диагностикой “missing return statement” – “ошибочное высказывание с return”.

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

Объявление параметров имеет вид

тип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 надо только первый параметр, а второй должен оставаться неизменным. В этом случае наш метод выглядел бы так: