ITI0011RUS:Objects

Allikas: Kursused
Redaktsioon seisuga 16. märts 2015, kell 18:17 kasutajalt Aleksandr (arutelu | kaastöö) (→‎Типы и подтипы)
(erin) ←Vanem redaktsioon | Viimane redaktsiooni (erin) | Uuem redaktsioon→ (erin)
Mine navigeerimisribale Mine otsikasti

Вернуться на страницу предмета

Объект-ориентированное программироваие (OOП)

Объектно-ориентированное программирование (англ. object oriented programming, aka OOP) это разновидность программирования, при котором программа рассматривается как набор объектов. Java по большей части объектно-ориентировнный язык программирование (элементарные типыданных, такие как int, double и т.д. не являются объектами).

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

Основные цели ООП:

  • Инкапсуляция информации (encapsulation). Другие объекты могут пользоваться не всеми методами и переменными данного объекта, а только теми, которыми им разрешено пользоваться.
  • Модульность (modularity). Разделение кода на более или менее независимые модули - классы. Каждый класс выполняет одну конкретную задачу и только ему (классу) известными способами - другие модули не знают как какой-то конкретный модуль выполняет свои задачи - все, что им следует знать - это какой будет результат работы модуля при определенных входных данных, логика работы самого модуля - "черный ящик".
  • Полиморфизм (polymorphism). Один и тот же метод в разных объектах может вести себя по-разному.
  • Наследование (inheritance). Классы могут образовывать иерархию, в которой производные (под)классы наследуют определенные методы и переменные родительского класса, помимо этого, дополняют его своими собственными переменными и методами.
  • Повторное использование кода. Готовые модули, написанные для одной программы, могут использоваться повторно для других целей в других проектах.

Основная концепция ООП

Класс - это часть кода (функционала/логики) программы, которую можно повторно использовать для разных целей несколько раз в разных контекстах. Центральный термин ООП - объект, который является экземпляром своего класса, обладающий набором данных (состояние) и функционалом (поведение). Несколько объектов могут быть объектами одного и того же класса. Объект может быть объектом только одного класса.

Для описания состояния объекта используют следующие термины: атрибут (attribute), свойство (property), поле (объекта) ((instance) field), переменная (объекта) ((instance) variable).


Идеология ООП

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

ООП позволяет (в случае более крупных программ):

  • лучше структурировать код программы
  • поддерживать код в понятной для понимания форме
  • дополнения/изменения в код делать легче

и т.д.

Класс и объект

Класс описывает абстрактные свойства объектов. Класс похож на шаблон, который описывает свойства чего-либо. Например, класс Собака описывал бы свойства, которые присущи всем собакам - такие как, например, порода, цвет, навыки (лаяние, сидение, бег). Объект - конкретная собака с набором значений параметров, присущим конкретно этой собаке. Таким образом, класс - это описание свойств объектов - то, какими свойствами будут обладать все объекты данного класса. Значения соответствующих свойств задаются объектами класса - у нескольких разных объектов одного и того же класса набор свойств один и тот же, а вот значения этих свойств различны.

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

В ООП часто вместо термина объект используется термин экземпляр (instance) класса.

В языке Java свойства класса подразделяются на поля и методы класса:

  • статическое поле (public static int dogCount;) это переменная, принадлежащая классу (не объекту!). Если класс описывает всех собак, то и статическая переменная описывает также свойство всех собак. Если где-то (в каком-то месте кода) значение этого поля меняется - оно меняется во всей программе.
  • не статическое поле (String name;), также известное как переменная класса или переменная экземпляра, описывает свойства конкретного объекта (собаки), в данном случае, имя конкретной собаки. Имя каждой собаки может отличаться. Если изменить имя в одном объекте, в других объектах значение этого поля не поменяется.

