Текст книги "Java: руководство для начинающих (ЛП)"
Автор книги: Герберт Шилдт
Жанр:
Программирование
сообщить о нарушении
Текущая страница: 13 (всего у книги 36 страниц)
Метод ok () объявлен как закрытый главным образом для того, чтобы проиллюстрировать управление доступом. Даже если бы он и был открытым, это не представляло бы никакой опасности, поскольку он не видоизменяет объект. Но этот метод используется только членами класса FailSoftArray, поэтому он и объявлен закрытым.
Обратите внимание на то, что переменная экземпляра length открыта. Это согласуется с правилами реализации массивов в Java. Для того чтобы получить данные о длине массива типа FailSoftArray, достаточно прочитать значение переменной экземпляра length.
Для сохранения данных в массиве типа FailSoftArray по указанному индексу вызывается метод put (), тогда как метод get () извлекает содержимое элемента этого массива по заданному индексу. Если указанный индекс оказывается вне границ массива, метбд put () возвращает логическое значение false, а метод get () – значение errval. Ради простоты в большинстве примеров программ, представленных в этой книге, на члены класса будет в основном распространяться тип доступа по умолчанию. Но не следует забывать, что в реальных объектно-ориентированных программах очень важно ограничивать доступ к членам класса, и в особенности к переменным. Как будет показано в главе 7, при использовании наследования роль средств управления доступом еще более возрастает.
Пример для опробования 6.1. Усовершенствование класса Queue
Модификатор доступа private можно использовать для усовершенствования класса Queue, разработанного в примере для опробования 5.2 из главы 5. В текущей версии этого класса используется тип доступа по умолчанию, который, по существу, делает все члены этого класса открытыми. Это означает, что другие классы могут непосредственно обращаться к элементам базового массива – и даже вне очереди. А поскольку назначение класса, реализующего очередь, состоит в том, чтобы обеспечить принцип доступа “первым пришел – первым обслужен”, то возможность произвольного обращения к элементам массива явно неуместна. В частности, это давало бы возможность недобросовестным программистам изменять индексы в переменных putloc и getloc, искажая тем самым организацию очереди. Подобные недостатки нетрудно устранить с помощью модификатора доступа private.
Последовательность действий
Создайте новый файл Queue. j ava.
Добавьте к массиву q, а также к переменным putloc и getloc модификатор доступа private в классе Queue. В результате код этого класса должен выглядеть так, как показано ниже. // Усовершенствованный класс очереди, предназначенной // для хранения символьных значений, class Queue { // Следующие члены класса теперь являются закрытыми, private char q[]; // Массив для хранения элементов очереди private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди Queue(int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0; } // поместить символ в очередь void put(char ch) { if(putloc==q.length-1) { System.out.println(" – Queue is full."); return; } putloc++; q[putloc] = ch; } // извлечь символ из очереди char get () { if(getloc == putloc) { System.out.println(" – Queue is empty."); return (char) 0; } getloc++; return q[getloc]; } }
Изменение типа доступа к массиву q и переменным putloc и getloc с выбираемого по умолчанию на закрытый (private) никак не скажется на работе тех программ, где класс Queue используется правильно. В частности, этот класс будет по-прежнему взаимодействовать с классом QDemo, созданным в примере для опробования 5.2. В то же время неправильное обращение к классу Queue станет невозможным. Например, следующий фрагмент кода недопустим:Queue test = new Queue(lO); test.q[0] =99; // Ошибка! test.putloc = -100; // He пройдет!
Теперь, когда массив q и переменные putloc и getloc стали закрытыми, класс Queue строго следует принципу “первым пришел – первым обслужен”, по которому действует очередь. Передача объектов методам
В приведенных ранее примерах программ в качестве параметров методам передавались лишь простые типы. Но параметрами могут быть и объекты. Например, в привеское значение true только в том случае, если все три размера обоих параллелепипедов совпадают. А в методе same Volume () сравниваются лишь объемы двух параллелепипедов. Но в обоих случаях параметр ob имеет тип Block. Несмотря на то что Block – это класс, параметры данного типа используются таким же образом, как и параметры встроенных в Java типов данных. Способы передачи аргументов методу
Как показывает приведенный выше пример, передача объекта методу производится очень просто. Но в этом примере показаны не все нюансы данного процесса. В некоторых случаях последствия передачи объекта по ссылке будут отличаться от тех результатов, к которым приводит передача значения обычного типа. Для выяснения причин этих отличий рассмотрим два способа передачи аргументов методу.
Первым способом является вызов по значению. В этом случае значение аргумента копируется в формальный параметр метода. Следовательно, изменения, вносимые в параметр метода, не оказывают никакого влияния на аргумент, используемый для вызова. А вторым способом передачи аргумента является вызов по ссылке. В данном случае параметру метода передается ссылка на аргумент, а не значение аргумента. В методе эта ссылка используется для доступа к конкретному аргументу, указываемому при вызове. Это означает, что изменения, вносимые в параметр, будут оказывать влияние на аргу¬ мент, используемый для вызова метода. Как будет показано далее, в Java используются оба способа. А выбор конкретного способа зависит от того, что именно передается.
Если методу передается простой тип, например int или double, он передается по значению. При этом создается копия аргумента, а то, что происходит с параметром, принимающим аргумент, не распространяется за пределы метода. Рассмотрим в качестве примера следующую программу: // Простые типы данных передаются методам по значению, class Test { /* Этот метод не может изменить значения аргументов, передаваемых ему при вызове. */ void noChange(int i, int j) { i = i + j; j = -j; } } class CallByValue { public static void main (String args.[]) { Test ob = new Test(); int a = 15, b = 20; System.out.println("a and b before call: " + a + " " + b); ob.noChange(a, b); System.out.println("a and b after call: " + a + " " + b); } } Ниже приведен результат выполнения данной программы.
a and b before call: 15 20 a and b after call: 15 20 Нетрудно заметить, что действия, выполняемые в теле метода noChange (), не оказывают никакого влияния на значения переменных а и b в вызывающем методе. Если методу передается объект, то ситуация меняется коренным образом, поскольку объекты передаются неявно по ссылке. Не следует забывать, что создание переменной типа класса, по существу, означает формирование ссылки на объект этого класса. И методу на самом деле передается только ссылка, а не сам объект. Поэтому при передаче этой ссылки методу принимающий ее параметр будет ссылаться на тот же самый объект, на который ссылается аргумент. Это означает, что и аргумент, и параметр ссылается на один и тот же объект и что объекты, по существу, передаются методам по ссылке. Таким образом, объект в методе будет оказывать влияние на объект, используемый в качестве аргумента. Для примера рассмотрим следующую программу:
// Объекты передаются методам по ссылке, class Test { int a, b; Test(int i, int j) { a = i; b = j; } /* Передача объекта методу. Теперь переменные ob.a b и ob.b из передаваемого объекта можно изменить. */ void change(Test ob) { ob.a = ob.a + ob.b; ob.b = -ob.b; }
} class CallByRef { public static void main(String args[]) { Test ob = new Test(15, 20); System.out.println("ob.a and ob.b before call: " + ob.a + " " + ob.b); ob.change(ob); System.out.println("ob.a and ob.b after call: " + ob.a + " " + ob.b); } } Выполнение этой программы дает следующий результат: ob.a and ob.b before call: 15 20 ob.a and ob.b after call: 35 -20
Как видите, в данном случае действия в методе change () оказывают влияние на объект, используемый в качестве аргумента этого метода.
Не следует, однако, забывать, что когда объект передается методу по ссылке, сама ссылка на него передается по значению. Но поскольку передаваемое значение лишь указывает на объект, копия этого значения будет по-прежнему указывать на тот же самый объект в соответствующем аргументе. Возврат объектов
Метод может возвращать данные любого типа, включая и типы классов. Например, объект приведенного ниже класса ErrorMsg может быть использован для сообщения об ошибке. В этом классе имеется метод getErrorMsg (), который возвращает объект типа String, описывающий ошибку. Объект типа String строится на основании кода ошибки, переданного методу. // Возврат объекта типа String, class ErrorMsg { String msgs[] = { "Output Error", "Input Error", "Disk Full", "Index Out-Of-Bounds" }; // возвратить объект типа String в виде сообщения об ошибке String getErrorMsg(int i) { if(i >=0 & i < msgs.length) return msgs[i]; else return "Invalid Error Code"; } } class ErrMsg { public static void main(String args[]) { ErrorMsg err = new ErrorMsg(); System.out.println(err.getErrorMsg(2)); System.out.println(err.getErrorMsg(19)); } }
Выполнение этой программы дает следующий результат: Disk Full Invalid Error Code
Разумеется, возвращать можно и объекты создаваемых классов. Например, приведенный ниже фрагмент кода представляет собой переработанную версию предыдущей программы, где создаются два класса формирования ошибок Err и Error Inf о. В классе Err, помимо кода ошибки, инкапсулируется символьная строка описания ошибки. А в классе Errorlnf о содержится метод getErrorlnf о (), возвращающий объект типа Err. // Возврат объекта, определяемого разработчиком программы, class Err { String msg; // Сообщение об ошибке int severity; // Код, определяющий серьезность ошибки Err(String m, int s) { msg = m; severity = s; } } class Errorlnfo { String msgs[] = { "Output Error", "Input Error", "Disk Full", "Index Out-Of-Bounds" }; int howbad[] = { 3, 3, 2, 4 }; // Возврат объекта типа Err. Err getErrorlnfo(int i) { if(i >=0 & i < msgs.length) return new Err(msgs[i], howbad[i]); else return new Err("Invalid Error Code", 0) ; } } class Errlnfo { public static void main(String args[]) { Errorlnfo err = new Errorlnfo(); Err e; e = err.getErrorlnfo (2); System.out.println(e.msg + " severity: " + e.severity); e = err.getErrorInfo.(19) ; System.out.println(e.msg + " severity: " + e.severity); } }
Disk Full severity: 2 Invalid Error Code severity: 0 При каждом вызове метода getErrorlnfo () создается новый объект типа Err и ссылка на него возвращается вызывающему методу. Этот объект затем используется методом main () для отображения кода серьезности ошибки и текстового сообщения. Объект, возвращенный методом, существует до тех пор, пока на него имеется хотя бы одна ссылка. Если ссылки на объект отсутствуют, он уничтожается системой “сборки мусора”. Поэтому при выполнении программы не возникает ситуация, когда объект разрушается лишь потому, что метод, в котором он был создан, завершился. ## Перегрузка методов методов В этом разделе речь пойдет об одном из самых интересных языковых средств Java – перегрузке методов. Несколько методов одного класса могут иметь одно и то же имя, отличаясь лишь набором параметров. Перегрузка методов является одним из способов реализации принципа полиморфизма в Java. Для того чтобы перегрузить метод, достаточно объявить его новый вариант, отличающийся от уже существующих, а все остальное сделает компилятор. Нужно лишь соблюсти одно условие: тип и/или число параметров в каждом из перегружаемых методов должны быть разными. Некоторые считают, что два метода могут отличаться лишь типом возвращаемого значения, но это заблуждение. Возвращаемый тип не предоставляет достаточных сведений для принятия решения о том, какой именно метод должен быть использован. Конечно, перегружаемые методы могут иметь разные возвращаемые типы, но при вызове метода выполняется лишь тот его вариант, в котором параметры соответствуют передаваемым аргументам. Ниже приведен простой пример программы, демонстрирующий перегрузку методов.
// Перегрузка методов, class Overload { // Первый вариант метода. void ovlDemo() { System.out.println("No parameters"); } // перегрузить метод ovlDemo с одним параметром типа int. // Второй вариант метода. void ovlDemo(int а) { System.out.println("One parameter: " + a); } // перегрузить метод ovlDemo с двумя параметрами типа int. // Третий вариант метода. int ovlDemo(int a, int b) { System.out.println("Two parameters: " + a + " " + b) ; return a + b; } // перегрузить метод ovlDemo с двумя параметрами типа double. // Четвертый вариант метода. double ovlDemo(double a, double b) { System.out.println("Two double parameters: " + a + " "+ b); return a + b; }
}
class OverloadDemo { public static void main(String args[]) { Overload ob = new Overload(); int resl; double resD; // вызвать все варианты метода ovlDemo() ob.ovlDemo(); System.out.println(); ob.ovlDemo(2) ; System.out.println(); resl = ob.ovlDemo(4, 6) ; System.out.println("Result of ob.ovlDemo(4, 6): " + resl); System.out.println(); resD = ob.ovlDemo(1.1, 2.32); System.out.println("Result of ob.ovlDemo(1.1, 2.32): " + resD); }
} Как видите, метод ovlDemo () перегружается четырежды. В первом его варианте параметры не предусмотрены, во втором – определен один целочисленный параметр, в третьем – два целочисленных параметра, в четвертом – два параметра типа double. Обратите внимание на то, что первые два варианта метода ovlDemo () имеют тип void, а два другие возвращают значение. Как пояснялось ранее, тип возвращаемого значения не учитывается при перегрузке методов. Следовательно, попытка определить два варианта метода ovlDemo () так, как показано ниже, приводит к ошибке.
// Возможен лишь один вариант метода ovlDemo (int). // Возвращаемое значение нельзя использовать // для различения перегружаемых методов. void ovlDemo(int а) { System.out.println("One parameter: " + a); } / Ошибка! Два варианта метода ovlDemo(int) не могут существовать, даже если типы возвращаемых ими значений отличаются. / int ovlDemo(int а) { System.out.println("One parameter: " + a); return a * a; } Как поясняется в комментариях к приведенному выше фрагменту кода, отличия возвращаемых типов недостаточно для перегрузки методов. Как следует из главы 2, в Java производится автоматическое приведение типов. Это приведение распространяется и на типы параметров перегружаемых методов. В качестве примера рассмотрим следующий фрагмент кода:
/ Автоматическое преобразование типов может оказывать влияние на выбор перегружаемого метода. / class 0verload2 { void f(int x) { System.out.println("Inside f(int): " + x) ; } void f(double x) { System.out.println("Inside f(double): " + x) ; }
}
class TypeConv { public static void main(String args[]) { overload2 ob = new 0verload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i); // Вызов метода оb.f(int) ob.f(d); // Вызов метода ob.f(double) ob.f(b); // Вызов метода oh.f(int) с преобразованием типов ob.f(s); // Вызов метода ob.f(int) с преобразованием типов ob.f(f); // Вызов метода ob.f(double) с преобразованием типов }
} Выполнение этого фрагмента кода дает следующий результат:
Inside f (int) : 10 Inside f(double): 10.1 Inside f (int): 99 Inside f(int): 10 Inside f(double): 11.5 В данном примере определены только два варианта метода f (): один принимает параметр типа int, а второй – параметр типа double. Но передать методу f () можно также значение типа byte, short или float. Значения типа byte и short исполняющая система Java автоматически преобразует в тип int. В результате будет вызван вариант метода f (int). А если параметр имеет значение типа float, то оно преобразуется в тип double и далее вызывается вариант метода f (double). Важно понимать, что автоматическое преобразование типов выполняется лишь в отсутствие прямого соответствия типов параметра и аргумента. В качестве примера ниже представлена другая версия предыдущей программы, в которой добавлен вариант метода f() с параметром типа byte.
// Добавление варианта метода f(byte). class 0verload2 { void f(byte x) { System.out.println("Inside f(byte): " + x) ; } void f(int x) { System.out.println("Inside f(int) : " + x); } void f(double x) { System.out.printlnpinside f(double): " + x); }
}
class TypeConv { public static void main(String args[]) { 0verload2 ob = new 0verload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i); // Вызов метода ob.f(int) ob.f(d); // Вызов метода ob.f(double) ob.f(b); // Вызов метода ob.f(byte) без преобразования типов ob.f(s) ; // Вызов метода ob.f (int) с преобразованием (типов ob.f(f) ; // Вызов метода ob.f(double) с преобразованием типов }
} Выполнение этой версии программы дает следующий результат:
Inside f(int): 10 Inside f(double): 10.1 Inside f(byte): 99 Inside f(int): 10 Inside f(double): 11.5 В данной версии программы используется вариант метода f () с параметром типа byte. Так, если при вызове метода f () ему передается значение типа byte, вызывается вариант метода f (byte) и автоматическое приведение к типу int не производится. Перегрузка методов представляет собой механизм воплощения полиморфизма, т.е. способ реализации в Java принципа “один интерфейс – множество методов”. Для того чтобы стёбю понятнее, как и для чего это делается, необходимо принять во внимание следующее соображение: в языках программирования, не поддерживающих перегрузку методов, каждый метод должен иметь уникальное имя. Но в ряде случаев требуется выполнять одну и ту же последовательность операций над разными типами данных. В качестве примера рассмотрим функцию, определяющую абсолютное значение. В языках, не поддерживающих перегрузку методов, приходится создавать три или более варианта данной функции с именами, отличающимися хотя бы одним символом. Например, в языке С функция abs () возвращает абсолютное значение числа типа int, функция labs () – абсолютное значение числа типа long, а функция f abs () применяется к значению с плавающей точкой. А поскольку в С не поддерживается перегрузка, то каждая из функций должна иметь свое собственное имя, несмотря на то, что все они выполняют одинаковые действия. Это приводит к неоправданному усложнению процесса написания программ. Разработчику приходится не только представлять себе действия, выполняемые функциями, но и помнить все три их имени. Такая ситуация не возникает в Java, потому что все методы, вычисляющие абсолютное значение, имеют одно и то же имя. В стандартной библиотеке Java для вычисления абсолютного значения предусмотрен метод abs (). Его перегрузка осуществляется в классе Math для обработки значений всех числовых типов. Решение о том, какой именно вариант метода abs () должен быть вызван, исполняющая система Java принимает, исходя из типа аргумента. Главная ценность перегрузки заключается в том, что она обеспечивает доступ к связанным вместе методам по общему имени. Следовательно, имя abs обозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам. Благодаря полиморфизму несколько имен сводятся к одному. Несмотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с более сложными ситуациями в программировании. Когда метод перегружается, каждый его вариант может выполнять какое угодно действие. Для установления взаимосвязи между перегружаемыми методами не существует какого-то твердого правила, но с точки зрения правильного стиля программирования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и то же имя для несвязанных друг с другом методов не следует, хотя это и возможно. Например, имя sqr можно было бы выбрать для методов, возвращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это принципиально разные операции. Такое применение перегрузки методов противоречит ее первоначальному назначению. На практике перегружать следует только тесно связанные операции. ## Перегрузка конструкторов Как и методы, конструкторы также могут перегружаться. Это дает возможность конструировать объекты самыми разными способами. В качестве примера рассмотрим следующую программу:
// Демонстрация перегрузки конструкторов, class MyClass { int х; // Конструкторы перегружаются разными способами. MyClass() { System.out.println("Inside MyClass()."); x = 0 ; } MyClass(int i) { System.out.println("Inside MyClass(int) . ") ; x = i; } MyClass(double d) { System.out.println("Inside MyClass(double)."); x = (int) d; } MyClass(int i, int j) { System.out.println("Inside MyClass(int, int)."); x = i * j; }
}
class OverloadConsDemo { public static void main(String args[]) { MyClass tl = new MyClass(); MyClass t2 = new MyClass(88); MyClass t3 = new MyClass(17.23); MyClass t4 = new MyClass(2, 4); System.out.println("tl.x: " + tl.x); System.out.println("t2.x: " + t2.x); System.out.println("t3.x: " + t3.x); System.out.println("t4.x: " + t4.x); }
} В результате выполнения этой программы получается следующий результат:
Inside MyClass(). Inside MyClass(int). Inside MyClass(double). Inside MyClass(int, int). tl.x: 0 t2.x: 88 t3.x: 17 t4.x: 8 В данном примере конструктор MyClass () перегружается четырежды. Во всех вариантах этого конструктора объект типа MyClass строится по-разному. Конкретный вариант конструктора выбирается из тех параметров, которые указываются при выполнении оператора new. Перегружая конструктор класса, вы предоставляете пользователю созданного вами класса свободу в выборе способа конструирования объекта. Перегрузка конструкторов чаще всего производится для того, чтобы дать возможность инициализировать один объект на основании другого объекта. Рассмотрим в качестве примера следующую программу, в которой класс Summation используется для вычисления суммы двух целочисленных значений.
// Инициализация одного объекта посредством другого, class Summation { int sum; // построить объект из целочисленного значения Summation(int num) { sum = 0; for(int i=l; i <= num; i++) sum += i; } // Построение одного объекта иэ другого. Summation(Summation ob) { sum = ob.sum; }
}
class SumDemo { public static void main(String args[]) { Summation si = new Summation(5); Summation s2 = new Summation(si); System.out.println("si.sum: " + si.sum); System.out.println("s2.sum: " + s2.sum); }
} Выполнение этой программы дает следующий результат:
si.sum: 15 s2.sum: 15 Как следует из приведенного выше примера, использование одного объекта при инициализации другого нередко оказывается вполне оправданным. В данном случае при конструировании объекта s2 нет необходимости вычислять сумму. Даже если подобная инициализация не повышает быстродействие программы, зачастую удобно иметь конструктор, создающий копию объекта. **Пример для опробования 6.2.** Перегрузка конструктора класса Queue В этом проекте предстоит усовершенствовать класс Queue, добавив в него два дополнительных конструктора. В первом из них новая очередь будет конструироваться на основании уже существующей, а во втором – присваиваться начальные значения элементам очереди при ее конструировании. Как станет ясно в дальнейшем, добавление этих конструкторов сделает класс Queue более удобным для использования. Последовательность действий 1. Создайте новый файл QDemo2 . j ava и скопируйте в него код класса Queue, созданный в примере для опробования 6.1. 2. Добавьте сначала в этот класс приведенный ниже конструктор, который будет строить одну очередь на основании другой. // Конструктор, строящий один объект типа Queue на основании другого. Queue(Queue ob) { putloc = ob.putloc; getloc = ob.getloc; q = new char[ob.q.length]; // копировать элементы очереди for(int i=getloc+l; i <= putloc; i++) q[i] = ob.q[i]; } ``` Внимательно проанализируем этот конструктор. Сначала переменные putloc и getloc инициализируются в нем значениями, содержащимися в объекте ob, который передается ему в качестве параметра. Затем в нем организуется новый массив для хранения элементов очереди, которые далее копируются из объекта ob в этот массив. Вновь созданная копия очереди будет идентична оригиналу, хотя они и являются совершенно отдельными объектами.
Добавьте в данный класс конструктор, инициализирующий очередь данными из символьного массива, как показано ниже. // Конструирование и инициализация объекта типа Queue. Queue(char а [ ]) { putloc = 0; getloc = 0; q = new char[a.length+1]; for(int i = 0; i < a.length; i++) put(a[i]); }
В этом конструкторе создается достаточно большая очередь для хранения символов из массива а. В силу особенностей алгоритма, реализующего очередь, длина очереди должна быть на один элемент больше, чем длина исходного массива.
Ниже приведен весь код видоизмененного класса Queue, а также код класса QDemo2, демонстрирующего организацию очереди для хранения символов и обращение с ней. ``` // Класс, реализующий очередь для хранения символов, class Queue { private char q[]; // Массив для хранения элементов очереди, private int putloc, getloc; // Индексы размещения и извлечения // элементов очереди.
// сконструировать пустую очередь заданного размера Queue(int size) { q = new char[size+1]; // выделить память для очереди putloc = getloc = 0;
}
// сконструировать очередь из существующего объекта типа Queue Queue(Queue ob) { putloc = ob.putloc; getloc = ob.getloc; q = new char[ob.q.length]; // копировать элементы в очередь for (int i=getloc+l; i <= putloc; i++) q[i] = ob.q[i];
}
// сконструировать очередь из массива исходных значений Queue(char а[]) { putloc = 0; getloc = 0; q = new char[a.length+1]; for(int i = 0; i < a.length; i++) put(a[i]);
}
// поместить символ в очередь void put(char ch) { if(putloc==q.length-1) { System.out.println(" – Queue is full."); return; } putloc++; q[putloc] = ch;
}
// извлечь символ из очереди char get() { if(getloc == putloc) { System.out.println(" – Queue is empty."); return (char) 0; } getloc++; return q[getloc];
} }
// продемонстрировать класс Queue в действии class QDemo2 { public static void main(String args[]) { // построить пустую очередь для хранения 10 элементов Queue ql = new Queue(10); char name[] = {'Т', 'o', 'm'}; // построить очередь из массива Queue q2 = new Queue(name); char ch; int i; // поместить ряд символов в очередь ql for(i=0; i < 10; i++) ql.put((char) ('A1 + i)); // построить одну очередь из другой очереди Queue q3 = new Queue(ql); // показать очереди System.out.print("Contents of ql: "); for(i=0; i < 10; i++) { ch = ql.get(); System.out.print(ch); } System.out.println("n"); System.out.print("Contents of q2: "); for(i=0; i < 3; i++) { ch = q2.get(); System.out.print(ch); } System.out.println("n"); System.out.print("Contents of q3: "); for(i=0; i < 10; i++) { ch = q3.get(); System.out.print(ch); } }
} ``` Результат выполнения данной программы выглядит следующим образом: ``` Contents of ql: ABCDEFGHIJ Contents of q2: Tom Contents of q3: ABCDEFGHIJ ``` Рекурсия
В Java допускается, чтобы метод вызывал самого себя. Этот процесс называется рекурсией, а метод, вызывающий самого себя, – рекурсивным. Вообще говоря, рекурсия представляет собой процесс, в ходе которого нечто определяет самое себя. В этом отношении она чем-то напоминает циклическое определение. Рекурсивный метод отличается в основном тем, что он содержит оператор, в котором этот метод вызывает самого себя. Рекурсия является эффективным механизмом управления программой.
Классическим примером рекурсии служит вычисление факториала числа. Факториал числа N представляет собой произведение всех целых чисел от 1 до N. Например, факториал числа 3 равен 1x2x3, или 6. В приведенном ниже примере программы демонстрируется рекурсивный способ вычисления факториала числа. Для сравнения в эту программу включен также нерекурсивный вариант вычисления факториала числа. // Простой пример рекурсии, class Factorial { // Рекурсивный метод, int factR(int n) { int result; if(n==l) return 1; // Рекурсивный вызов метода factRO . result = factR(n-l) * n; return result; } // Вариант программы, вычисляющий факториал итеративным способом, int factl(int n) { int t, result; result = 1; for(t=l; t <= n; t++) result *= t; return result; } } class Recursion { public static void main(String args[]) { Factorial f = new Factorial(); System.out.println("Factorials using recursive method."); System.out.println("Factorial of 3 is " + f.factR(3)); System.out.println("Factorial of 4 is " + f.factR(4)); System.out.println("Factorial of 5 is " + f.factR(5)); System.out.println(); System.out.println("Factorials using iterative method."); System.out.println("Factorial of 3 is " + f.factl(3)); System.out.println("Factorial of 4 is " + f.factl(4)); System.out.println("Factorial of 5 is " + f.factl(5)); } }
Ниже приведен результат выполнения данной программы. Factorials using recursive method. Factorial of 3 is 6 Factorial of 4 is 24 Factorial of 5 is 120 Factorials using iterative method. Factorial of 3 is 6 Factorial of 4 is 24 Factorial of 5 is 120
Действия нерекурсивного метода fact I () не требуют особых пояснений. В нем используется цикл, в котором числа, начиная с 1, последовательно умножаются друг на друга, постепенно образуя произведение, дающее факториал.
Рекурсивный метод f actR () действует несколько сложнее. Когда метод factR() вызывается с аргументом, равным 1, он возвращает 1, а иначе —произведение, определяемое из выражения factR(n-l)*n. Для вычисления этого выражения вызывается метод factR () с аргументом п-1. Этот процесс повторяется до тех пор, пока значение переменной п не окажется равным 1, после чего из предыдущих вызовов данного метода начнут возвращаться полученные значения. Например, при вычислении факториала 2 первый вызов метода factR () повлечет за собой второй вызов того же самого метода, но с аргументом 1. В результате метод возвратит значение 1, которое затем умножается на 2 (т.е. исходное значение переменной п). В результате всех этих вычислений будет получен факториал, равный 2. По желанию в тело метода factR () можно ввести операторы println (), чтобы сообщать, на каком именно уровне осуществляется очередной вызов, а также отображать промежуточные результаты вычислений.