Else
stck[++tos] = item;
}
int pop() {
if (tos < 0){
System. out. println("Стек пуст");
return 0;
}
Else
return stck[tos--];
}
}
public class TestStack {
public static void main(String[] args) {
Stack mystack1 = new Stack(5);
Stack mystack2 = new Stack(8);
for (int i=0; i<5; i++) mystack1.push(i);
for (int i=0; i<8; i++) mystack2.push(i);
System. out. println("Стек в mystack1:");
for (int i=0; i<5; i++)
System. out. println(mystack1.pop());
System. out. println("Стек в mystack2:");
for (int i=0; i<8; i++)
System. out. println(mystack2.pop());
}
}
Данные или переменные, определенные в классе, называются переменными экземпляра или экземплярными переменными. В данном примере это - stck[] и tos. Код содержится внутри методов (push, pop). Все вместе, методы и переменные, определенные внутри класса, называются членами класса. Каждый экземпляр класса (т. е. каждый объект класса - mystack1, mystack2) содержит свою собственную копию этих переменных. Таким образом, данные одного объекта отделены от данных другого.
В тексте данной программы содержится 2 класса: Stack и TestStack. Как в этом случае должен называться файл в котором данная программа храниться? Ответ: TestStack.java, т.к. класс TestStack содержит метод main() и имеет объявление public. В исходном файле может быть только один класс объявленный как public и любое кол-во классов без этого объявления.
Конструкторы
Конструктор (Stack)инициализирует объект после его создания. Он имеет такое же имя, как класс, в котором он постоянно находится. Если конструктор определен, то он автоматически вызывается сразу же после того, как объект создается, и прежде, чем завершается выполнение операции new. Конструкторы не имеют ни спецификатора возвращаемого типа, ни спецификатора void. Неявным возвращаемым типом конструктора класса является тип самого класса. Работа конструктора заключается в том, чтобы инициализировать внутреннее состояние объекта.
Спецификаторы доступа
Инкапсуляция связывает данные с кодом, который манипулирует ими. Однако инкапсуляция обеспечивает другой важный атрибут: управление доступом. Через инкапсуляцию можно управлять доступом различных частей программы к членам класса и предотвращать неправильное использование таких членов. Например, разрешая доступ к данным только через хорошо определенный набор методов, есть возможность предотвращения неверного использования этих данных. Таким образом, при правильной реализации класс создает для использования "черный ящик", внутренняя работа которого не доступна для вмешательства.
Спецификаторы доступа Java: public (общий), private (частный) и protected (защищенный).
Начнем с определения спецификаторов public и private. Когда элемент класса модифицирован спецификатором public, то к этому элементу возможен доступ из любой точки программы. Если член класса определен как private, к нему могут обращаться только члены этого класса. К элементам без спецификатора доступа можно получить доступ из этого же пакета (Понятие «пакет» будет рассмотрено позднее). Спецификатор protected применяется только при использовании наследования и будет рассмотрено позднее.
/* Эта программа демонстрирует различие между
методами доступа public и private.
*/
class Test {
int a; // доступ по умолчанию (public)
public int b; // общий (public) доступ
private int с; // частный (private) доступ
// методы для доступа к переменной с
void setc(int i) { // установить значение с
с = i;
}
int getc() { // получить значение с
return с;
}
}
class AccessTest {
public static void main(String args[]) {
Test ob = new Test();
ob.a = 10; ob.b = 20; //к а и b возможен прямой доступ.
// ob.c = 100; // Ошибка!
// к переменной c возможен доступ только через ее методы.
ob.setc(100);
System. out. println("a,b и с:"+ob.a+" "+ob.b+" "+ob.getc());
}
}
В данном примере к переменным a и b возможен прямой доступ, что нежелательно. Лучше использовать методы доступа к экземплярным переменным. Именно такие методы должны определять интерфейс с большинством классов. Они позволяют разработчику класса скрывать специфику внутреннего размещения внутренних структур данных за более ясными абстракциями методов.
Важно понимать, что происходит при выполнении оператора присваивания, в котором участвуют объекты.
class Test2class {
int a,b;
public static void main(String args[]) {
Test2class ot_1 = new Test2class();
ot_1.a = 11;
ot_1.b = 22;
Test2class ot_2 = new Test2class();
ot_2 = ot_1;
// Теперь объект ot2 ссылается на ту же область памяти, что и объект ot1
System. out. println(ot_2.a);
ot_1.b = 33;
System. out. println(ot_2.b);
}
}
Когда вы назначаете одну ссылочную переменную объекта другой (ссылочной переменной объекта), вы не создаете копии объекта, а делаете только копию ссылки.
Назначение ot_1 переменной ot_2 не распределяет никакой памяти и не копирует какую-либо часть первоначального объекта. Эта операция просто помещает в ot_2 ссылку из ot_1. Таким образом, любые изменения, сделанные в объекте через ot_2 затронут объект, на который ссылается ot_l, т. к. это один и тот же объект.
Ключевое слово this
Иногда у метода возникает необходимость обращаться к объекту, который его вызвал. Для этого Java определяет ключевое слово this. Его можно использовать внутри любого метода, чтобы сослаться на текущий объект. То есть this — это всегда ссылка на объект, метод которого был вызван. Вы можете использовать this везде, где разрешается ссылка на объект текущего класса.
Перегрузка методов
В Java в пределах одного класса можно определить два или более методов, которые совместно используют одно и то же имя, но имеют разное количество параметров. Когда это имеет место, методы называют перегруженными, а о процессе говорят как о перегрузке метода. Перегрузка методов — один из способов, с помощью которого реализуется полиморфизм. Когда происходит вызов перегруженного метода, выполняется тот метод, чьи параметры соответствуют параметрам, используемым в вызове. В некоторых случаях определенную роль в выборе перегруженного метода могут сыграть автоматические преобразования типов Java (например, int и double). Java использует эти автоматические преобразования типов только тогда, когда никакого точного соответствия не находится. Рассмотрим пример.
class Stack {
private int stck[];
private double stckd[];
private int tos;
private boolean fl; // true if int false if double
Stack(int size) {
stck = new int [size];
stckd = new double [size];
tos = -1;
}
void push(int item) {
fl = true;
if (tos == stck.length-1)
System. out. println("Стек заполнен");
else {
stck[++tos] = item;
System. out. print("i");}
}
void push(double item) {
fl = false;
if (tos == stck.length-1)
System. out. println("Стек заполнен");
else {
stckd[++tos] = item;
System. out. print("d");}
}
double pop() {
if (tos < 0){
System. out. println("Стек пуст");
return 0;
}
else {
if (fl)
return stck[tos--];
Else
return stckd[tos--];
}
}
}
public class TestStack {
public static void main(String[] args) {
Stack mystack1 = new Stack(5);
Stack mystack2 = new Stack(8);
for (int i=0; i<5; i++) mystack1.push(i);
for (int i=0; i<8; i++) mystack2.push(i+1.1);
System. out. println("Стек в mystack1:");
for (int i=0; i<5; i++)
System. out. println(mystack1.pop());
System. out. println("Стек в mystack2:");
for (int i=0; i<8; i++)
System. out. println(mystack2.pop());
}
}
При заполнении стека mystack1 в качестве параметра при вызове метода push передается целое значение, что приводит к вызову перегруженного метода
void push(int item)
и использованию целочисленного массива stck.
При заполнении стека mystack2 в качестве параметра при вызове метода push передается дробное значение, что приводит к вызову перегруженного метода
void push(double item)
и использованию массива stckd.
Рассмотрим еще один пример.
/* Класс Box: три конструктора для разных способов
инициализации размеров блока. */
class Box {
double width;
double height;
double depth;
// конструктор для инициализации всех размеров
Box(double w, double h, double d) {
width = w;
height = h;
depth = d;
}
// конструктор для инициализации без указания размеров
Box () {
width = -1; // использовать —1 для указания
height = -1; // не инициализированного
depth = -1; // блока
}
// конструктор для создания куба
Box(double len) {
width = height = depth = len;
}
// вычислить и возвратить объем
double volume() {
return width * height * depth;
}
}
class OverloadCons {
public static void main(String args[]) {
// создать блоки, используя различные конструкторы
Box myboxl = new Box(10, 20, 15);
Box mybox2 = new Box();
Box mycube = new Box(7);
double vol;
// получить объем первого блока
vol = myboxl.volume();
System. out. println("Объем myboxl равен " + vol);
// получить объем второго блока
vol = mybox2.volume();
System. out. println("Объем mybox2 равен " + vol);
// получить объем куба
vol = mycube.volume();
System. out. println("Объем mycube равен " + vol); }
}
Подходящий перегруженный конструктор вызывается, основываясь на параметрах, указанных при выполнении операции new.
Скрытие переменной экземпляра
Как известно, в Java недопустимо объявление двух локальных переменных с одним и тем же именем внутри той же самой, или включающей области действия идентификаторов. Заметим, что вы можете иметь локальные переменные, включая формальные параметры для методов, которые перекрываются с именами экземплярных переменных класса. Однако, когда локальная переменная имеет такое же имя, как переменная экземпляра, локальная переменная скрывает переменную экземпляра. Вот почему width, height и depth не использовались как имена параметров конструктора Box() внутри класса Box. Если бы они были использованы для именования этих параметров, то, скажем width, как формальный параметр, скрыл бы переменную экземпляра width. Хотя обычно проще указывать различные имена, существует другой способ обойти эту ситуацию. Поскольку this позволяет обращаться прямо к объекту, это можно применять для разрешения любых конфликтов пространства имен, которые могли бы происходить между экземплярными и локальными переменными. Ниже представлена другая версия Box(), которая использует width, height и depth для имен параметров и затем применяет this, чтобы получить доступ к переменным экземпляра с теми же самыми именами:
// Используйте этот вариант конструктора
// для разрешения конфликтов пространства имен.
Box(double width, double height, double depth) {
this. width = width;
this. height = height;
this. depth = depth;
}
Использование this в указанном контексте иногда может привести к путанице, так что некоторые программисты предпочитают не применять имена локальных переменных и формальных параметров, которые совпадают с переменными экземпляра. Конечно, другие программисты, наоборот, верят, что это хорошее соглашение — использовать одинаковые имена — для ясности, и this — чтобы преодолеть скрытие переменной экземпляра. Какой подход принимаете вы — это вопрос вкуса.
Передача аргументов
Существуют два способа, с помощью которых машинный язык может передавать аргумент подпрограмме. Первый способ — передача аргумента по значению. Этот метод копирует значение аргумента в формальный параметр подпрограммы. Поэтому любые изменения этого параметра подпрограммой не имеют никакого влияния на соответствующий аргумент вызова. Второй способ — передача аргумента по ссылке. В этом методе ссылка на параметр (а не его значение) передается параметру. Внутри подпрограммы она используется, чтобы обратиться к фактическому параметру, указанному в вызове. Это означает, что изменения параметра будут влиять на аргумент, использованный для вызова подпрограммы. Java использует оба метода, в зависимости от того, что передается во время вызова подпрограмме.
Когда методу передается простой тип, он передается по значению. Таким образом, то, что происходит с параметром, который принимает аргумент, никак не влияет на сам аргумент (т. е. аргумент при этом не изменяется). Например, рассмотрим следующую программу:
// Простые типы передаются по значению
class Test {
void meth(int i, int j) {
i *= 2; j /= 2;
}
}
class CallByValue {
public static void main(String args[]) {
Test ob = new Test();
int a = 15, b = 20;
System. out. println("а и b перед вызовом: "+a+" "+b);
ob.meth(a, b);
System. out. println("а и b после вызова: "+a+" "+b);
}
}
Когда вы передаете методу объект, ситуация изменяется, потому что объекты передаются по ссылке. При создании переменной типа "класс", создается только ссылка на объект. Таким образом, когда вы передаете эту ссылку методу, принимающий ее параметр будет ссылаться на тот же самый объект, что и аргумент. Это и означает, что объекты передаются методам по ссылке. Все изменения объекта внутри метода затрагивают объект, используемый как аргумент. Например, рассмотрим следующую программу:
// Объекты передаются по ссылке.
class Test{
int a, b;
Test(int i, int j) {
a = i;
b = j;
}
// передать объект
void meth(Test o) {
o.a *= 2;
o.b /= 2;
}
}
class CallByRef {
public static void main(String args []) {
Test ob = new Test(15, 20);
System. out. println("ob.а и ob.b перед вызовом: " + ob.a + " " + ob.b);
ob.meth(ob);
System. out. println("ob.a и ob.b после вызова: " + ob.a + " " + ob.b);
}
}
Интересно отметить, что при передаче объектной ссылки методу сама ссылка передается по значению. Однако, т. к. передаваемое значение ссылается на объект, копия этого значения будет ссылаться на тот же объект, что и соответствующий аргумент.
Статические элементы
Иногда возникает необходимость определить элемент (член) класса так, чтобы появилась возможность пользоваться им независимо от какого-либо объекта этого класса. Обычно к элементам класса нужно обращаться только через объект этого класса. Однако можно создать элемент для использования без ссылки на определенный объект. Чтобы это сделать, укажите в начале его объявления ключевое слово static. Когда элемент объявляется как static, к нему можно обращаться до того, как создаются какие-либо объекты его класса, и без ссылки на какой-либо объект. Статическими можно объявлять как методы, так и переменные. Наиболее общим примером static-элемента является метод main (). Он объявляется как static, потому что должен вызваться прежде, чем будут созданы какие-либо объекты.
Переменные экземпляра, объявленные как static, это, по существу, глобальные переменные. Когда создаются объекты их класса, никакой копии static -переменной не делается. Вместо этого, все экземпляры класса совместно используют (разделяют) одну и ту же static -переменную.
Методы, объявленные как static имеют несколько ограничений:
- могут вызывать только другие static-методы;
- должны обращаться только к static-данным;
- никогда не могут ссылаться на this или super. (Ключевое слово super касается наследования и описано в дальнейшем.)
Для организации вычислений с целью инициализировать статические переменные можно объявить статический блок, который выполняется только один раз, когда класс впервые загружается. Следующий пример UseStatic демонстрирует класс, который имеет статические методы, несколько переменных и блок инициализации:
class UseStatic {
static int a = 3;
static int b;
static void meth(int x) {
System. out. println("x= " + x);
System. out. println("a= " + a);
System. out. println("b= " + b);
}
static {
System. out. println("Статический блок инициализирован");
b = a * 4;
}
public static void main(String args[]) {
meth (42);
}
}
Bce static-инструкции выполняются сразу же после загрузки класса usestatic. Сначала, в а устанавливается 3, затем выполняется static -блок (печатая сообщение «Статический блок инициализирован.»). Затем вызывается метод main(), который обращается к meth(), передавая 42 параметру х. Три оператора println() обращаются к двум static-переменным (а и b) и к локальной переменной х.
Внутри статического метода недопустимо обращаться к любым экземплярным переменным.
Вне класса, в котором они определены, статические методы и переменные могут использоваться независимо от любого объекта. В этом случае для обращения к ним используются имена их класса и точечная операция. Например, если нужно вызвать static-метод вне его класса, можно воспользоваться следующей общей формой вызова:
Classname.methodname()
Например так можно вызвать методы класса Math, для вычисления различных математических функций: Math.sin(x), Math.cos(x), Math.sqrt(x), ….
Наследование
Наследование позволяет создавать иерархические классификации. Используя наследование, можно создать главный класс, который определяет свойства, общие для набора связанных элементов. Затем этот класс может быть унаследован другими, более специфическими классами, каждый из которых добавляет те свойства, которые являются уникальными для него. В терминологии Java класс, который унаследован, называется суперклассом (superclass). Класс, который выполняет наследование, называется подклассом (subclass). Поэтому подкласс — это специализированная версия суперкласса. Он наследует все переменные экземпляра и методы, определенные суперклассом, и прибавляет свои собственные уникальные элементы. Чтобы наследовать класс, нужно просто включить определение одного класса в другое, используя ключевое слово extends.
Рассмотрим пример.
// Программа использует наследование для расширения Box.
class Box {
double width;
double height;
double depth;
// конструктор, используемый, когда указаны все размеры
Box(double w, double h, double d) {
width = w;
height = h;
depth = d;
}
// конструктор, используемый, когда размеры не указаны
Box() {
width = -1; // использовать -1 для указания
height = -1; // неинициализированного
depth = -1; // блока
}
// конструктор, используемый для создания куба
Box(double len) {
width = height = depth = len;
}
// вычислить и вернуть объем
double volume() {
return width * height * depth;
}
}
// Box расширяется для включения веса.
class BoxWeight extends Box {
double weight; // вес блока
// конструктор для BoxWeight
BoxWeight(double w, double h, double d, double m)
{
width = w;
height = h;
depth = d;
weight = m;
}
}
class DemoBoxWeight {
public static void main(String args[]) {
BoxWeight myboxl = new BoxWeight(10, 20, 15, 34.3);
BoxWeight mybox2 = new BoxWeight(2, 3, 4, 0.076);
double vol;
vol = myboxl.volume();
System. out. println("Объем myboxl равен " + vol);
System. out. println("Beс myboxl равен " + myboxl.weight);
System. out. println();
vol = mybox2.volume();
System. out. println("Объем mybox2 равен " + vol);
System. out. println("Bec mybox2 равен " + mybox2.weight);
}
}
Главное преимущество наследования состоит в том, что, как только вы создали суперкласс, который определяет общие для набора объектов атрибуты, его можно использовать для создания любого числа более специфичных подклассов. Каждый подкласс может добавить свою собственную классификацию.
Помните, как только вы создали суперкласс, который определяет общие аспекты объекта, этот суперкласс может наследоваться для формирования специализированных классов. Каждый подкласс просто прибавляет свои собственные, уникальные атрибуты.
Хотя подкласс включает все элементы (члены) своего суперкласса, он не может обращаться к тем элементам суперкласса, которые были объявлены как private.
Рассмотрим пример.
/* В иерархии классов private-члены остаются private для ее классов.
Эта программа содержит ошибку и не будет компилироваться. */
// Создать суперкласс.
class А {
int i; // public по умолчанию
private int j; // private для A
void setij(int x, int y) {
i = x;
j = y;
}
}
// j класса А здесь не доступна.
class В extends А {
int total;
void sum () {
total = i + j; // ОШИБКА, j здесь не доступна
}
}
class Access {
public static void main(String args[]) {
В subOb = new В();
subOb.setij(10, 12);
subOb.sum();
System. out. println("Всего " + subOb.total);
}
}
В переменную суперкласса можно поместить ссылку на объект подкласса.
Рассмотрим пример.
class RefDemo {
public static void main(String args[]) {
BoxWeight weightbox = new BoxWeight(3, 5, 7, 8.37);
Box plainbox = new Box();
double vol;
vol = weightbox.volume();
System. out. println("Объем weightbox равен " + vol);
System. out. println("Beс weightbox равен"+ weightbox.weight);
System. out. println();
// назначить ссылку на BoxWeight ссылке на Box
plainbox = weightbox;
vol = plainbox.volume(); // OK, volume() определена в Box
System. out. println("Объем plainbox равен " + vol);
/* Следующее утверждение не верно, потому что plainbox не определяет член weight. */
//System.out.println("Вес plainbox равен"+plainbox.weight);
}
}
Ключевое слово super.
В случае, если подкласс должен обратиться к своему непосредственному суперклассу, он может сделать это при помощи ключевого слова super.
Ключевое слово super имеет две общие формы. Первая вызывает конструктор суперкласса. Вторая используется для доступа к элементу суперкласса, который был скрыт элементом подкласса.
//BoxWeight теперь использует super для инициализации Box-атрибутов.
class BoxWeight extends Box {
double weight; // вес блока
// инициализировать width, height и depth, используя super()
BoxWeight(double w, double h, double d, double m) {
super (w, h, d); // вызвать конструктор суперкласса
weight = m;
}
}
Рассмотрим реализацию класса BoxWeight с использованием вызова конструктора суперкласса.
// Полная реализация BoxWeight.
class Box {
private double width;
private double height;
private double depth;
// построить клон объекта
Box(Box ob) { // передать объект конструктору
width = ob.width;
height = ob.height;
depth = ob.depth;
}
// конструктор, используемый для всех размеров
Box(double w, double h, double d) {
width = w;
height = h;
depth = d;
}
// конструктор, используемый без размеров
Box() {
width = -1; // использовать -1 для указания
height = -1; // неинициализированного
depth = -1; // блока
}
// конструктор, используемый для создания куба
Box(double len) {
width = height = depth = len;
}
// вычислить и возвратить объем
double volume() {
return width * height * depth;
}
}
// BoxWeight теперь полностью реализует все конструкторы.
class BoxWeight extends Box {
double weight; // вес блока
// построить клон объекта
BoxWeight(BoxWeight ob) { // передать объект конструктору
super (ob);
weight = ob.weight;
}
// конструктор, используемый для всех размеров
BoxWeight(double w, double h, double d, double m) {
super (w, h, d); // вызвать конструктор суперкласса
weight = m;
}
// конструктор по умолчанию
BoxWeight() {
super ();
weight = -1;
}
// конструктор, используемый для создания куба
BoxWeight(double len, double m) {
super (len);
weight = m;
}
}
class DemoSuper {
public static void main(String args[]) {
BoxWeight myboxl = new BoxWeight(10, 20, 15, 34.3);
BoxWeight mybox2 = new BoxWeight(2, 3, 4, 0.076);
BoxWeight mybox3 = new BoxWeight(); //по умолчанию
BoxWeight mycube = new BoxWeight(3, 2);
BoxWeight myclone = new BoxWeight(myboxl);
double vol;
vol = myboxl.volume();
System. out. println("Объем myboxl равен " + vol);
System. out. println("Bec myboxl равен " + myboxl.weight);
System. out. println();
vol = mybox2.volume();
System. out. println("Объем mybox2 равен " + vol);
System. out. println("Bec mybox2 равен " + mybox2.weight);
System. out. println();
vol = mybox3.volume();
System. out. println("Объем mybox3 равен " + vol);
System. out. println("Вес mybox3 равен " + mybox3.weight);
System. out. println();
vol = myclone.volume();
System. out. println("Объем myclone равен " + vol);
System. out. println("Beс myclone равен " + myclone.weight);
System. out. println();
vol = mycube.volume();
System. out. println("Объем mycube равен " + vol);
System. out. println("Bec mycube равен " + mycube.weight);
System. out. println();
}
}
Вторая форма super действует в чем-то подобно ссылке this, за исключением того, что она всегда обращается к суперклассу подкласса, в котором используется. Общий формат такого использования super имеет вид:
Super.member
где member может быть либо методом, либо переменной экземпляра.
Вторая форма super больше всего применима к ситуациям, когда имена элементов (членов) подкласса скрывают элементы с тем же именем в суперклассе.
// Использование super для преодоления скрытия имен.
class А {
int i;
}
// Создание подкласса В расширением класса А.
class В extends А {
int i; // этот i скрывает i в А
В(int a, int b) {
super. i = a; // i из A
i = b; // i из В
}
void show() {
System. out. println("i из суперкласса: " + super. i);
System. out. println("i из подкласса: " + i);
}
}
class UseSuper {
public static void main(String args[]) {
В subOb = new В(1, 2);
subOb.show();
}
}
Порядок вызова конструкторов
Рассмотрим пример (проект ConstSeq файл CallingCons):
// Демонстрирует порядок вызова конструкторов.
//
// Создать суперкласс А.
class A {
A() {
System. out. println("Внутри А-конструктора.");
}
}
// Создать подкласс В расширением А.
class B extends A {
B() {
System. out. println("Внутри В-конструктора.");
}
}
// Создать другой класс (С), расширяющий В.
class C extends B {
C() {
System. out. println("Внутри С-конструктора.");
}
}
class CallingCons {
public static void main(String args[]) {
C c = new C();
}
}
Результатом работы этой программы будет:
Внутри А-конструктора.
Внутри В-конструктора.
Внутри С-конструктора.
При создании объекта подкласса происходит цепочка вызовов конструкторов классов данной иерархии. Конструкторы вызываются в порядке подчиненности классов.
Поскольку суперкласс не имеет никакого знания о каком-либо подклассе, любая инициализация, которую ему нужно выполнить, является отдельной и, по возможности, предшествующей любой инициализации, выполняемой подклассом. Поэтому-то она и должна выполняться первой.
Переопределение методов
В случае, если метод в подклассе имеет такое же имя, тип и список параметров, как метод в его суперклассе, то метод в подклассе переопределяет (override) метод в суперклассе. Когда переопределенный метод вызывается в подклассе, он будет всегда обращаться к версии этого метода, определенной подклассом. Версия метода, определенная суперклассом, будет скрыта.
Рассмотрим следующий фрагмент (проект Override файл Override.java):
// Переопределение методов.
class A {
int i, j;
A(int a, int b) {
i = a;
j = b;
}
// показать i и j на экране
void show() {
System. out. println("i и j: " + i + " " + j);
}
}
class В extends A {
int k;
В(int a, int b, int c) {
super (a, b);
k = c;
}
// Показать на экране k (этот show() переопределяет show() из A)
void show() {
System. out. println("k: " + k);
}
}
class Override {
public static void main(String args[]) {
В subOb = new В(1, 2, 3);
subOb.show(); // здесь вызывается show() из В
}
}
Переопределение метода происходит только тогда, когда имена, возвращаемые значения и параметры этих двух методов идентичны. Если это не так, то оба метода просто перегружены. Например, рассмотрим следующую модифицированную версию предыдущего примера (проект Override файл Override2.java):
// Методы с различными сигнатурами типов перегружаются,
// а не переопределяются.
class A {
int i, j;
A(int a, int b) {
i = a;
j = b;
}
// Показать i и j
void show() {
System.out.println("i и j: " + i + " " + j);
}
}
// Создать подкласс В расширением класса А.
class B extends A {
int k;
B(int a, int b, int c) {
super (a, b);
k = c;
}
// Перегруженный show()
void show(String msg) {
System. out. println(msg + k);
}
}
class Override2 {
public static void main(String args[]) {
B subOb = new B(1, 2, 3);
subOb.show("Это к: "); // вызов show() класса В
subOb.show(); // вызов show() класса A
}
}
Полиморфизм времени выполнения
Полиморфизм времени выполнения это механизм, с помощью которого решение на вызов переопределенной функции принимается во время выполнения, а не во время компиляции.
Поскольку ссылочная переменная суперкласса может обращаться к объекту подкласса, Java может принимать решения о вызове переопределенных методов во время выполнения. Когда переопределенный метод вызывается через ссылку суперкласса, Java определяет, какую версию этого метода следует выполнять, основываясь на типе объекта, на который указывает ссылка в момент вызова. Когда ссылка указывает на различные типы объектов, будут вызываться различные версии переопределенного метода. Именно тип объекта, на который сделана ссылка (а не тип ссылочной переменной) определяет, какая версия переопределенного метода будет выполнена.
Например (проект Dispatch файл Dispatch.java):
// Динамическая диспетчеризация методов.
class A {
void callme () {
System. out. println("Внутри А метод callme");
}
}
class B extends A {
// переопределить callme()
void callme() {
System. out. println("Внутри В метод callme");
}
}
class C extends A {
// переопределить callme()
void callme() {
System. out. println("Внутри С метод callme");
}
}
class Dispatch {
public static void main(String args[]) {
A a = new A(); // объект типа A
B b = new B(); // объект типа В
C с = new C(); // объект типа С
A r; // определить ссылку типа А
r = a; // r на А-объект
r.callme(); // вызывает А-версию callme
r = b; // r указывает на В-объект
r.callme(); // вызывает В-версию callme
r = с; // r указывает,на С-объект
r.callme(); // вызывает С-версию callme
}
}
Еще один пример (проект FindAreas файл FindAreas.java):
// Использование полиморфизма времени выполнения.
class Figure {
double diml;
double dim2;
Figure(double a, double b) {
diml = a;
dim2 = b;
}
double area() {
System. out. println("Площадь Figure не определена.");
return 0;
}
}
class Rectangle extends Figure {
Rectangle(double a, double b) {
super (a, b);
}
// переопределить area для прямоугольника
double area() {
System. out. println("Внутри Area для Rectangle.");
return diml * dim2;
}
}
class Triangle extends Figure {
Triangle(double a, double b) {
super (a, b);
}
// переопределить area для прямоугольного треугольника
double area() {
System. out. println("Внутри Area для Triangle.");
return diml * dim2 / 2;
}
}
class FindAreas {
public static void main(String args[]) {
Figure f = new Figure(10, 10);
Rectangle r = new Rectangle(9, 5);
Triangle t = new Triangle(10, 8);
Figure figref;
figref = r;
System. out. println("Площадь равна " + figref.area());
figref = t;
System. out. println("Площадь равна " + figref.area ());
figref = f;
System. out. println("Площадь равна " + figref.area ());
}
}
Результат работы:
Внутри Area для Rectangle.
Площадь равна 45.0
Внутри Area для Triangle.
Площадь равна 40.0
Площадь Figure не определена.
Площадь равна 0.0
Полиморфизм – это замечательная возможность сделать Ваши программы более короткими и изящными, хотя без этого можно обойтись (см. пример FindAreas2.java).