Класс также описывает действия, или поведение объектов данного класса. Эти действия называются методами (фактически это просто функции, но в терминологии ООП они называются методами). Методы позволяют объектам выполнять те или иные действия. Например, у объектов класса "Собака" может быть метод лаять, который заставляет лаять конкретную собаку, в чьем объекте метод был вызван.

  • статический метод (public static int getDogLimit()), также называемый функцией или методом класса, это функция, которая принадлежит классу и не зависит от конкретного объекта. Например количество собак, которые могут находиться в конуре не зависит от конкретных собак. Это функция, единая в рамках всей программы.
  • не статический метод (public void bark()), также называемый функцией или методом объекта, привязан к конкретному объекту. Этот метод или функция вызывается отдельно для каждого объекта (можно вызывать этот метод и для всех объектов, вызвав этот метод у каждого объекта в отдельности).

Рассмотрим пример кода: <source lang="java">

/**

* Describes a student which
* has some test results.
*
*/

public class Student { /** * The name of the student. */ public String name; /** * Unique ID over all the students. */ public int ID; /** * The results of three tests. */ public double test1, test2, test3;

/** * Calculates the average test result. * @return Average test result */ public double getAverage() { return (test1 + test2 + test3) / 3; }

/** * Static variable which holds the next unique id. * The value of this variable is usually student count. * This is the same for all the students. */ private static int nextUniqueID = 0;

/** * Gets a unique ID for the student. * The ID number itself is increased. * @return Unique id for the student. */ public static int getUniqueID() { nextUniqueID++; return nextUniqueID; } }

</source>

В примере выше в классе студент присутствуют как статические, так и не статические методы. Статический метод getUniqueID() не зависит от какого-либо конкретного студента. Он зависит лишь от того, какие значения уже были назначены (уникальность значения предполагает, что ни у какого другого студента такого номера больше нет). Самый простой способ сгенерировать уникальный номер заключается в том, что каждому новому студенту генерируется номер, который на единицу больше чем номер, сгенерированный последнему студенту. Если в университете 100 студентов, то нет необходимости обращаться к каждому из них в отедельности, чтобы получить уникальный номер. Поэтому этот метод и является статическим, поскольку он един для всей программы (например, един для всех студентов в университете).

Метод getAverage() связан с конкретным студентом. В данном примере предполагается, что каждый студент знает только свои оценки. Если теперь у кого-либо конкретного студента спросить какова его средняя оценка, то он сможет ответить на этот вопрос (посчитает арифметическое среднее значение своих оценок).

Создание объектов

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

Таким образом, имя класса используется как тип данных. Например, мы можем создать переменную, у которой тип данных Student: <source lang="java"> Student std; </source>

В Javas объявление переменной (как показано в примере выше) еще не создает объект - мы только создаем переменную для хранения объектов данного типа данных. В общем:

  • В Java ни одна переменная не хранит объект в себе.
  • Переменная хранит лишь ссылку на объект (сам объект находится где-то в другой области памяти процесса).

Таким образом, в Java любая переменная для хранения объектов (все равно каких, созданных вами, или встроенных объеков Java), содержит в себе лишь ссылку на этот объект в памяти. Конкретное значение ссылки (reference) или указателя (pointer) программист не знает. При создании объекта, Java проиводит все необходимые действия (выделяет память для хранения объекта и сохраняет ссылку на эту область памяти в переменной).

Создание объекта осуществляется ключевым словом new: <source lang="java"> std = new Student(); </source>

Код выше создает новый объект класса Student в памяти, и записывает ссылку на этот объект в переменную std. Проинициализированная таким образом переменная std используется в качестве объекта. Чтобы обратиться к полям или методам данного объекта, можно использовать созданную переменную, например std.name .

Null ссылка

