Смекни!
smekni.com

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

Логика разрушения объектов является обратной той, что используется при их создании: сначала разрушается часть, относящаяся к самому классу. Затем разрушается часть, относящаяся к непосредственному прародителю, и далее по иерархии, заканчивая частью, относящейся к базовому классу. Поэтому последним оператором деструктора бывает вызов прародительского деструктора super.destroy().

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

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

Чаще всего перегружают конструкторы при желании иметь разные их варианты, так как имя конструктора определяется именем класса. Например, рассмотренные ранее конструкторы

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 и сделали вызов Math1.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);

}

}

В качестве фактического параметра такой подпрограммы вместо figure можно подставлять выражение, имеющее тип любого класса из иерархии фигур. Пусть, например, новая фигура создаётся по нажатию на кнопку в зависимости от того, какой выбор сделал пользователь во время работы программы: если в радиогруппе отмечен пункт “Точка”, создаётся объект типа Dot. Если в радиогруппе отмечен пункт “Окружность”, создаётся объект типа Circle. Если же отмечен пункт “Круг”, создаётся объект типа FilledCircle. Отметим также, что класс FilledCircle был написан уже после компиляции классов Figure, 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; - Так нельзя! Выдастся ошибка компиляции. Несмотря на то, что переменной figure назначен объект типа Circle – ведь проверка на допустимость присваивания делается на этапе компиляции, а не динамически.

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

Figure figure;

Circle circle;

Dot dot;

figure =new Circle (); //так можно

circle= (Circle)figure; //так можно!

dot=(Dot) figure; //так тоже можно!

Отметим, что приведение типа принципиально отличается от преобразования типа, хотя синтаксически записывается так же. Преобразование типа приводит к изменению содержимого ячейки памяти и может приводить к изменению её размера. А вот приведение типа не меняет ни размера, ни содержимого никаких ячеек памяти – оно меняет только тип, сопоставляемый ячейке памяти. В Java приведение типа применяется к ссылочным типам, а преобразование – к примитивным. Это связано с тем, что изменение типа ссылочной переменной не приводит к изменению той ячейки, на которую она ссылается. То есть в случае приведения тип объекта не меняется – меняется тип ссылки на объект.