Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Скачиваний:
41
Добавлен:
12.05.2015
Размер:
624.64 Кб
Скачать

Класс Stack

Хотя класс box полезен для иллюстрации существенных элементов класса, он имеет небольшое практическое значение. Чтобы показать действительную мощь классов, данную главу закончим более сложным примером. Как вы помните из обсуждения объектно-ориентированного программирования (OOP), представленного в главе 2, одним из наиболее важных его преиму­ществ является инкапсуляция данных и кода, который манипулирует этими данными. Механизмом, с помощью которого достигается инкапсуляция, яв­ляется класс. Создавая класс, вы организуете новый тип данных, который определяет как характер данных, так и подпрограммы, используемые для манипулирования этими данными. Непротиворечивый и управляемый ин­терфейс с данными класса определяют методы. Таким образом, вы можете использовать класс через его методы, не беспокоясь о деталях его реализа­ции или о том, как данные фактически управляются внутри класса. В неко­тором смысле, класс подобен "машине данных". Чтобы использовать маши­ну через ее органы управления, никаких знаний о том, что происходит внутри машины, не требуется. Фактически, поскольку подробности скрыты, ее внутренняя работа может быть изменена так, как это необходимо. Пока ваш код использует класс через его методы, внутренние подробности могут изменяться, не вызывая побочных эффектов вне класса.

Чтобы получить практическое приложение предшествующего обсуждения, давайте разработаем один из типичных примеров инкапсуляции – стек. Стек хранит данные, используя очередь типа LIFO ("Last-In, First-Out") – последним вошел, первым вышел. То есть стек подобен стопке тарелок на столе – последняя тарелка, поставленная на стопку, снимается со стопки первой. Стеки управляются через две операции, традиционно называемые push (поместить) и pop (извлечь, вытолкнуть). Чтобы поместить элемент в вершину стека, нужно использовать операцию push. Чтобы извлечь элемент из стека, нужно использовать операцию pop. Заметим, что инкапсуляция полного механизма стека – довольно простая задача.

В следующей программе класс с именем stack реализует стек целых чисел:

// Этот класс определяет целый стек для хранения 10 значений.

class Stack {

int stck[] = new int[10];

int tos;

// инициализировать вершину стека Stack () {

tos = -1; }

// поместить элемент в стек

void push(int item) {

if (tos=9)

System.out.println("Стек заполнен.");

else

stck[++tos] = item;

}

// Извлечь элемент из стека

int pop() {

if tos < 0) {

System.out.println("Стек пуст.");

return 0;

}

else

return stck[tos–];

}

}

Нетрудно видеть, что класс stack определяет два элемента данных и три ме­тода. Стек целых чисел содержится в массиве stck. Этот массив индексиро­ван переменной tos, которая всегда содержит индекс вершины стека. Кон­структор stack() инициализирует tos значением – 1, которое указывает, что стек пуст. Метод push () помещает элемент в стек. Чтобы извлечь элемент, вызовите метод pop(). Так как доступ к стеку выполняется через push() и pop о, тот факт, что стек содержится в массиве, не мешает использованию стека. Например, стек мог бы храниться в более сложной структуре данных, скажем, типа связного списка, а интерфейс, определенный методами push() и pop(), остался бы тем же самым.

Показанный ниже класс Teststack, демонстрирует работу с классом stack. Он создает два целочисленных стека, помещает некоторые значения в каж­дый и затем выталкивает их.

class TestStack {

public static void main(String args[]) {

Stack mystackl = new Stack ();

Stack mystack2 = new Stack ();

// поместить несколько чисел в стек

for(int i=0; i<10; i++)

mystackl. push (i);

for(int i=10; i<20; i++)

mystack2.push (i);

// вытолкнуть эти числа из стека

System.out.println("Стек в mystackl:"};

for(int 1=0; i<10; i++)

System.out.println(mystackl.pop());

System.out.println("Стек в mystack2:"),

for(int 1=0; i<10; i++)

System.out.println(mystack2.pop());

}

}

Эта программа генерирует следующий вывод:

Стек в mystack1:

9

8

7

6 . - .

5

4

3

2

1

о

Стек в mystack2:

19

18

17

16

15

14

13

12

11

10

Нетрудно заметить, что содержимое каждого стека различно.

Наконец, последнее замечание относительно класса stack: в данной реали­зации возможно изменение массива stck, который содержит стек, кодом, находящимся вне класса stack, оставляя его открытым для неправильного использования или повреждений. В следующей главе вы увидите, как ис­править эту ситуацию.

Методы и классы

Эта глава продолжает обсуждение методов и классов, начатое в предшест­вующей главе. В ней рассматривается несколько тем, касающихся методов, включая перегрузку, передачу параметров и рекурсию. Затем изложение воз­вращается к классам, обсуждая управление доступом, использование клю­чевого слова static и один из наиболее важных встроенных Java-классов String.

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

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

Чтобы определить при вызове, какую версию перегруженного метода в дей­ствительности вызывать, Java руководствуется типом и/или числом его па­раметров. Таким образом, перегруженные методы должны отличаться по типу и/или числу их параметров. Хотя такие методы могут иметь различные типы возвращаемого значения, однако одного его недостаточно, чтобы раз­личить две версии метода. Когда Java сталкивается с вызовом перегружен­ного метода, он просто выполняет его (метод) версию, чьи параметры соот­ветствуют параметрам, используемым в вызове.

