Java: руководство для начинающих
Шрифт:
В качестве параметра данному конструктору передается объект Triangle, который затем с помощью вызова super передается конструктору TwoDShape, как показано ниже. // Построение одного объекта на основании другого объекта. TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; }
Следует заметить, что конструктор TwoDshape должен получить объект типа TwoDShape, но конструктор Triangle передает ему объект типа Triangle. Тем не менее никаких недоразумений не возникает. Ведь, как пояснялось ранее, переменная ссылки на суперкласс может ссылаться на объект производного от него подкласса. Следовательно, конструктору TwoDShape можно передать ссылку на экземпляр подкласса, производного от класса TwoDShape. Конструктор TwoDShape инициализирует лишь те части передаваемого ему объекта подкласса, которые являются членами класса TwoDShape, и поэтому не имеет значения, содержит ли этот объект дополнительные члены, добавленные в производных подклассах. Переопределение методов
В иерархии классов часто присутствуют методы с одинаковой сигнатурой и одинаковым возвращаемым значением как в суперклассе, так и в подклассе. В этом случае говорят,
Выполнение этого фрагмента кода дает следующий результат: к: 3
Когда метод show вызывается для объекта типа В, выбирается вариант этого метода, определенный в классе В. Таким образом, вариант метода show в классе В переопределяет вариант одноименного метода, объявленный в классе А.
Если требуется обратиться к исходному варианту переопределяемого метода, т.е. тому, который определен в суперклассе, следует воспользоваться ключевым словом super. Например, в приведенном ниже варианте класса В из метода show вызывается вариант того же метода, определенный в суперклассе. При этом отображаются все переменные экземпляра. class В extends А { int к; В(int a, int b, int с) { super (а, Ь); к = с; } void show { // Использование ключевого слова super для // вызова метода show, определенного в классе А. super.show; System.out.println("k: " + k); } }
Если подставить новый вариант метода show в предыдущую версию рассматриваемого здесь фрагмента кода, результат его выполнения изменится и будет иметь следующий вид: i and j: 1 2 k: 3
В данном случае super. show — это вызов метода show , определенного в суперклассе.
Переопределение метода происходит только в том случае, если сигнатуры переопределяемого и переопределяющего методов совпадают. В противном случае происходит обычная перегрузка методов. Рассмотрим следующую видоизмененную версию предыдущего примера: /* Методы с разными сигнатурами не переопределяются, а перегружаются. */ class А { int i, j; A(int a, int b) { i = a; j = b; } // отобразить переменные i и j void show { System.out.println("i and j: " + i + " " + j) ; } } // создать подкласс путем расширения класса Л class В extends А { int к; В (int a, int b, int с) { super(а, Ь); к = с; } // Сигнатуры данного метода и метода show из класса А отличаются, // поэтому вместо переопределения происходит перегрузка метода. void show(String msg) { System.out.println(msg + k); } } class Overload { public static void main(String args[]) { В subOb = new В(1, 2, 3); subOb.show("This is k: "); // вызывается метод show из класса В subOb.show; // вызывается метод show из класса А } }
Выполнение этого фрагмента кода дает следующий результат: This is k: 3 i and j: 1 2
На этот раз в варианте метода show из класса В предусмотрен строковый параметр. И благодаря этому сигнатура данного метода отличается от сигнатуры метода show из класса А, для которого параметры не предусмотрены. В результате переопределение метода не происходит. Поддержка полиморфизма
в переопределяемых методах Примеры из предыдущего раздела демонстрируют переопределение методов, но по ним трудно судить, насколько богатые возможности предоставляет этот механизм. В самом деле, если переопределение методов используется только для соблюдения соглашений о пространствах имен, то его можно рассматривать как любопытную, но почти бесполезную особенность языка программирования. Но это совсем не так. Переопределение методов лежит в основе одного из наиболее эффективных языковых средств Java: динамической диспетчеризации методов, представляющей собой механизм вызова переопределяемых методов, когда выбор конкретного метода осуществляется не на этапе компиляции, а в процессе выполнения программы. Динамическая диспетчеризация методов имеет очень большое значение, поскольку именно с ее помощью принцип полиморфизма реализуется на стадии выполнения программ на Java.
Прежде всего напомним один очень важный принцип: переменная ссылки на суперкласс может ссылаться на объект подкласса. В Java этот принцип используется для вызова переопределяемых методов во время выполнения. Если переопределяемый метод вызывается по ссылке на суперкласс, то исполняющая система Java определяет по типу объекта, какой именно вариант метода следует вызвать, причем делает это во время выполнения программы. Если ссылки указывают на разные типы объектов, то и вызываться будут разные версии переопределенных методов. Иными словами, вариант переопределенного метода для вызова определяет не тип переменной, а тип объекта, на который она ссылается. Так, если суперкласс содержит метод, переопределяемый в подклассе, то вызывается метод, соответствующий тому типу объекта, на который указывает переменная ссылки на суперкласс.
Ниже приведен простой пример, демонстрирующий динамическую диспетчеризацию методов в действии. // Демонстрация динамической диспетчеризации методов. class Sup { void who { System.out.println("who in Sup"); } } class Subl extends Sup { void who { System.out.println("who in Subl"); } } class Sub2 extends Sup { void who { System.out.println("who in Sub2"); } } class DynDispDemo { public static void main(String args[]) { Sup superOb = new Sup; Subl subObl = new Subl; Sub2 sub0b2 = new Sub2; Sup supRef; // В каждом из приведенных ниже вызовов конкретный вариант // метода who выбирается во время выполнения по типу // объекта, на который делается ссылка. supRef = superOb; supRef.who; supRef = subObl; supRef.who; supRef = sub0b2; supRef.who; } }
Результат выполнения данной программы выглядит следующим образом: who in Sup who in Subl who in Sub2
В данном примере программы определяются суперкласс Sup и два его подкласса Subl и Sub2. В классе Sup объявляется метод who , переопределяемый в его подклассах. В методе main создаются объекты типа Sup, Subl и Sub2. Там же объявляется переменная supRef ссылки на объект типа Sup. Затем переменной supRef в методе main поочередно присваиваются ссылки на объекты разного типа, и далее эти ссылки используются для вызова метода who . Как следует из результата выполнения данной программы, вызываемый вариант метода who определяется типом объекта, на который ссылается переменная supRef в момент вызова, а не типом самой этой переменной. Причины для переопределения методов
Как упоминалось выше, переопределяемые методы обеспечивают соблюдение принципа полиморфизма при выполнении программ на Java. Полиморфизм в объектно-ориентированных программах имеет большое значение потому, что благодаря ему появляется возможность объявить в суперклассе методы, общие для всех его подклассов, а в самих подклассах — определить специфические реализации всех этих методов или некоторых из них. Переопределение методов — один из способов, которыми в Java реализуется принцип полиморфизма “один интерфейс — множество методов”.
Залогом успешного применения полиморфизма является, в частности, понимание того, что суперклассы и подклассы образуют иерархию в направлении от меньшей специализации к большей. Если суперкласс организован правильно, он предоставляет своему подклассу все элементы, которыми тот может пользоваться непосредственно. В нем также определяются те методы, которые должны быть по-своему реализованы в порожденных классах. Таким образом, подклассы получают достаточную свободу определять собственные методы, реализуя в то же время согласованный интерфейс. Сочетая наследование с переопределением методов, в суперклассе можно определить общую форму методов для использования во всех его подклассах. Демонстрация механизма переопределения методов на примере класса TwoDShape Для того чтобы стало понятнее, насколько эффективным является механизм переопределения методов, продемонстрируем его на примере класса TwoDShape. В приведенных ранее примерах в каждом классе, порожденном от класса TwoDShape, определялся метод area . Теперь мы знаем, что в этом случае имеет смысл включить метод area в состав класса TwoDShape и позволить каждому его подклассу переопределить этот метод: в частности, реализовать вычисление площади в зависимости от конкретного типа геометрической фигуры. Именно такой подход и воплощен в приведенном ниже примере программы. Для удобства в класс TwoDShape добавлено поле name, упрощающее написание демонстрационных программ. // Применение динамической диспетчеризации методов. class TwoDShape { private double width; private double height; private String name; // Конструктор по умолчанию. TwoDShape { width = height = 0.0; name = "null"; } // Параметризированный конструктор. TwoDShape(double w, double h, String n) { width = w; height = h; name = n; } // построить объект с одинаковыми значениями // переменных экземпляра width и height TwoDShape(double х, String n) { width = height = x; name = n; } // построить один объект на основании другого объекта TwoDShape(TwoDShape ob) { width = ob.width; height = ob.height; name = ob.name; } // Методы доступа к переменным width и height, double getWidth { return width; } double getHeightO { return height; } void setWidth(double w) { width = w; } void setHeight(double h) { height = h; } String getName { return name; } void showDim { System.out.println("Width and height are " + width + " and " + height); } // Метод area определен в классе TwoDShape. double area { System.out.println("area must be overridden"); return 0.0; } } // Подкласс, производный от класса TwoDShape, // для представления треугольников, class Triangle extends TwoDShape { private String style; // Конструктор по умолчанию. Triangle { super; style = "null"; } // Конструктор класса Triangle. Triangle(String s, double w, double h) { super(w, h, "triangle"); style = s; } // Конструктор с одним аргументом для построения треугольника Triangle(double х) { super(х, "triangle"); // вызвать конструктор суперкласса style = "isosceles"; } // построить один объект на основании другого объекта Triangle(Triangle ob) { super(ob); // передать объект конструктору класса TwoDShape style = ob.style; } // Переопределение метода area для класса Triangle. double area { return getWidth * getHeight / 2; } void showStyle { System.out.println("Triangle is " + style); } } // Подкласс, производный от класса TwoDShape, // для представлёния прямоугольников, class Rectangle extends TwoDShape { // Конструктор по умолчанию. Rectangle { super; } // Конструктор класса Rectangle. Rectangle(double w, double h) { super(w, h, "rectangle"); // вызвать конструктор суперкласса } // построить квадрат Rectangle(double х) { super(х, "rectangle"); // вызвать конструктор суперкласса } // построить один объект на основании другого объекта Rectangle(Rectangle ob) { super(ob); // передать объект конструктору класса TwoDShape } boolean isSquare { if (getWidth == getHeightO) return true; return false; } // Переопределение метода area для класса Rectangle. double area { return getWidth * getHeightO; } } class DynShapes { public static void main(String args[]) { TwoDShape shapes[] = new TwoDShape[5]; shapes[0] = new Triangle("right", 8.0, 12.0); shapes[1] = new Rectangle(10); shapes[2] = new Rectangle(10, 4); shapes[3] = new Triangle(7.0); shapes[4] = new TwoDShape(10, 20, "generic"); for (int i=0; i < shapes.length; i++) { System.out.println("object is " + shapes[i].getName); // Требуемый вариант метода area вызывается // для каждой геометрической фигуры в отдельности. System.out.println("Area is " + shapes[i].area); System.out.println ; } } }