Бывают случаи, когда требуется показать, что переменная, подобная std, тип данных которой - класс, не ссылается ни на один существующий объект. В этом случае переменная std хранит так называемую null-ссылку или пустую ссылку (null reference). В Java null-ссылка обозначается как null. Можно присвоить переменной значение null-ссылки следующим образом: <source lang="java"> std = null; </source> и проверка, содержит ли переменная null-ссылку выглядит следующим образом: <source lang="java"> if (std == null) { </source>

Если значением переменной является null, то фактически у нас нет никакого объекта к которому можно было бы обратиться. Если например std = null;, то обращение к полю std.name приведет к исключению null pointer exception (NullPointerException).

Экземпляр класса

Конкретный объект называют экземпляром своего класса (instance). Ниже приведен пример того, как создавать различные экземпляры описанного ранее класса Student.

<source lang="java"> // Declare four variables of type Student Student std, std1, std2, std3; // Create a new object belonging to the class Student, // store a ref to that object in the var std. std = new Student(); std1 = new Student(); // Create a second Student object std2 = std1; // Copy the reference value in std1 into the variable std2. std3 = null; // Store a null reference in the variable std3. std.name = "John Smith"; // Set values of some // instance vars, getUniqueIdisa static method, // therefore is accessed from class Student // (not object instance like std) std.ID = Student.getUniqueID(); std1.name = "Mary Jones"; std1.ID = Student.getUniqueID(); // (Other instance variables have default initial values of zero.) </source>

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

Java-juhend-Objects-in-heap.png

Если кака-либо переменная ссылается на объект - то это обозначено на картинке стрелкой. Видно, что std, std1 и std2 ссылаются на объект типа Student, причем std1 и std2 ссылаются на один и тот же объект. Если несколько переменных ссылаются на один объект, и через одну из переменных в объекте что-то меняют (например, имя), то при чтении этого поля через другие переменные, полуим новое (обновленное значение). Тоесть сама переменная - это лишь ссылка на область памяти (как бы адрес объекта в памяти). Если несколько переменных ссылаются на один и тот же объект в памяти, то при изменении чего-то в объекте все переменные "видят" это изменение.

Стоит обратить внимание на то, что String также является объектом - тоесть имя студента также является ссылкой на какую-то область памяти, где хранятся данные об имени.

Если переменной не примитивного типа (тип данных переменной является объектом) присваивается значение другой не примитивной переменной (в примере выше std2 = std1), то std2 начнет указывать на тот же объект, что и std1 - тоесть std2 присваивается та же ссылка (адрес в памяти), что и у переменной std1. В результате такого присваивания обе переменные будут ссылаться на ту же самую область памяти. std1.name и std2.name будут ссылаться на одно и то же имя. Если, например, используя std1 изменить имя, то при прочтении его используя std1 имя сменится. std1.name и std2.name - две разные возможности сослаться на одну и ту же область памяти.

Конструктор

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

  • при создании объекта нужно передать какие-либо определенные параметры, которыми следует проинициализировать поля класса.
  • создание объекта для определенных внешних объектов/классов запрещено.

Пример того, когда при создании объекта следует передать явно какие-либо параметры его инициализации. У каждого студента есть имя. Если использовать только конструктор по умолчанию (который вообще не инициализирует это поле), придется вызывать метод student.setName("..."), чтобы задать имя студенту. Но то же самое можно сделать и сразу при инициализации объекта, если создать вариант конструктора, который принимает имя в качестве параметра и инициализирует поле name переданным значением.

Student.java: <source lang="java"> public class Student { private String name;

public Student(String name) { this.name = name; } } </source>

Теперь можно создать объект этого класса следующим образом: <source lang="java"> Student s = new Student("Reinuvader"); </source>

Причем следующий код вернет ошибку: <source lang="java"> Student s = new Student(); </source>

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

<source lang="java"> public static class Student { private String name;

public Student() { }

public Student(String name) { this.name = name; } } </source>

Эта техника называется перегрузкой конструктора. Перегрузка означает, что в классе есть несколько методов с одинаковым именем, но разным набором аргументов (разным количеством аргументов или разными типами данных аргуменов). Если в коде объект создается как new Student();, то запускается конструктор без параметров. Если объект создается как new Student("Koolipoiss");, то запускается тот конструктор, который принимает имя строку в качестве аргумента.

При объявлении конструктора обратите внимание на некоторые особенности:

  • нет возвращаемого значения
  • имя метода идентично имени класса

Сравнение объектов

При сравнении двух переменных, которые ссылаются на какие-либо объекты if (std1 == std2) проверяется, ссылаются ли обе переменные на одну и ту же область памяти. Сравниваются только адреса в памяти, а данные, хранящиеся в этой области памяти, не анализируются.

String является объектом в Java. Пусть у нас будут две строки str1 и str2. При их сравнении if (str1 == str2) результатом будет значение true только если обе переменные ссылаются на один и тот же объект (на один и тот же адрес в памяти). Часто в программах этого недостаточно - нам нужно сравнить содержимое объектов, находящихся по разным адресам. Пусть у нас есть две строки с одинаковым содержимым: <source lang="java"> String str1 = "hello"; String str2 = "hello"; </source>

В примерах выше мы видели, что при создании объектов используется инструкция new, но в примере со строками она не была использована. Поскольку тип данных "слово" - очень распространенный тип данных, то в Java сделали т.н. упрощенный вариант создания подобных объектов, в котором инструкцию new использовать не нужно. В случае создания объектов таким образом, при создании объекта не создается новый объект каждый раз заново. Если строка с таким же содержимым уже находится в памяти (например, по адресу 123), то при создании еще одной строки (str2) с точно таким же содержимым, переменная str2 инициализируется значением, по которому в памяти находится строка hello - адресом 123. Если сравнить эти две переменные if (str1 == str2), то с большой вероятностью в результате получим значение true.

Строку можно создать и "классическим" способом: String str3 = new String("hello");. В этом случае при создании объекта каждый раз в памяти будет выделяться новая область памяти. Если str3 сравнить с str2 или str1, в результате получим false, поскольку str3 ссылается на совсем другую область памяти, чем str1 и str2 (которые ссылаются на одну и ту же область памяти).

Пример кода: <source lang="java"> public class StringExample { public static void main(String[] args) { String str1 = "tere"; String str2 = "tere"; // str1 and str2 point to the same memory location System.out.println(str1 == str2); // and the contents are equal System.out.println(str1.equals(str2));

// enforce new object creation String str3 = new String("tere"); // now str3 is stored in a separate location in memory System.out.println(str1 == str3); // but the contents are still equal System.out.println(str1.equals(str3)); } } </source>

Наследование

Класс описывает объекты схожей структуры и поведения. В дополнение к этому можно создать классы, которые описывают объект лишь частично, описывая лишь часть его структуры или поведения. Такие классы можно "наследовать", что значит, что при наследовании классы-потомки получают определенные части структуры и поведения, которые содержатся в "родительским" классе. Иногда вместо термина "наследование" используется термин "расширения".

В Java, класс, который наследует, называется под-классом (subclass), а класс, от которого наследуют, называется сверх-классом (superclass). В объявлении класса можно указать, что создаваемый класс является подклассом какого-то другого класса. Например, если нужно описать класс B и этот класс должен наследовать часть структуры и поведения класса A, то класс B объявляется следующим образом:

<source lang="java"> class B extends A {

   // additions to, and modifications of,
   // stuff inherited from class A

} </source>

Несколько классов могут наследовать один и тот же класс. Например, классы B, C и D все наследуют класс А. Можно сказать, что классы B, C и D частично схожи в структуре и поведении (но только частично).

Java-juhend-Subclass-superclass.png

В левой части рисунка показан "упрощенный" случай, когда один класс наследует другой класс. В правой части рисунка показана более развернутая версия этого наследования.

Пример наследования: транспортные средства

Создадим классы для описания транспортных средств. Пусть будет общий на все транспортные средства класс, который так и назовем "транспортное средство" (Vehicle). Помимо этого класса, создадим также классы машина (car), грузовик (truck), и мотоцикл (motorcycle) для описания различных типов транспортных средств. Все эти три класса наследуют класс Vehicle. В графическом представлении это выглядело бы следующим образом:

Java-juhend-Vehicle-hierarchy.png

Класс транспортного средства Vehicle мог бы содержать такие свойства, которые есть у всех транспортных средств, вне зависимости от конкретного типа транспортного средства: например, регистрационный номер, владелец, и какой-нибудь метод, например, transferOwnership(). Все поля и методы, описанные в классе Vehicle, будут присутствовать в классах, которые его наследуют - в Car, Truck, и Motorcycle.

В дополнение к этим свойствам, каждый из трех классов транспортных средств может иметь свои поля и методы, характерные для объектов только этого класса. Например:

  • Класс Car может содержать поле numberOfDoors.
  • Класс Truck может содержать поле numberOfAxels.
  • Класс Motorcycle может содержать поле hasSidecar.

В коде все это могло бы выглядеть следующим образом:

<source lang="java"> class Vehicle {

  int registrationNumber;
  Person owner;  // (Assuming that a Person class has been defined!)
  void transferOwnership(Person newOwner) {
      . . .
  }
  . . .

}

class Car extends Vehicle {

  int numberOfDoors;
  . . .

}

class Truck extends Vehicle {

  int numberOfAxles;
  . . .

}

class Motorcycle extends Vehicle {

  boolean hasSidecar;
  . . .

} </source>

В этом примере показаны 4 различных класса. Технически это означает, что весь код разделен на 4 файла - каждый класс в своем отдельном файле.

Создать объект какого-либо из транспортных средств можно следующим образом: <source lang="java"> Car myCar = new Car(); // from the Car itself System.out.println(myCar.numberOfDoors); // and from superclass Vehicle System.out.println(myCar.registrationNumber); System.out.println(myCar.owner); myCar.transferOwnership(); </source>

Из примера выше видно, то в классе Car мы можем обращаться как к полям, которые описаны в классе Car, так и к полям, которые описаны в классе Vehicle.

Типы и подтипы

Рассмотрим пример транспортных средств, описанный выше. В реальной жизни легковая машина, грузовик, и мотоцикл - транспортные средства разного типа. Но все же их всех можно описать одним словом - транспортное средство. Точно так же мы сделали в нашей программе - классы Car, Truck, и Motorcycle наследуют класс Vehicle.

Переменная, которая ссылается на какой-либо объект класса А, также может ссылаться на любой объект, описанный любым из подклассов класса А. В примере с транспортными средствами это означает то, что переменная типа Vehicle может ссылаться на любой из его подклассов, например, на Car:

<source lang="java"> Vehicle myVehicle = myCar; // or Vehicle otherVehicle = new Car(); </source>

Объект сам "знает", какого он типа. Информация о том, каким классом этот объект описан, сохраняется в памяти вместе с объектом. В коде можно проверить, является ли объект объектом заданного класса (или подкласса):

<source lang="java"> if (myVehicle instanceof Car) </source>

Код выше проверяет, является ли объект, на который ссылается переменная myVehicle, объектом класса Car.

Если переменную типа базового класса мы могли проинициализировать объектом одного из кодклассов этого класса (как в примере выше), то в обратную сторону это правило не работает:

<source lang="java"> Vehicle myVehicle = ....;

Car myCar = myVehicle; // illegal! </source>

Тоесть переменной, которая хранит объекты типа подкласса какого-либо класса, нельзя присвоить ссылку на объект суперкласса. Это связано с тем, что одному объекту суперкласса могут соответствовать несколько объектов его подклассов, но одному отдельно взятому объекту подкласса может соответствовать лишь один объект суперкласса. Поэтому нельзя преобразовать переменную myVehicle к типу данных Car, поскольку эта переменная, возможно, является объектом класса Motorcycle.

Преобразование типов

Мы уже пользовались преобразованием примитивных типов данных: int a = (int)Math.round(...); или double b = (double) 5 / 3;. Схожим образом можно преобразовывать типы данных объектов.

Например, если точно известно, что объект, на который ссылается переменная myVehile - объект класса Car, то можно осуществить следующее преобразование: <source lang="java"> Car c = (Car) myVehicle; </source>

c ссылается на ту же самую область памяти, что и myVehicle. Также, используя преобразование типов данных мы у объекта класса Vehicle (если мы определенно точно уверены в том, что это объект класса Car) можем обратиться к одному из методов класса Car:

<source lang="java"> System.out.println(((Car) myVehicle).numberOfDoors); </source>

Продолжение примера транспортных средств

<source lang="java"> Vehicle myVehicle = null; System.out.println("Vehicle Data:"); System.out.println("Registration number: " + myVehicle.registrationNumber);

if (myVehicle instanceof Car) { System.out.println("Type of vehicle: Car"); Car c = (Car) myVehicle; System.out.println("Number of doors: " + c.numberOfDoors); } else if (myVehicle instanceof Truck) { System.out.println("Type of vehicle: Truck"); Truck t = (Truck) myVehicle; System.out.println("Number of axels: " + t.numberOfAxels); } else if (myVehicle instanceof Motorcycle) { System.out.println("Type of vehicle: Motorcycle"); Motorcycle m = (Motorcycle) myVehicle; System.out.println("Has a sidecar: " + m.hasSidecar); } </source>


Полиморфизм

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

Рассмотрим пример фигур. Предположим, что у нас есть программа, которая рисует фигуры на экране. Программа умеет рисовать прямоугольники (Rectangle), овалы (Oval), а также прямоугольники со сглаженными краями (RoundRect).

Java-juhend-Various-shapes.png

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

<source lang="java"> class Shape {

Color color; // (must be imported from package java.awt)

void setColor(Color newColor) { // Method to change the color of the shape. color = newColor; // change value of instance variable redraw(); // redraw shape, which will appear in new color }

void redraw() { // method for drawing the shape

// <--- what commands should go here?

}

// more instance variables and methods

} // end of class Shape </source>

При реализации метода redraw() возникает проблема, поскольку различные фигуры перерисовывают себя по-разному. Таким образом в классе Shape мы не можем описать метод, который умел бы перерисовывать любую фигуру. С методом setColor() все гораздо проще - он одинаков у всех фигур (вне зависимости от того, имеем мы дело с прямоугольником или с овалом). Но что касается перерисовки - тут каждая фигура лучше всего сама знает, как себя следует правильно перерисовать. Тоесть перерисовка прямоугольника должна быть описана в классе прямогольника, и т.д. Таким образом, во всех подклассах должен присутствовать метод redraw(), где проиходит перерисовка соответствующей фигуры.

Например: <source lang="java"> class Rectangle extends Shape {

 void redraw() {
   // commands for drawing a rectangle
 }

} </source>

Если в программе присутствует переменная myShape типа Shape, то такая переменная может ссылаться на фигуру любого типа (на объект любого подкласса): Rectangle, Oval, или RoundRect.

Если в коде присутствует вызов

<source lang="java"> myShape.redraw(); </source>

то этот метод вызывается того объекта, на который ссылается переменная myShape. В коде это не всегда понятно - только при запуске программы можно понять, на объект какого класса ссылается myShape.

Пусть в программе есть список фигур, и в этом списке фигуры разных типов.


<source lang="java"> for (Shape s : shapes) {

   s.redraw();

} </source>

Пример кода выше демонстрирует случай, когда shapes это объект, например, класса ArrayList. С каждой последующей итерации цикла мы берем следующий элемент из списка, и у этого элемента вызываем метод redraw(). По коду можно сделать вывод о том, что мы всегда вызываем метод redraw() у объекта класса Shape (поскольку переменная s является переменной типа Shape). На самом деле метод redraw() вызывается в том классе, которому принадлежит соответствующая фигура.

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

Добавление новой фигуры

Если бы мы хотели к имеющимуся разнообразию фигур добавить возможность рисовать треугольники - достаточно добавить класс Triangle, который наследует класс Shape.

<source lang="java"> public class Triangle extends Shape {

 public void redraw() {
   // how to draw triangle
 }

} </source>

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

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

Перегрузка (overloading) методов могла бы быть уже вам знакома. Java позволяет в одном классе использовать несколько методов с одинаковыми именами. Несмотря на то, что у методов одно и то же имя, все они являются разными методами с точки зрения класса. Методы являются идентичными, если у них идентичны:

  • имя
  • количество аргументов
  • типы аргументов в соответствующих позициях

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

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

Пример:

<source lang="java"> public class OverloadingExample {

public void print() {}

public void print(String s) {}

public void print(int s) {}

public static void main(String[] args) {

print(); print("tere"); print(13); }

} </source>

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

Переопределение (overriding) методов - определение в подклассе метода с сигнатурой, идентичной сигнатуре метода, определенного в базовом классе (суперклассе). При запуске такого метода запускается метод в подклассе.

В примере, на котором был продемонстрирован полиморфизм, подклассы фигур переопределяли метод redraw() базового класса Shape. Для обозначения того, что метод переопределен, советуют обозначать такие методы аннотацией @Override:

Shape.java: <source lang="java"> public class Shape {

 public void redraw() {
   // some general stuff
 }

} </source>

Rectangle.java: <source lang="java"> public class Rectangle extends Shape {

 @Override
 public void redraw() {
   // rectangle specific stuff
 }

} </source>

Main.java: <source lang="java">

public class Main {

 public static void main(String[] args) {
   Shape s = new Rectangle();
   s.redraw(); // redraw() from Rectangle is called
 }

} </source>

this, super

Рассмотрим метод setColor класса Shape:

<source lang="java">

 void setColor(Color newColor) {
   color = newColor; // change the value of instance variable
   redraw(); // redraw the shape with new color
 }

</source>

В этом методе вызывается метод redraw();, но у объекта какого класса? Метод redraw(); вызывается у того же самого объекта, у которого был вызван метод setColor. Если это прямоугольник, то метод redraw(); вызывается у класса Rectangle. Вызов метода redraw() не приводит к вызову метода redraw() в классе Shape, если этод метод переопределен в каком-либо подклассе класса Shape.

В Java класс может ссылаться сам на себя, а также на базовый класс, который он наследовал. Для этого используют две переменные: this и super.

this

Переменная this в классе ссылается на этот же самый объект или класс. Одно из возможных применений этой конструкции состоит в том, что нужно передать себя (объект целиком) какому-либо методу. Например, в примере с фигурами, пусть фигуры можно делать активными. Метод, которым можно активировать определенную фигуру, является статическим и определен в классе Shapes. Вызов этого метода мог бы выглядеть следующим образом:

<source lang="java"> public class Rectangle extends Shape {

 public void click() {
   Shapes.activate(this);
 }

} </source>

Этот вызов передает себя (весь объект класса Rectangle) целиком методу Shapes.activate, который может сделать что-то полезное с ним.

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

<source lang="java"> void setColor(Color color) {

 this.color = color; // change the value of instance variable
 redraw(); // redraw the shape usingnew color

} </source>

В методе setColor присутствуют две переменные с именем color: одна из них является аргументом метода setColor, другая является полем класса Shape. В этом случае аргумент "затеняет" поле класса - что бы мы ни делали, при любом обращении к переменной color, мы будем обращаться к аргументу функции, а не к полю класса. Для того, чтобы явно указать на то, что мы хотим обратиться имено к полю класса с именем color, а не к аргументу функции, перед именем переменной следует использовать конструкицю this: this.color.

super

super ссылается на базовый класс. super.x ссылается на поле х базового класса. Эта конструкция полезна тогда, если в подклассе присутсвует поле с тем же именем, что и в базовом классе. В этом случае у объекта две переменные с одинаковым именем: одна в наследуемом классе, другая в базовом классе. Переменная в наследуемом классе не замещает переменную, определенную с тем же именем в базовом классе, но "затеняет" ее. Для того, чтобы обратиться к "скрытой" переменной базового класса, следует воспользоваться конструкцией super: super.var.

Другое применение этой конструкции заключается к обращению к базовому классу в переопределенных методах классов, которые наследуют этот базовый класс. Предположим, что у нас есть класс Rectangle, который умеет рисовать прямоугольник сплошной линией, и мы хотим добавить класс, который рисовал бы этот же самый прямоугольник пунктирной линией. Предположим, что метод redraw() в классе Rectangle рисует прямоугольник линиями, стиль которых задан в каком-либо отдельном поле класса, и испольузет сплошную линию как стиль рисования по умолчанию. Тогда нарисовать прямогольник пунктиром можно было бы следующим образом:

<source lang="java"> public class DashedRectangle extends Rectangle {

 public void redraw() {
   // change the line style to dashed
   super.redraw();
 }

} </source>

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

<source lang="java"> import java.awt.Color;

public class Triangle extends Shape { @Override void setColor(Color newColor) { if (Color.RED.equals(newColor) || Color.BLUE.equals(newColor)) { // allow only red and blue super.setColor(newColor); } } } </source>