Конструктор класса BaseCourse

GetId() класса BaseCourse

id=90

Конструктор класса Course

GetId() класса BaseCourse

id=0

Конструктор класса BaseCourse

GetId() класса BaseCourse

id=90

objA: id=0

objB: id=90

Конструктор класса Course

GetId() класса Course

id=71

Метод getId() содержится как в классе Course, так и в классе BaseCourseи является переопределенным. При создании объекта класса BaseCourseодним из способов:

Course objA = new BaseCourse();

BaseCourse objB = new BaseCourse();

в любом случае перед вызовом конструктора BaseCourse() вызывается конструктор класса Course. Но так как в обоих случаях создается объект класса BaseCourse, то вызывается метод getId(), объявленный в классе
BaseCourse, который в свою очередь оперирует полем id, еще не проинициализированным для класса BaseCourse. В результате idполучит значение по умолчанию, т.е. нуль.

Воспользовавшись преобразованием типов вида ((BaseCourse)objA).idили ((Course)objB).id, легко можно получить доступ к полю id из соответствующего класса.

Использование final

Нельзя создать подкласс для класса, объявленного со спецификатором final:

// класс ConstCourse не может быть суперклассом

final classConstCourse {/*код*/}

// следующий класс невозможен

class BaseCourse extends ConstCourse {/*код*/}

Использование super и this

Ключевое слово super используется для вызова конструктора суперкласса и для доступа к члену суперкласса. Например:

super(список_параметров);/* вызов конструктора суперкласса

с передачей параметров или без нее*/

super.id = 71; /* обращение к атрибуту суперкласса */

super.getId(); // вызов метода суперкласса

Вторая форма super используется для доступа из подкласса к переменной id суперкласса. Третья форма специфична для Java и обеспечивает вызов из подкласса переопределенного метода суперкласса, причем если в суперклассе этот метод не определен, то будет осуществляться поиск по цепочке наследования до тех пор, пока метод не будет найден.

Каждый экземпляр класса имеет неявную ссылку this на себя, которая передается также и методам. После этого метод «знает», какой объект его вызвал. Вместо обращения к атрибуту id в методах можно писать this.id,хотя и не обязательно, так как записи id и this.id равносильны.

Point2D
-
x: int
-
y: int
+
Point2D(int, int)
Point3D
-
z: int
+
Point3D(int, int, int)
+
Point3D()
Point4D
-
ltime: long
+
Point4D(int, int, int, long)
+
Point4D()

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

 

// пример # 3 : this в конструкторе: Point2D.java, Point3D.java, Point4D.java

package chapt04;

 

public class Point2D {

private int x, y;

 

public Point2D(int x, int y) {

this.x = x;//this используется для присваивания полям класса

this.y = y;//x, y, значений параметров конструктора x, y, z

}

}

package chapt04;

 

public class Point3D extends Point2D {

private int z;

 

public Point3D(int x, int y, int z) {

super(x, y);

this.z = z;

}

public Point3D() {

this(-1,-1,-1);// вызов конструктора Point3D с параметрами

}

}

package chapt04;

 

public class Point4D extends Point3D{

private long time;

 

public Point4D(int x, int y, int z, long time) {

super(x, y, z);

this.time = time;

}

public Point4D() {

// по умолчанию super();

}

}

В классе Point3D второй конструктор для завершения инициализации объекта обращается к первому конструктору. Такая конструкция применяется в случае, когда в класс требуется добавить конструктор по умолчанию с обязательным использованием уже существующего конструктора.

Ссылка this используется в методе для уточнения того, о каких именно переменных x, y и z идет речь в методе, а конкретно для доступа к переменным класса из метода, если в методе есть локальные переменные с тем же именем, что и у класса. Инструкция this() должна быть единственной в вызывающем конструкторе и быть первой по счету выполняемой операцией.

Переопределение методов и полиморфизм

Способность Java делать выбор метода, исходя из типа объекта во время выполнения, называется поздним связыванием. При вызове метода его поиск происходит сначала в данном классе, затем в суперклассе, пока метод не будет найден или не достигнут Object – суперкласс для всех классов.

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

Course
-
id: int
-
name: String
+
toString() : void
BaseCourse
-
idTeacher: int
+
toString() : void
OptionalCourse
-
required: int
+
toString() : void

Рис. 4.1. Пример реализации полиморфизма

/* пример # 4 : динамическое связывание методов: Course.java: BaseCourse.java: OptionalCourse.java: DynDispatcher.java */

package chapt04;

 

public class Course {

private int id;

private String name;

 

public Course(int i, String n) {

id = i;

name = n;

}

public String toString() {

return "Название: " + name + "(" + id + ")";

}

}

package chapt04;

 

