Passing Information to a Method or a Constructor
The declaration for a method or a constructor declares the number and the type of the arguments for that method or constructor. For example, the following is a method that computes the monthly payments for a home loan, based on the amount of the loan, the interest rate, the length of the loan (the number of periods), and the future value of the loan:
This method has four parameters: the loan amount, the interest rate, the future value and the number of periods. The first three are double-precision floating point numbers, and the fourth is an integer. The parameters are used in the method body and at runtime will take on the values of the arguments that are passed in.
Parameter Types
You can use any data type for a parameter of a method or a constructor. This includes primitive data types, such as doubles, floats, and integers, as you saw in the computePayment method, and reference data types, such as objects and arrays.
Here's an example of a method that accepts an array as an argument. In this example, the method creates a new Polygon object and initializes it from an array of Point objects (assume that Point is a class that represents an x, y coordinate):
Arbitrary Number of Arguments
You can use a construct called varargs to pass an arbitrary number of values to a method. You use varargs when you don't know how many of a particular type of argument will be passed to the method. It's a shortcut to creating an array manually (the previous method could have used varargs rather than an array).
To use varargs, you follow the type of the last parameter by an ellipsis (three dots, . ), then a space, and the parameter name. The method can then be called with any number of that parameter, including none.
You can see that, inside the method, corners is treated like an array. The method can be called either with an array or with a sequence of arguments. The code in the method body will treat the parameter as an array in either case.
You will most commonly see varargs with the printing methods; for example, this printf method:
allows you to print an arbitrary number of objects. It can be called like this:
or with yet a different number of arguments.
Parameter Names
When you declare a parameter to a method or a constructor, you provide a name for that parameter. This name is used within the method body to refer to the passed-in argument.
The name of a parameter must be unique in its scope. It cannot be the same as the name of another parameter for the same method or constructor, and it cannot be the name of a local variable within the method or constructor.
A parameter can have the same name as one of the class's fields. If this is the case, the parameter is said to shadow the field. Shadowing fields can make your code difficult to read and is conventionally used only within constructors and methods that set a particular field. For example, consider the following Circle class and its setOrigin method:
The Circle class has three fields: x , y , and radius . The setOrigin method has two parameters, each of which has the same name as one of the fields. Each method parameter shadows the field that shares its name. So using the simple names x or y within the body of the method refers to the parameter, not to the field. To access the field, you must use a qualified name. This will be discussed later in this lesson in the section titled "Using the this Keyword."
Passing Primitive Data Type Arguments
Primitive arguments, such as an int or a double , are passed into methods by value. This means that any changes to the values of the parameters exist only within the scope of the method. When the method returns, the parameters are gone and any changes to them are lost. Here is an example:
Passing Reference Data Type Arguments
Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object's fields can be changed in the method, if they have the proper access level.
For example, consider a method in an arbitrary class that moves Circle objects:
Let the method be invoked with these arguments:
Inside the method, circle initially refers to myCircle . The method changes the x and y coordinates of the object that circle references (that is, myCircle ) by 23 and 56, respectively. These changes will persist when the method returns. Then circle is assigned a reference to a new Circle object with x = y = 0 . This reassignment has no permanence, however, because the reference was passed in by value and cannot change. Within the method, the object pointed to by circle has changed, but, when the method returns, myCircle still references the same Circle object as before the method was called.
Java. Методы. Передача параметров в методах (функциях) класса. Передача переменных простых типов и объектов в метод в качестве параметра
Методы. Передача параметров в методах (функциях) класса. Передача переменных примитивных типов и объектов в метод в качестве параметра
Содержание
Поиск на других ресурсах:
1. Какие существуют способы передачи аргументов методу (функции)?
В Java существует 2 способа для передачи переменной или объекта класса в функцию:
- передача по значению. В этом случае значение аргумента копируется в формальный параметр функции. Поскольку создается копия аргумента в функции, то все изменения над копией не повлияют на значение аргумента;
- передача по ссылке (по адресу). В этом случае параметру передается ссылка на аргумент, который используется при вызове. По этой ссылке есть доступ к аргументу. Таким образом, все изменения, сделанные в теле функции над значением параметра, будут изменять значение аргумента который был передан в функцию.
2. Какие особенности передачи аргумента примитивного типа ?
Если методу передается аргумент примитивного типа, то происходит передача по значению. То есть, делается копия аргумента.
3. Каким образом в метод передается объект некоторого класса?
В отличие от переменных примитивных типов, объекты класса передаются по ссылке. Это значит, что изменения, сделанные в теле функции (методе) будут изменять также значения объекта, который задавался в качестве аргумента.
4. Примеры передачи переменных примитивных типов в методы (функции)
Пример 1. Реализация метода Max() , получающего два параметра примитивного типа.
Демонстрация использования метода в другом методе:
Демонстрация использования метода Inc() в методе main() :
будет существовать ссылка на память, выделенную динамично внутри метода Method() . Таким образом, для переменной-объекта varObj не нужно выделять память в вызывающем коде.
Пример. Пусть задан класс MyDay , реализующий день недели.
Name already in use
java_for_beginners_book / c6.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Дополнительные сведения о методах и классах
Основные навыки и понятия
- Управление доступом к членам классов
- Передача объектов при вызове методов
- Возврат объектов из методов
- Перегрузка методов
- Перегрузка конструкторов
- Применение рекурсии
- Использование ключевого слова static
- Применение внутренних классов
- Использование аргументов переменной длины
В этой главе возобновляется рассмотрение классов и методов. Сначала будет показано, каким образом контролируется доступ к членам класса, а затем рассмотрены особенности передачи и возврата объектов из методов, перегрузки методов, использования рекурсии и ключевого слова static. Кроме того, в этой главе будут представлены вложенные классы и методы с аргументами переменной длины.
Управление доступом к членам класса
Поддержка свойства инкапсуляции в классе дает два главных преимущества. Во-первых, класс связывает данные с кодом. Это преимущество использовалось в предыдущих примерах программ, начиная с главы 4. И во-вторых, класс предоставляет средства для управления доступом к его членам. Именно эта, вторая преимущественная особенность и будет рассмотрена ниже.
В языке Java, по существу, имеются два типа членов класса: открытые (public) и закрытые (private), хотя в действительности дело обстоит немного сложнее. Доступ к открытому члену свободно осуществляется из кода, определенного за пределами класса. Именно этот тип члена класса использовался в рассматривавшихся до сих пор примерах программ. А закрытый член класса доступен только методам, определенным в самом классе. С помощью закрытых членов и организуется управление доступом.
Ограничение доступа к членам класса является основополагающей частью объектно-ориентированного программирования, поскольку оно позволяет исключить неверное использование объекта. Разрешая доступ к закрытым данным только с помощью строго определенного ряда методов, можно предупредить присваивание неверных значений этим данным, выполняя, например, проверку диапазона представления чисел. Для закрытого члена класса нельзя задать значение непосредственно в коде за пределами класса. Но в то же время можно полностью управлять тем, как и когда данные используются в объекте. Следовательно, правильно реализованный класс образует некий “черный ящик”, которым можно пользоваться, но внутренний механизм «его действия закрыт для вмешательства извне.
В рассмотренных ранее примерах программ не уделялось никакого внимания управлению доступом, поскольку в Java члены класса по умолчанию доступны из остальных частей программы. (Иными словами, они открыты для доступа по умолчанию.) Это удобно для создания небольших программ (в том числе и тех, что служат примерами в данной книге), но недопустимо в большинстве реальных условий эксплуатации программного обеспечения. Ниже будет показано, какими языковыми средствами Java можно пользоваться для управления доступом.
Модификаторы доступа в Java
Управление доступом к членам класса в Java осуществляется с помощью трех модификаторов доступа (называемых также спецификаторами): public, private и protected. Если модификатор не указан, то принимается тип доступа по умолчанию. В этой главе будут рассмотрены модификаторы public и private. Модификатор protected непосредственно связан с наследованием, и поэтому он будет обсуждаться в главе 8.
Когда член класса обозначается модификатором public, он становится доступным из любого другого кода в программе, включая и методы, определенные в других классах. Когда же член класса обозначается модификатором private, он может быть доступен только другим членам этого класса. Следовательно, методы из других классов не имеют доступа к закрытому члену данного класса.
Если все классы в программе относятся к одному пакету, то отсутствие модификатора доступа равнозначно указанию модификатора public по умолчанию. Пакет представляет собой группу классов, предназначенных как для организации классов, так и для управления доступом. Рассмотрение пакетов откладывается до главы 8, а для примеров программ, представленных в этой и предыдущих главах, тип доступа по умолчанию не отличается от public.
Модификатор доступа указывается перед остальной частью описания типа отдельного члена класса. Это означает, что именно с него должен начинаться оператор объявления члена класса. Ниже приведены соответствующие примеры.
Для того чтобы стал понятнее эффект от применения модификаторов доступа public и private, рассмотрим следующий пример программы:
Нетрудно заметить, что в классе MyClass переменная alpha определена как private, переменная beta — как public, а перед переменной gamma модификатор доступа отсутствует, т.е. в данном примере она ведет себя как открытый член класса, которому по умолчанию присваивается модификатор доступа public. Переменная alpha закрыта, и поэтому к ней невозможно обратиться за пределами ее класса. Следовательно, в классе AccessDemo нельзя пользоваться переменной alpha непосредственно. Доступ к ней организуется с помощью открытых методов доступа setAlpha() и getAlpha(), определенных в одном с ней классе. Если удалить комментарии в начале следующей строки кода, то скомпилировать рассматриваемую здесь программу не удастся:
Компилятор выдаст сообщение об ошибке, связанной с нарушением правил доступа. Несмотря на то что переменная alpha недоступна для кода за пределами класса MyClass, пользоваться ею можно с помощью открытых методов доступа setAlpha() и getAlpha().
Таким образом, закрытые переменные могут быть свободно использованы другими членами класса, но недоступны за пределами этого класса.
Рассмотрим практическое применение средств управления доступом на примере приведенной ниже программы. Во время ее выполнения предотвращается возникновение ошибок нарушения границ отказоустойчивого целочисленного массива. Это достигается следующим образом. Массив объявляется как закрытый член класса, а доступ к нему осуществляется с помощью специально предназначенных для этой цели методов. Эти методы отслеживают попытки обращения к элементам, не входящим в массив, и вместо аварийной остановки программы возвращают сообщение об ошибке. Массив определяется в классе FailSof tArray, код которого приведен ниже.
Выполнение этой программы дает следующий результат:
Рассмотрим подробнее приведенный выше пример программы. В классе Fail So ft Array определены три закрытых члена. Первым из них является перемен¬ ная а, в которой содержится ссылка на массив, предназначенный для хранения данных. Вторым членом является переменная errval, в которой хранится значение, возвращае¬ мое вызывающей части программы в том случае, если вызов метода get() оказывает¬ ся неудачным. И третьим членом является метод ok(), в котором определяется, нахо¬ дится ли индекс в границах массива. Эти три члена могут быть использованы только другими членами класса FailSof tArray. Остальные члены данного класса объявлены открытыми и могут быть вызваны из любой части программы, где используется класс FailSoftArray.
При построении объекта типа FailSof tArray следует указать размер массива и значение, которое должно быть возвращено, если вызов get() окажется неудачным. Ошибочное значение должно отличаться от тех значений, которые могут храниться в массиве. Конкретный массив, обращение к которому осуществляется по ссылке в переменной а, а также ошибочное значение в переменной errval не могут быть непосредственно доступны пользователям построенного объекта типа FailSoftArray, и благодаря этому неправильное их употребление исключается. В частности, пользователь не может непосредственно обратиться к массиву по ссылке в переменной а, указав индекс нужного элемента и не нарушив, возможно, при этом границы массива. Это можно сделать только с помощью методов get() и put().
Метод 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. В результате код этого класса должен выглядеть так, как показано ниже.
- Теперь, когда массив q и переменные putloc и getloc стали закрытыми, класс Queue строго следует принципу “первым пришел — первым обслужен”, по которому действует очередь.
Передача объектов методам
В приведенных ранее примерах программ в качестве параметров методам передавались лишь простые типы. Но параметрами могут быть и объекты. Например, в привеское значение true только в том случае, если все три размера обоих параллелепипедов совпадают. А в методе same Volume() сравниваются лишь объемы двух параллелепипедов. Но в обоих случаях параметр ob имеет тип Block. Несмотря на то что Block — это класс, параметры данного типа используются таким же образом, как и параметры встроенных в Java типов данных.
Способы передачи аргументов методу
Как показывает приведенный выше пример, передача объекта методу производится очень просто. Но в этом примере показаны не все нюансы данного процесса. В некоторых случаях последствия передачи объекта по ссылке будут отличаться от тех результатов, к которым приводит передача значения обычного типа. Для выяснения причин этих отличий рассмотрим два способа передачи аргументов методу.
Первым способом является вызов по значению. В этом случае значение аргумента копируется в формальный параметр метода. Следовательно, изменения, вносимые в параметр метода, не оказывают никакого влияния на аргумент, используемый для вызова. А вторым способом передачи аргумента является вызов по ссылке. В данном случае параметру метода передается ссылка на аргумент, а не значение аргумента. В методе эта ссылка используется для доступа к конкретному аргументу, указываемому при вызове. Это означает, что изменения, вносимые в параметр, будут оказывать влияние на аргу¬ мент, используемый для вызова метода. Как будет показано далее, в Java используются оба способа. А выбор конкретного способа зависит от того, что именно передается.
Если методу передается простой тип, например int или double, он передается по значению. При этом создается копия аргумента, а то, что происходит с параметром, принимающим аргумент, не распространяется за пределы метода. Рассмотрим в качестве примера следующую программу:
Ниже приведен результат выполнения данной программы.
Нетрудно заметить, что действия, выполняемые в теле метода noChange(), не оказывают никакого влияния на значения переменных а и b в вызывающем методе.
Если методу передается объект, то ситуация меняется коренным образом, поскольку объекты передаются неявно по ссылке. Не следует забывать, что создание переменной типа класса, по существу, означает формирование ссылки на объект этого класса. И методу на самом деле передается только ссылка, а не сам объект. Поэтому при передаче этой ссылки методу принимающий ее параметр будет ссылаться на тот же самый объект, на который ссылается аргумент. Это означает, что и аргумент, и параметр ссылается на один и тот же объект и что объекты, по существу, передаются методам по ссылке. Таким образом, объект в методе будет оказывать влияние на объект, используемый в качестве аргумента. Для примера рассмотрим следующую программу:
Выполнение этой программы дает следующий результат:
Как видите, в данном случае действия в методе change() оказывают влияние на объект, используемый в качестве аргумента этого метода.
Не следует, однако, забывать, что когда объект передается методу по ссылке, сама ссылка на него передается по значению. Но поскольку передаваемое значение лишь указывает на объект, копия этого значения будет по-прежнему указывать на тот же самый объект в соответствующем аргументе.
Метод может возвращать данные любого типа, включая и типы классов. Например, объект приведенного ниже класса ErrorMsg может быть использован для сообщения об ошибке. В этом классе имеется метод getErrorMsg(), который возвращает объект типа String, описывающий ошибку. Объект типа String строится на основании кода ошибки, переданного методу.
Выполнение этой программы дает следующий результат:
Разумеется, возвращать можно и объекты создаваемых классов. Например, приведенный ниже фрагмент кода представляет собой переработанную версию предыдущей программы, где создаются два класса формирования ошибок Err и Error Inf о. В классе Err, помимо кода ошибки, инкапсулируется символьная строка описания ошибки. А в классе Errorlnf о содержится метод getErrorlnf о (), возвращающий объект типа Err.
При каждом вызове метода getErrorlnfo() создается новый объект типа Err и ссылка на него возвращается вызывающему методу. Этот объект затем используется методом main() для отображения кода серьезности ошибки и текстового сообщения.
Объект, возвращенный методом, существует до тех пор, пока на него имеется хотя бы одна ссылка. Если ссылки на объект отсутствуют, он уничтожается системой “сборки мусора”. Поэтому при выполнении программы не возникает ситуация, когда объект разрушается лишь потому, что метод, в котором он был создан, завершился.
В этом разделе речь пойдет об одном из самых интересных языковых средств Java — перегрузке методов. Несколько методов одного класса могут иметь одно и то же имя, отличаясь лишь набором параметров. Перегрузка методов является одним из способов реализации принципа полиморфизма в Java.
Для того чтобы перегрузить метод, достаточно объявить его новый вариант, отличающийся от уже существующих, а все остальное сделает компилятор. Нужно лишь соблюсти одно условие: тип и/или число параметров в каждом из перегружаемых методов должны быть разными. Некоторые считают, что два метода могут отличаться лишь типом возвращаемого значения, но это заблуждение. Возвращаемый тип не предоставляет достаточных сведений для принятия решения о том, какой именно метод должен быть использован. Конечно, перегружаемые методы могут иметь разные возвращаемые типы, но при вызове метода выполняется лишь тот его вариант, в котором параметры соответствуют передаваемым аргументам.
Ниже приведен простой пример программы, демонстрирующий перегрузку методов.
Как видите, метод ovlDemo() перегружается четырежды. В первом его варианте параметры не предусмотрены, во втором — определен один целочисленный параметр, в третьем — два целочисленных параметра, в четвертом — два параметра типа double. Обратите внимание на то, что первые два варианта метода ovlDemo() имеют тип void, а два другие возвращают значение. Как пояснялось ранее, тип возвращаемого значения не учитывается при перегрузке методов. Следовательно, попытка определить два варианта метода ovlDemo() так, как показано ниже, приводит к ошибке.
Как поясняется в комментариях к приведенному выше фрагменту кода, отличия возвращаемых типов недостаточно для перегрузки методов.
Как следует из главы 2, в Java производится автоматическое приведение типов. Это приведение распространяется и на типы параметров перегружаемых методов. В качестве примера рассмотрим следующий фрагмент кода:
Выполнение этого фрагмента кода дает следующий результат:
В данном примере определены только два варианта метода f(): один принимает параметр типа int, а второй — параметр типа double. Но передать методу f() можно также значение типа byte, short или float. Значения типа byte и short исполняющая система Java автоматически преобразует в тип int. В результате будет вызван вариант метода f (int). А если параметр имеет значение типа float, то оно преобразуется в тип double и далее вызывается вариант метода f (double).
Важно понимать, что автоматическое преобразование типов выполняется лишь в отсутствие прямого соответствия типов параметра и аргумента. В качестве примера ниже представлена другая версия предыдущей программы, в которой добавлен вариант метода f() с параметром типа byte.
Выполнение этой версии программы дает следующий результат:
В данной версии программы используется вариант метода f() с параметром типа byte. Так, если при вызове метода f() ему передается значение типа byte, вызывается вариант метода f (byte) и автоматическое приведение к типу int не производится.
Перегрузка методов представляет собой механизм воплощения полиморфизма, т.е. способ реализации в Java принципа “один интерфейс — множество методов”. Для того чтобы стёбю понятнее, как и для чего это делается, необходимо принять во внимание следующее соображение: в языках программирования, не поддерживающих перегрузку методов, каждый метод должен иметь уникальное имя. Но в ряде случаев требуется выполнять одну и ту же последовательность операций над разными типами данных. В качестве примера рассмотрим функцию, определяющую абсолютное значение. В языках, не поддерживающих перегрузку методов, приходится создавать три или более варианта данной функции с именами, отличающимися хотя бы одним символом. Например, в языке С функция abs() возвращает абсолютное значение числа типа int, функция labs() — абсолютное значение числа типа long, а функция f abs() применяется к значению с плавающей точкой. А поскольку в С не поддерживается перегрузка, то каждая из функций должна иметь свое собственное имя, несмотря на то, что все они выполняют одинаковые действия. Это приводит к неоправданному усложнению процесса написания программ. Разработчику приходится не только представлять себе действия, выполняемые функциями, но и помнить все три их имени. Такая ситуация не возникает в Java, потому что все методы, вычисляющие абсолютное значение, имеют одно и то же имя. В стандартной библиотеке Java для вычисления абсолютного значения предусмотрен метод abs(). Его перегрузка осуществляется в классе Math для обработки значений всех числовых типов. Решение о том, какой именно вариант метода abs() должен быть вызван, исполняющая система Java принимает, исходя из типа аргумента.
Главная ценность перегрузки заключается в том, что она обеспечивает доступ к связанным вместе методам по общему имени. Следовательно, имя abs обозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам. Благодаря полиморфизму несколько имен сводятся к одному. Несмотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с более сложными ситуациями в программировании.
Когда метод перегружается, каждый его вариант может выполнять какое угодно действие. Для установления взаимосвязи между перегружаемыми методами не существует какого-то твердого правила, но с точки зрения правильного стиля программирования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и то же имя для несвязанных друг с другом методов не следует, хотя это и возможно. Например, имя sqr можно было бы выбрать для методов, возвращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это принципиально разные операции. Такое применение перегрузки методов противоречит ее первоначальному назначению. На практике перегружать следует только тесно связанные операции.
Как и методы, конструкторы также могут перегружаться. Это дает возможность конструировать объекты самыми разными способами. В качестве примера рассмотрим следующую программу:
В результате выполнения этой программы получается следующий результат:
В данном примере конструктор MyClass() перегружается четырежды. Во всех вариантах этого конструктора объект типа MyClass строится по-разному. Конкретный вариант конструктора выбирается из тех параметров, которые указываются при выполнении оператора new. Перегружая конструктор класса, вы предоставляете пользователю созданного вами класса свободу в выборе способа конструирования объекта.
Перегрузка конструкторов чаще всего производится для того, чтобы дать возможность инициализировать один объект на основании другого объекта. Рассмотрим в качестве примера следующую программу, в которой класс Summation используется для вычисления суммы двух целочисленных значений.
Выполнение этой программы дает следующий результат:
Как следует из приведенного выше примера, использование одного объекта при инициализации другого нередко оказывается вполне оправданным. В данном случае при конструировании объекта s2 нет необходимости вычислять сумму. Даже если подобная инициализация не повышает быстродействие программы, зачастую удобно иметь конструктор, создающий копию объекта.
Пример для опробования 6.2. Перегрузка конструктора класса Queue
В этом проекте предстоит усовершенствовать класс Queue, добавив в него два дополнительных конструктора. В первом из них новая очередь будет конструироваться на основании уже существующей, а во втором — присваиваться начальные значения элементам очереди при ее конструировании. Как станет ясно в дальнейшем, добавление этих конструкторов сделает класс Queue более удобным для использования.
Создайте новый файл QDemo2 . j ava и скопируйте в него код класса Queue, созданный в примере для опробования 6.1.
Добавьте сначала в этот класс приведенный ниже конструктор, который будет строить одну очередь на основании другой.
Внимательно проанализируем этот конструктор. Сначала переменные putloc и getloc инициализируются в нем значениями, содержащимися в объекте ob, который передается ему в качестве параметра. Затем в нем организуется новый массив для хранения элементов очереди, которые далее копируются из объекта ob в этот массив. Вновь созданная копия очереди будет идентична оригиналу, хотя они и являются совершенно отдельными объектами.
Добавьте в данный класс конструктор, инициализирующий очередь данными из символьного массива, как показано ниже.
В этом конструкторе создается достаточно большая очередь для хранения символов из массива а. В силу особенностей алгоритма, реализующего очередь, длина очереди должна быть на один элемент больше, чем длина исходного массива.
Ниже приведен весь код видоизмененного класса Queue, а также код класса QDemo2, демонстрирующего организацию очереди для хранения символов и обращение с ней.
Результат выполнения данной программы выглядит следующим образом:
В Java допускается, чтобы метод вызывал самого себя. Этот процесс называется рекурсией, а метод, вызывающий самого себя, — рекурсивным. Вообще говоря, рекурсия представляет собой процесс, в ходе которого нечто определяет самое себя. В этом отношении она чем-то напоминает циклическое определение. Рекурсивный метод отличается в основном тем, что он содержит оператор, в котором этот метод вызывает самого себя. Рекурсия является эффективным механизмом управления программой.
Классическим примером рекурсии служит вычисление факториала числа. Факториал числа N представляет собой произведение всех целых чисел от 1 до N. Например, факториал числа 3 равен 1x2x3, или 6. В приведенном ниже примере программы демонстрируется рекурсивный способ вычисления факториала числа. Для сравнения в эту программу включен также нерекурсивный вариант вычисления факториала числа.
Ниже приведен результат выполнения данной программы.
Действия нерекурсивного метода fact I() не требуют особых пояснений. В нем используется цикл, в котором числа, начиная с 1, последовательно умножаются друг на друга, постепенно образуя произведение, дающее факториал.
Рекурсивный метод f actR() действует несколько сложнее. Когда метод factR() вызывается с аргументом, равным 1, он возвращает 1, а иначе —произведение, определяемое из выражения factR(n-l)*n. Для вычисления этого выражения вызывается метод factR() с аргументом п-1. Этот процесс повторяется до тех пор, пока значение переменной п не окажется равным 1, после чего из предыдущих вызовов данного метода начнут возвращаться полученные значения. Например, при вычислении факториала 2 первый вызов метода factR() повлечет за собой второй вызов того же самого метода, но с аргументом 1. В результате метод возвратит значение 1, которое затем умножается на 2 (т.е. исходное значение переменной п). В результате всех этих вычислений будет получен факториал, равный 2. По желанию в тело метода factR() можно ввести операторы println(), чтобы сообщать, на каком именно уровне осуществляется очередной вызов, а также отображать промежуточные результаты вычислений.
Когда метод вызывает самого себя, в системном стеке распределяется память для новых локальных переменных и параметров, и код метода выполняется с этими новыми переменными и параметрами с самого начала. При рекурсивном вызове метода не создается его новая копия, но лишь используются его новые аргументы. А при возврате из каждого рекурсивного вызова старые локальные переменные и параметры извлекаются из стека, и выполнение возобновляется с точки вызова в методе. Рекурсивные методы можно сравнить по принципу действия с постепенно сжимающейся и затем распрямляющейся пружиной.
Рекурсивные варианты многих процедур могут выполняться немного медленнее, чем их итерационные эквиваленты, из-за дополнительных затрат системных ресурсов на неоднократные вызовы метода. Если же таких вызовов окажется слишком много, то в конечном итоге может быть переполнен системный стек. А поскольку параметры и локальные переменные рекурсивного метода хранятся в системном стеке и при каждом новом вызове этого метода создается их новая копия, то в какой-то момент стек может оказаться исчерпанным. Если возникнет подобная ситуация, исполняющая система Java сгенерирует исключение. Но в большинстве случаев об этом не стоит особенно беспокоиться. Как правило, переполнение системного стека происходит тогда, когда рекурсивный метод выходит из под контроля.
Главное преимущество рекурсии заключается в том, что она позволяет реализовать некоторые алгоритмы яснее и проще, чем итерационным способом. Например, алгоритм быстрой сортировки довольно трудно реализовать итерационным способом. А некоторые задачи, например искусственного интеллекта, очевидно, требуют именно рекурсивного решения. При написании рекурсивных методов следует непременно указать в соответствующем месте условный оператор, например if, чтобы организовать возврат из метода без рекурсии. В противном случае возврат из вызванного однажды рекурсивного метода может вообще не произойти. Подобного рода ошибка весьма характерна для реализации рекурсии в практике программирования. Поэтому рекомендуется пользоваться операторами, содержащими вызовы метода println(), чтобы следить за происходящим в рекурсивном методе и прервать его выполнение, если в нем обнаружится ошибка.
Применение ключевого слова static
Иногда требуется определить такой член класса, который будет использоваться независимо от всех остальных объектов этого класса. Как правило, доступ к члену класса организуется посредством объекта этого класса, но в то же время можно создать член класса для самостоятельного применения без ссылки на конкретный экземпляр объекта. Для того чтобы создать такой член класса, достаточно указать в самом начале его объявления ключевое слово static. Если член класса объявляется как static, он становится доступным до создания любых объектов своего класса и без ссылки на какой-нибудь объект. С помощью ключевого слова static можно объявлять как переменные, так и методы. Наиболее характерным примером члена типа static служит метод main(), который объявляется таковым потому, что он должен вызываться виртуальной машиной Java в самом начале выполняемой программы.
Для того чтобы воспользоваться членом типа static за пределами класса, достаточно указать имя этого класса с оператором-точкой. Но создавать объект для этого не нужно. В действительности член типа static оказывается доступным не по ссылке на объект, а по имени своего класса. Так, если требуется присвоить значение 10 переменной count типа static, являющейся членом класса Timer, то для этой цели можно воспользоваться следующей строкой кода:
Эта форма записи подобна той, что используется для доступа к обычным переменным экземпляра посредством объекта, но в ней указывается имя класса, а не объекта. Аналогичным образом можно вызвать метод типа static, используя имя класса и оператор-точку.
Переменные, объявляемые как static, по существу являются глобальными. Когда же объекты объявляются в своем классе, копия переменной типа static не создается. Вместо этого все экземпляры класса совместно пользуются одной и той же переменной типа static. Ниже приведен пример программы, демонстрирующий отличия переменной, объявленной как static, от обычной переменной экземпляра.
Выполнение этой программы дает следующий результат:
Нетрудно заметить, что статическая переменная у используется как объектом obi, так и объектом оЬ2. Изменения в ней оказывают влияние на весь класс, а не только на его экземпляр.
Метод типа static отличается от обычного метода тем, что его можно вызывать по имени его класса, не создавая экземпляр объекта этого класса. Пример такого вызова уже приводился ранее. Это был метод sqrt() типа static, относящийся к классу Math из стандартной библиотеки классов Java. Ниже приведен пример программы, в которой объявляется статическая переменная и создается метод типа static.
Выполнение этой программы дает следующий результат:
На применение методов типа static накладывается ряд следующих ограничений.
- В методе типа static допускается непосредственный вызов только других методов типа static.
- Для метода типа static непосредственно доступными оказываются только другие данные типа static, определенные в его классе.
- В методе типа static должна отсутствовать ссылка this.
В приведенном ниже классе код статического метода valDivDenom() создан некорректно.
Иногда для подготовки к созданию объектов в классе должны быть выполнены некоторые инициализирующие действия. В частности, может возникнуть потребность установить соединение с удаленным сетевым узлом или задать значения некоторых статических переменных перед тем, как воспользоваться статическими методами класса. Для решения подобных задач в Java предусмотрены статические блоки. Статический блок выполняется при первой загрузке класса, еще до того, как класс будет использован для каких-нибудь других целей. Ниже приведен пример применения статического блока.
Результат выполнения данной программы выглядит следующим образом:
Как видите, статический блок выполняется еще до того, как будет создан какой-нибудь объект.
Пример для опробования 6.3. Быстрая сортировка
В главе 5 был рассмотрен простой способ так называемой пузырьковой сортировки, а кроме него, вкратце упоминались и более совершенные способы сортировки. В этом проекте предстоит реализовать один из самых лучших способов: быструю сортировку. Алгоритм быстрой сортировки был разработан Ч. Хоаром и назван его именем. На сегодняшний день это самый лучший универсальный алгоритм сортировки. Он не был продемонстрирован в главе 5 лишь потому, что реализовать быструю сортировку лучше всего с помощью рекурсии. В данном проекте будет создана программа для сортировки символьного массива, но демонстрируемый подход может быть применен к сортировке любых объектов.
Быстрая сортировка опирается на принцип разделения. Сначала выбирается опорное значение (так называемый компаранд), и массив разделяется на две части. Все элементы, которые больше или равны разделяемому компаранду, помещаются в одну часть массива, а те элементы, которые меньше компаранда, — в другую часть. Затем процесс рекурсивно повторяется для каждой оставшейся части до тех пор, пока массив не окажется отсортированным. Допустим, имеется массив, содержащий последовательность символов f edacb, а в качестве компаранда выбран символ d. На первом проходе массив будет частично упорядочен следующим образом:
Исходные данные | f e d a с b |
Проход 1 | b с a d e f |
Этот процесс повторяется для каждой части: Ьса и def. Как видите, процесс рекурсивен по своей сути, поэтому рекурсивный способ лучше всего подходит для реализации быстрой сортировки.
Компаранд можно выбрать двумя способами: случайно либо вычислив среднее значение части элементов массива. Эффективность сортировки окажется оптимальной в том случае, когда компаранд выбран как раз посредине диапазона значений элементов, содержащихся в массиве, но зачастую выбрать такое значение непросто. Если же выбрать компаранд случайным образом, то вполне возможно, что он окажется на краю диапазона. Но и в этом случае алгоритм быстрой сортировки будет действовать правильно. В том варианте быстрой сортировки, который реализуется в данном проекте, в качестве компаранда выбирается элемент, находящийся посередине массива.
- Создайте новый файл QSDemo. j ava.
- Создайте сначала класс Quicksort, код которого приведен ниже.
Вложенные и внутренние классы
В Java определены вложенные классы. Вложенным называется такой класс, который объявляется в другом классе. Вложенные классы не относятся к базовым языковым средствам Java. Они даже не поддерживались до появления версии Java 1.1, хотя с тех пор часто применяются в реальных программах, и поэтому о них нужно знать.
Вложенный класс не может существовать независимо от объемлющего класса, потому что последний ограничивает область его действия. Если вложенный класс объявлен в пределах области действия объемлющего класса, он становится членом последнего. Имеется также возможность объявить вложенный класс, который станет локальным в пределах блока.
Существуют два типа вложенных классов. Одни вложенные классы объявляются с помощью модификатора доступа static, а другие — без него. В этой книге будет рассматриваться только нестатический вариант вложенных классов. Классы такого типа называются внутренними. Внутренний класс имеет доступ ко всем переменным и методам внешнего (т.е. объемлющего) класса и может обращаться к ним непосредственно, как и все остальные нестатические члены внешнего класса. Иногда внутренний класс используется для предоставления услуг объемлющему классу. Ниже приведен пример применения внутреннего класса для вычисления различных значений в объемлющем его классе.
Результат выполнения данной программы выглядит следующим образом:
В данном примере внутренний класс Inner обрабатывает массив nums, являющийся членом класса Outer. Как упоминалось выше, вложенный класс имеет доступ к членам объемлющего класса, и поэтому он может непосредственно обращаться к массиву nums. А вот обратное не справедливо. Так, например, метод analyze() не может непосредственно вызвать метод min(), не создав объект типа Inner.
Как упоминалось выше, класс можно вложить в области действия блока. В итоге получается локальный класс, недоступный за пределами блока. В следующем примере программы класс ShowBits, созданный в примере для опробования 5.3, преобразуется таким образом, чтобы стать локальным.
Выполнение этой программы дает следующий результат:
В данном примере класс ShowBits недоступен за пределами метода main(), а следовательно, попытка получить доступ к нему из любого метода, кроме main(), приведет к ошибке.
И последнее замечание: внутренний класс может быть безымянным. Экземпляр безымянного, или анонимного, внутреннего класса создается при объявлении класса с помощью оператора new. Безымянные внутренние классы будут подробнее рассмотрены в главе 15.
Аргументы переменной длины
Иногда оказываются полезными методы, способные принимать переменное число аргументов. Например, методу, устанавливающему соединение с Интернетом, могут понадобиться имя и пароль пользователя, имя файла, протокол и другие параметры. Если при вызове метода какие-нибудь из этих данных не указаны, то должны использоваться значения по умолчанию. В таком случае было бы уместнее передавать только те аргументы, для которых не предусмотрены значения по умолчанию. А для этого требуется метод, поддерживающий список аргументов переменной, не фиксированной длины.
Раньше для поддержки переменного числа аргументов применялись два способа, причем оба были далеки от совершенства. Первый способ состоял в следующем: если максимальное число аргументов невелико и известно заранее, то можно создать разные варианты одного метода. Очевидно, что такой подход применим лишь в отдельных случаях. Если же значений оказывалось слишком много или их максимальное количество не было известно заранее, то применялся второй способ: параметры помещались в массив, который и передавался методу. Обоим этим способам присущи определенные недостатки, и со временем стало ясно, что требуется какой-то другой, более совершенный подход к решению данной задачи.
В версии JDK 5 появились языковые средства Java, упрощающие создание методов, которым требуется переменное число аргументов. Эти средства называются аргументами переменной длины, а метод, принимающий переменное число аргументов,— методом переменной арности, или же методом с аргументами переменной длины. Список параметров, соответствующих аргументам переменной длины, имеет не фиксированную, а переменную длину. Поэтому метод с аргументами переменной длины может принимать произвольно изменяющееся число аргументов.
Общие положения об аргументах переменной длины
Для указания на то, что метод может принимать переменное число аргументов, в его объявление включается многоточие (. )• Ниже приведен пример метода vaTest(), принимающего переменное число аргументов.
Обратите внимание на следующий синтаксис объявления аргумента v:
Этот синтаксис сообщает компилятору, что метод vaTest() может вызываться с указанием произвольного числа аргументов, в том числе и совсем без них. Более того, аргумент v неявно объявляется как массив типа int [ ]. А в теле метода vaTest() доступ к аргументу v осуществляется с помощью обычного синтаксиса обращения к массивам.
Ниже приведен весь исходный код примера программы, демонстрирующего метод vaTest() в действии.
Выполнение этой программы дает следующий результат:
В приведенной выше программе обращает на себя внимание следующее. Во-первых, как пояснялось выше, обращение к аргументу v в методе vaTest() осуществляется как к массиву. Дело в том, что он действительно является массивом. Многоточие в объявлении этого метода указывает компилятору на использование переменного числа аргументов и на необходимость поместить их в массив v. Во-вторых, при обращении к методу vaTest() в методе main() указывается разное число аргументов, включая и вызов данного метода вообще без аргументов. Указываемые аргументы автоматически помещаются в массив v. Если же аргументы не указаны, длина этого массива будет равна нулю.
Помимо аргумента переменной длины, в методе можно также указывать и обычные аргументы, но при одном условии: аргумент переменной длины должен быть указан последним. Например, приведенное ниже объявление метода является вполне допустимым,
В данном случае первые три аргумента, передаваемые при вызове метода dolt(), будут соответствовать первым трем параметрам. А остальные аргументы будут считаться относящимися к параметру переменной длины vals.
Ниже приведен переработанный вариант метода vaTest(), в котором используются как обычные аргументы, так и аргументы переменной длины.
Выполнение этого фрагмента кода дает следующий результат:
He следует, однако, забывать, что аргумент переменной длины должен быть указан последним. Например, следующее объявление метода недопустимо:
В данном примере сделана попытка указать обычный аргумент после аргумента переменной длины.
Существует еще одно ограничение, которое следует соблюдать: аргументы переменной длины можно указать в методе только один раз. Например, приведенное ниже объявление метода составлено неверно.
Ошибкой в данном случае является попытка указать два разных типа аргументов переменной длины.
Перегрузка методов с аргументами переменной длины
Если требуется, то метод, принимающий переменное число аргументов, можно перегрузить. Например, в следующей программе трижды перегружается метод vaTest():
Выполнение этой программы дает следующий результат:
В приведенном выше примере программы демонстрируются два способа перегрузки методов с аргументами переменной длины. Во-первых, типы параметров аргументов длины у перегружаемых методов могут отличаться. Это демонстрируют варианты метода vaTest (int . . .) и vaTest (boolean . . .). Напомним: многоточие обозначает, что соответствующий аргумент должен рассматриваться как массив указанного типа. Следовательно, как и при перегрузке обычных методов указываются разные типы параметров, так и перегрузке методов с аргументами переменной длины задаются разные типы подобных аргументов. Исполняющая система Java использует эти данные для правильного выбора вызываемого метода.
Второй способ перегрузки методов с аргументами переменной длины состоит в добавлении обьгчных аргументов. Он реализован в варианте метода vaTest (String, int . . .). В этом случае исполняющая система Java использует для выбора нужного варианта метода данные как о числе параметров, так и об их типах.
Аргументы переменной длины и неоднозначность
При перегрузке методов с аргументами переменной длины может возникнуть довольно неожиданная ошибка. А возникает она вследствие неоднозначности в выборе метода. Рассмотрим в качестве примера следующую программу:
В этой программе перегрузка метода vaTest() указана правильно, но она не компилируется. И причиной тому служит следующий вызов:
Переменное число аргументов подразумевает в том числе и нулевое их число, и поэтому приведенный выше вызов может быть интерпретирован и как vaTest (int . . .), и как vaTest (boolean . . .). Оба вызова допустимы, и поэтому обращение к данному методу неоднозначно.
Рассмотрим еще один пример неоднозначности при обращении к методу. Из приведенных ниже вариантов метода vaTest() невозможно однозначно выбрать требуемый, несмотря на то, что в одном из вариантов метода, помимо аргумента переменной длины, присутствует также обычный аргумент.
И хотя списки аргументов у обоих вариантов метода vaTest() отличаются, компилятор все равно не может правильно выбрать вариант для следующего вызова:
В самом деле, не понятно, нужно ли преобразовать этот вызов в vaTest (int . . .) с одним аргументом переменной длины или же в вызов vaTest (int, int . . .) без аргументов переменной длины? В итоге возникает неоднозначная ситуация.
Вследствие ошибок, подобных описанным выше, в ряде случаев приходится отказываться от перегрузки и присваивать методам разные имена. Кроме того, ошибки неоднозначности вскрывают концептуальные просчеты в программировании, которые можно исправить, более тщательно обдумав структуру программы.
Java: передача параметров по значению или по ссылке
Простое объяснение принципов передачи параметров в Java.
Многие программисты часто путают, какие параметры в Java передаются по значению, а какие по ссылке. Давайте визуализируем этот процесс, и тогда вы увидите насколько все просто.
Данные передаются между методами через параметры. Есть два способа передачи параметров:
Передача по значению (by value). Значения фактических параметров копируются. Вызываемый метод создает свою копию значений аргументов и затем ее использует. Поскольку работа ведется с копией, на исходный параметр это никак не влияет.
Передача по ссылке (by reference). Параметры передаются как ссылка (адрес) на исходную переменную. Вызываемый метод не создает свою копию, а ссылается на исходное значение. Следовательно, изменения, сделанные в вызываемом методе, также будут отражены в исходном значении.
В Java переменные хранятся следующим образом:
Локальные переменные, такие как примитивы и ссылки на объекты, создаются в стеке.
Объекты — в куче (heap).
Теперь вернемся к основному вопросу: переменные передаются по значению или по ссылке?
Java всегда передает параметры по значению
Чтобы разобраться с этим, давайте посмотрим на пример.
Пример передачи примитивов по значению
Поскольку Java передает параметры по значению, метод processData работает с копией данных. Следовательно, в исходных данных (в методе main ) не произошло никаких изменений.
Теперь рассмотрим другой пример:
Передача объекта
Что здесь происходит? Если Java передает параметры по значению, то почему был изменен исходный список? Похоже, что Java все-таки передает параметры не по значению? Нет, неправильно. Повторяйте за мной: «Java всегда передает параметры по значению».
Чтобы с этим разобраться, давайте посмотрим на следующую диаграмму.
Память стека (stack) и кучи (heap)
В программе, приведенной выше, список fruits передается методу processData . Переменная fruitRef — это копия параметра fruit . И fruits и fruitsRef размещаются в стеке. Это две разные ссылки. Но самое интересное заключается в том, что они указывают на один и тот же объект в куче. То есть, любое изменение, которое вы вносите с помощью любой из этих ссылок, влияет на объект.
Давайте посмотрим на еще один пример:
Передача объекта по ссылке
Память стека (stack) и кучи (heap)
В этом случае для изменения ссылки fruitRef мы использовали оператор new . Теперь fruitRef указывает на новый объект, и, следовательно, любые изменения, которые вы вносите в него, не повлияют на исходный объект списка фруктов.
Итак, Java всегда передает параметры по значению. Однако мы должны быть осторожны при передаче ссылок на объекты.
Вышеприведенная концепция очень важна для правильного решения ваших задач.
Например, рассмотрим удаление узла в односвязном списке.
Удаление узла в связанном списке
Это решение работает во всех случаях, кроме одного — когда вы удаляете первый узел ( Position = 1 ). Основываясь на ранее описанной концепции, видите ли вы в чем здесь проблема? Возможно, поможет следующая диаграмма.
Удаление первого узла односвязного списка
Для исправления алгоритма необходимо сделать следующее:
В этой статье мы обсудили одну небольшую, но важную концепцию Java: передачу параметров.
Перевод статьи подготовлен в преддверии старта курса «Подготовка к сертификации Oracle Java Programmer (OCAJP)».
Подробнее о курсе и программе обучения можно узнать на бесплатном вебинаре, который пройдет 15 апреля.
ЗАПИСАТЬСЯ НА ВЕБИНАР