Простой пример, который иллюстрирует перегруженный метод:

// Демонстрация перегруженного метода,

class OverloadDemo {

void test() {

System.out.println("Параметры отсутствуют");

}

// Перегруженный метод test с одним int-параметром.

void test(int a) j

System.out.println("a: " + a);

}

// Перегруженный метод test с двумя int-параметрами.

void test(int a, int b) {

System.out.println("а и Ь: " + a + " " + b);

}

// Перегруженный метод test с double-параметром,

double test(double a) {

System.out.println("Вещественное двойной точности а: " + a),

return a*a;

}

}

class Overload {

public static void main(String arcfs[]) {

OverloadDemo ob = new OverloadDemo();

double result;

// вызвать все версии

testf) ob.test();

ob.test(10);

ob.test(10, 20);

result = ob.test(123.2);

System.out.println("Результат ob.test(123.2): " + result)

}

}

Эта программа генерирует следующий вывод:

Параметры отсутствуют

а: 10

а и Ь: 10 20

Вещественное двойной точности а: 123.2

Результат ob.test(123.2): 15178.24

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

Когда вызывается перегруженный метод, Java ищет соответствие между ар­гументами вызова метода и его параметрами. Однако это соответствие не всегда может быть точным. В некоторых случаях определенную роль в вы­боре перегруженного метода могут сыграть автоматические преобразования типов Java. Например, рассмотрим следующую программу:

// Автоматическое преобразование типов в применении к перегрузке,

class OverloadDemo {

void test() {

System.out.println("Параметры отсутствуют");

}

// Перегруженный test с двумя int-параметрами.

void test(int a, int b) {

System.out.println("а и b: " + a + " " + b);

}

// Перегруженный test с double-параметром и возвращаемым типом,

void test(double a) {

System.out.println("Внутри test(double) a: " + a);

}

}

class Overload {

public static void main(String args[]) {

OverloadDemo ob = new OverloadDemo ();

int i = 88;

ob.testO;

ob.test(10, 20);

ob.test(i); // здесь будет вызван test(double)

ob.test(123.2); // здесь будет вызван test(double)

}

}

Эта программа генерирует следующий вывод:

Параметры отсутствуют

а и Ь: 10 20

Внутри test (double) a: 88

Внутри test (double) a: 123.2

Эта версия overioadDemo не определяет test(int) с одним целым парамет­ром. Поэтому, когда test() вызывается с целым аргументом внутри класса overload, никакого согласованного метода не находится. Однако Java может автоматически преобразовывать int в double, и это преобразование можно использовать для разрешения вызова. Поэтому, после того, как test(int) не находится, Java расширяет i до double и затем вызывает test (double). Ко­нечно, если бы test(int) был определен, то он вызывался бы вместо test (double). Java использует эти автоматические преобразования типов только тогда, когда никакого точного соответствия не находится.

Перегрузка методов поддерживает полиморфизм, потому что это один из способов, с помощью которых Java реализует парадигму "один интерфейс, множество методов". Чтобы понять, как это делается, приведем следующие рассуждения. На языках, которые не поддерживают перегрузку методов, каждому методу необходимо давать уникальное имя. Однако часто нужно реализовать, по существу, один и тот же метод для различных типов данных. Рассмотрим функцию абсолютного значения. На языках, которые не под­держивают перегрузку, существует обычно три или более версий этой функ­ции, каждая со слегка отличающимся именем. Например, в С, функция abs() возвращает абсолютное значение целого числа, labs() возвращает аб­солютное значение длинного целого числа, a fabs() – абсолютное значение числа с плавающей точкой. Так как С не поддерживает перегрузку, каждая функция должна иметь свое собственное имя, даже при том, что все три функции выполняют, по существу, одно и то же. Это делает ситуацию более сложной, чем она фактически есть на самом деле. Хотя основная концепция каждой функции одна и та же, вам все еще нужно помнить три разных име­ни. Подобная ситуация отсутствует в Java, потому что метод получения аб­солютного значения един для всех типов данных. Действительно, библиоте­ка стандартных классов Java включает метод абсолютного значения, с име­нем abs(). Этот метод перегружен в Math-классе Java, чтобы обрабатывать все числовые типы. Java определяет, какую версию abs() вызывать, основы­ваясь на типе аргумента. Значение перегрузки заключается в том, что она позволяет осуществлять доступ к связанным методам при помощи общего имени. Таким образом, имя abs представляет общее выполняемое действие. Право же выбирать правильную специфическую версию для конкретного обстоятельства предоставлено компилятору. Вы же, как программист, долж­ны только помнить общую выполняемую операцию. При использовании полиморфизма несколько имен были сокращены до одного. Хотя этот при­мер довольно прост, но если расширить концепцию, то можно увидеть, как перегрузка может помочь вам управлять большей сложностью.

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

чтобы перезагрузить несвязанные методы, но этого делать не нужно. На­пример, можно использовать имя sqr, чтобы создать методы, возвращающие квадрат целого числа и квадратный корень числа с плавающей точкой. Но эти две операции совершенно различны. Такой способ применения пере­грузки метода противоречит его первоначальной цели. Практически, следует перегружать только тесно связанные операции.

Соседние файлы в папке JavaLit