Опубликован: 04.12.2009 | Доступ: свободный | Студентов: 8416 / 657 | Оценка: 4.30 / 3.87 | Длительность: 27:27:00
Лекция 6:

Начальные сведения об объектном программировании

6.12. Правила совместимости ссылочных типов как основа использования полиморфного кода. Приведение и проверка типов

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

переменной некоторого объектного типа можно присваивать выражение, имеющее тот же тип или тип класса-наследника.

Аналогичное правило действует при передаче фактического параметра в подпрограмму:

В качестве фактического параметра вместо формального параметра некоторого объектного типа можно подставлять выражение, имеющее тот же тип или тип класса-наследника.

В качестве выражения может выступать переменная объектного типа, оператор создания нового объекта (слово 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 приведение типа применяется к ссылочным типам, а преобразование – к примитивным. Это связано с тем, что изменение типа ссылочной переменной не приводит к изменению той ячейки, на которую она ссылается. То есть в случае приведения тип объекта не меняется – меняется тип ссылки на объект.

Приводить тип можно как в сторону генерализации, так и в сторону специализации.

Приведение в сторону генерализации является безопасным, так как объект класса-потомка всегда является экземпляром прародителя, хоть и усложненным. А вот приведение в сторону специализации является опасным – вполне допустимо, что во время выполнения программы окажется, что объект, назначенный переменной, не является экземпляром нужного класса. Например, при приведении (Circle)figure может оказаться, что переменной figure назначен объект типа 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 позволяет проверять, является ли объект figure экземпляром класса, на который ссылается c:

if(c.isInstance(figure))
    System.out.println("figure isInstance of Circle");

6.13. Рефакторинг

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

Приведем три наиболее часто встречающихся примера рефакторинга.

  • Во-первых, это переименование элементов программы – классов, переменных, методов.
  • Во-вторых, перемещение элементов программы с одного места на другое.
  • В-третьих, инкапсуляция полей данных.

В сложных проектах, конечно, возникают и другие варианты рефакторинга (например, выделение части кода в отдельный метод – "Extract method"), но с упомянутыми приходится встречаться постоянно. Поэтому рассмотрим эти три случая подробнее.

Первый случай - переименование элементов программы.

Для того, чтобы в среде NetBeans переименовать элемент, следует щелкнуть по его имени правой кнопкой мыши. Это можно сделать в исходном коде программы, а можно и в окне Projects или Navigator. В появившемся всплывающем меню следует выбрать Refactor/Rename… После чего ввести новое имя и нажать кнопку "Next>".

Переименование класса. Шаг 1

Рис. 6.5. Переименование класса. Шаг 1
Переименование класса. Шаг 2

Рис. 6.6. Переименование класса. Шаг 2

Если галочка "Preview All Changes" ("Предварительный просмотр всех изменений") не снята, в самом нижнем окне, Output ("Вывод"), появится дерево со списком мест, где будут проведены исправления. В случае необходимости галочки можно снять, и в этих местах переименование проводиться не будет. При нажатии на кнопку "Do Refactoring" ("Провести рефакторинг") проводится операция переименования в выбранных местах программы. В отличие от обычных текстовых процессоров переименование происходит с учетом синтаксиса программы, так что элементы, не имеющие отношения к переименовываемому, но имеющие такие же имена, не затрагиваются. Что в выгодную сторону отличает NetBeans от многих других сред разработки, не говоря уж об обычных текстовых редакторах.

Переименование класса. Шаг 3

увеличить изображение
Рис. 6.7. Переименование класса. Шаг 3

Требуется быть внимательными: довольно часто начинающие программисты не замечают появления в окне Output списка изменений и кнопки "Do Refactoring". Особенно если высота этого окна сделана очень малой. Если в диалоге переименования (шаг 2) флажок "Preview all Changes" снят, при нажатии на кнопку "Next>" сразу происходит рефакторинг.

Следует также отметить, что после проведения рефакторинга возможен возврат к первоначальному состоянию ("откат", операция undo ). Обычно такая операция осуществляется с помощью главного меню проекта (кнопка Undo или пункт меню Edit/Undo ), но в случае рефакторинга требуется правой клавишей мыши вызвать всплывающее окно и выбрать пункт Refactor/Undo. Откат может быть на несколько шагов назад путем повторения данного действия. При необходимости отказа от отката в меню рефакторинга следует выбрать пункт Redo.

Второй случай - перемещение элементов программы с одного места на другое.

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

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

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

Третий случай - инкапсуляция полей данных.

Напрямую давать доступ к полю данных – дурной тон программирования. Поэтому рекомендуется давать полям уровень видимости private, а доступ к ним по чтению и записи осуществлять с помощью методов get ИмяПоля и set ИмяПоля - получить и установить значение этого поля. Такие методы в Java называют геттерами (getters) и сеттерами (setters).

Но при введении в класс новых полей на первом этапе часто бывает удобнее задать поля с модификатором public и обеспечивать чтение значения полей напрямую, а изменение значения – путем присваивания полям новых значений. А затем можно исправить данный недостаток программы с помощью инкапсуляции полей данных. Это делается просто: в дереве элементов программы окна Projects в разделе Fields ("поля") щелкнем правой кнопкой мыши по имени поля и выберем в появившемся всплывающем меню Refactor/Encapsulate Fields… ("Провести рефакторинг"/ "Инкапсулировать поля…").В появившемся диалоге нажмем на кнопку "Next>" и проведем рефакторинг. При этом каждое поле приобретет модификатор видимости private, а во всех местах программы, где напрямую шел доступ к этому полю, в коде будет проведена замена на вызовы геттеров и сеттеров.

Более подробную информацию по идеологии и методах рефакторинга проектов, написанных на языке Java, можно найти в монографии [7]. Правда, эта книга уже несколько устарела – среда NetBeans позволяет делать в автоматическом режиме многие из описанных в [7] действий.

Полетаев Дмитрий
Полетаев Дмитрий
Не очень понятно про оболочечные Данные,ячейки памяти могут наверно размер менять,какое это значение те же операции только ячейки больше,по скорости тоже самое
Максим Старостин
Максим Старостин

Код с перемещением фигур не стирает старую фигуру, а просто рисует новую в новом месте. Точку, круг.