public class BaseCourse extends Course {

private int idTeacher;

 

public BaseCourse(int i, String n, int it) {

super(i, n);

idTeacher = it;

}

public String toString() {

/* просто toString() нельзя!!!

метод будет вызывать сам себя, что

приведет к ошибке во время выполнения */

Return

super.toString() + " препод.(" + idTeacher + ")";

}

}

package chapt04;

 

public class OptionalCourse extends BaseCourse {

private boolean required;

public OptionalCourse(int i, String n, int it,

boolean r) {

super(i, n, it);

required = r;

}

public String toString() {

return super.toString() + " required->" + required;

}

}

package chapt04;

 

public class DynDispatcher{

public void infoCourse(Course c) {

System.out.println(c.toString());

//System.out.println(c);//идентично

}

}

package chapt04;

 

public class Runner {

public static void main(String[] args) {

DynDispatcher d = new DynDispatcher();

Course cс = new Course(7, "МА");

d.infoCourse(cc);

BaseCourse bc = new BaseCourse(71, "МП", 2531);

d.infoCourse(bc);

OptionalCourse oc =

new OptionalCourse(35, "ФА", 4128, true);

d.infoCourse(oc);

}

}

Результат:

Название: МА(7)

Название: МП(71) препод.(2531)

Название: ФА(35) препод.(4128) required->true

Следует помнить, что при вызове toString() обращение super всегда происходит к ближайшему суперклассу. Аналогично при вызове super() в конструкторе обращение происходит к соответствующему конструктору непосредственного суперкласса.

Основной вывод: выбор версии переопределенного метода производится на этапе выполнения кода.

Все методы Java являются виртуальными (ключевое слово virtual, как в C++, не используется).

Статические методы могут быть переопределены в подклассе, но не могут быть полиморфными, так как их вызов не затрагивает объекты. Их следует вызывать только с использованием имени класса.

Методы подставки

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

/* пример # 5 : методы-подставки: CourseHelper.java:

BaseCourseHelper.java: RunnerCourse.java*/

package chapt04;

 

public class CourseHelper {

public Course getCourse(){

System.out.println("Course");

return new Course();

}

}

package chapt04;

 

public class BaseCourseHelper extends CourseHelper {

public BaseCourse getCourse(){

System.out.println("BaseCourse");

return new BaseCourse();

}

}

package chapt04;

 

public class RunnerCourse {

public static void main(String[] args) {

CourseHelper bch = new BaseCourseHelper();

Course course = bch.getCourse();

//BaseCourse course = bch.getCourse();//ошибка компиляции

System.out.println(bch.getCourse().id);

}

}

В данной ситуации при компиляции в подклассе BaseCourseHelper создаются два метода. При обращении к методу getCourse() версия метода определяется «ранним связыванием» без использования полиморфизма, но при выполнении вызывается метод-подставка. Обращение к полю производится по типу ссылки, возвращаемой методом getCourse(), то есть к полю класса Course.

Полиморфизм и расширяемость

В объектно-ориентированном программировании применение наследования предоставляет возможность расширения и дополнения программного обеспечения, имеющего сложную структуру с большим количеством классов и методов. В задачи базового класса в этом случае входит определение интерфейса (как способа взаимодействия) для всех наследников.

В следующем примере приведение к базовому типу происходит в выражении:

Transport s1 = new Bus();

Transport
+
repair() : void
Bus
+
repair() : void
Tram
+
repair() : void

Transport s2 = new Tram();

Рис. 4.2. Пример реализации полиморфизма

Базовый класс Transport предоставляет общий интерфейс для своих подклассов. Порожденные классы Bus и Tram перекрывают эти определения для обеспечения уникального поведения.

/* пример # 5 : полиморфизм: Transport.java: Bus.java: Tram.java:

RepairingCenter.java: Runner.java*/

package chapt04;

import java.util.Random;

 

class Transport {

public void repair() {/* пустая реализация */

}

}

class Bus extends Transport {

public void repair() {

System.out.println("отремонтирован АВТОБУС");

}

}

class Tram extends Transport {

public void repair() {

System.out.println("отремонтирован ТРАМВАЙ");

}

}

class RepairingFactory {//шаблон Factory

public Transport getClassFromFactory(int numMode) {

switch (new Random().nextInt(numMode)) {

case 0:

return new Bus();

case 1:

return new Tram();

default:

throw new IllegalArgumentException();

// assert false;

// return null;

/*

* if((int)(Math.random() * numMode)==0) return new Bus(); else

* return new Tram(); как альтернативный и не очень удачный

* вариант. Почему?

*/

}

}

}

public class Runner {

public static void main(String[] args) {

RepairingFactory rc = new RepairingFactory();

Transport[] box = new Transport[15];

 

for (int i = 0; i < box.length; i++)

/* заполнение массива единицами проверямого транспорта */

box[i] = rc.getClassFromFactory(2);// 2 вида транспорта

for (Transport s : box)

s.repair();// вызов полиморфного метода

}

}

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

Класс RepairingFactory содержит метод getClassFromFactory(int numMode), который возвращает ссылку на случайно выбранный объект подкласса класса Transport каждый раз, когда он вызывается. Приведение к базовому типу производится оператором return, который возвращает ссылку на Bus или Tram. Метод main() содержит массив из ссылок Transport, заполненный с помощью вызова getClassFromFactory(). На этом этапе известно, что имеется некоторое множество ссылок на объекты базового типа и ничего больше (не больше, чем знает компилятор). Kогда происходит перемещение по этому массиву, метод repair() вызывается для каждого случайным образом выбранного объекта.

Если понадобится в дальнейшем добавить в систему, например, класс TrolleyBus, то это потребует только переопределения метода repair() и добавления одной строки в код метода getClassFromFactory(), что делает систему легко расширяемой.

Статические методы и полиморфизм

Переопределение статических методов класса не имеет практического смысла, так как обращение к статическому атрибуту или методу осуществляется посредством задания имени класса, которому они принадлежат. К статическим методам принципы «позднего связывания» неприменимы. При использовании ссылки для доступа к статическому члену компилятор при выборе метода или поля учитывает тип ссылки, а не тип объекта, ей присвоенного.

/* пример # 6 : поведение статического метода при «переопределении»: Runner.java */

package chapt04;

 

class Base {

public static void assign() {

System.out.println(

"метод assign() из Base");

}

}

class Sub extends Base {

public static void assign() {

System.out.println(

"метод assign() из Sub");

}

}

public class Runner {

public static void main(String[] args) {

Base ob1 = new Base();

Base ob2 = new Sub();

Sub ob3 = new Sub();

ob1.assign();//некорректный вызов статичесого метода

ob2.assign();//следует вызывать Base.assign();

ob3.assign();

}

}

В результате выполнения данного кода будет выведено:

Метод assign() из Base

Метод assign() из Base

Метод assign() из Sub

При таком способе инициализации объектов ob1 и ob2, метод assign() будет вызван из класса Base. Для объекта ob3 будет вызван собственный метод assign(), что следует из способа объявления объекта. Если же спецификатор static убрать из объявления методов, то вызовы методов будут осуществляться в соответствии с принципами полиморфизма.

Статические методы всегда следует вызывать через имя класса, в котором они объявлены, а именно:

Base.assign();

Sub.assign();

Вызов статических методов через объект считается нетипичным и нарушающим смысл статического определения.

Абстракция и абстрактные классы

Множество предметов реального мира обладает некоторым набором общих характеристик и правил поведения. Абстрактное понятие «Геометрическая фигура» может содержать описание геометрических параметров и расположения центра тяжести в системе координат, а также возможности определения площади и периметра фигуры. Однако в общем случае дать конкретную реализацию приведенных характеристик и функциональности невозможно ввиду слишком общего их определения. Для конкретного понятия, например «Квадрат», дать описание линейных размеров и определения площади и периметра не составляет труда. Абстрагирование понятия должно предоставлять абстрактные характеристики предмета реального мира, а не его ожидаемую реализацию. Грамотное выделение абстракций позволяет структурировать код программной системы в целом и повторно использовать абстрактные понятия для конкретных реализаций при определении новых возможностей абстрактной сущности.

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

С помощью абстрактного класса объявляется контракт (требования к функциональности) для его подклассов. Примером может служить уже рассмотренный выше абстрактный класс Number и его подклассы Byte, Float и другие. Класс Number объявляет контракт на реализацию ряда методов по преобразованию данных к значению конкретного базового типа, например
floatValue(). Можно предположить, что реализация метода будет различной для каждого из классов-оболочек. Хотя объект класса Number нельзя создать, он может получить численное значение любого базового типа. Однако у самого класса нет возможности преобразовать это значение к конкретному базовому типу.

/* пример # 7 : абстрактный класс и метод : AbstractManager.java */

package chapt04;

 

public abstract class AbstractManager {

private int id;

public AbstractManager(int id) {// конструктор

this.id = id;

}

// абстрактный метод

public abstract void assignGroupToCourse(

int groupId, String nameCourse);

}

/* пример # 8 : подкласс абстрактного класса : CourseManager.java */

package chapt04;

 

// assignGroupToCourse() должен быть реализован в подклассе

public class CourseManager extends AbstractManager {

public void assignGroupToCourse(

int groupId, String nameCourse) {

// ...

System.out.println("группа " + groupId

+ " назначена на курс " + nameCourse);

}

}

/* пример # 9 : объявление объектов и вызов методов : Runner.java */

package chapt04;

 

public class Runner {

public static void main(String[] args) {

AbstractManager mng; // можно объявить ссылку

// mng = new AbstractManager();нельзя создать объект!

mng = new CourseManager();

mng.assignGroupToCourse(10, "Алгебра");

}

}

В результате будет получено: