Зачем нужна перегрузка методов
Перейти к содержимому

Зачем нужна перегрузка методов

  • автор:

C#. Перегрузка методов в классах. Примеры. Преимущество перегрузки методов. Перегрузка конструкторов

Язык программирования C# позволяет осуществлять перегрузку методов в классе. Перегрузка означает использование одного имени для разных методов. Фактически, перегружается имя метода.

Перегрузка метода – это программная реализация нескольких методов для которых выполняются следующие условия:

  • все перегруженные методы носят одинаковое имя;
  • перегруженные методы отличаются параметрами. Точнее говоря, тип параметров или количество параметров в перегруженных методах должны отличаться.

Перегрузка метода в классе – это объявления другого метода с таким самым именем в классе но с отличаемыми параметрами. Параметры перегруженного метода должны отличаться типами или количеством.

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

2. Что такое сигнатура метода? Какие требования к сигнатуре метода с точки зрения перегрузки?

Сигнатура метода – это имя метода и список его параметров. С точки зрения перегрузки к сигнатуре метода относится следующее требование:

  • в одном классе не должно существовать двух методов с одинаковой сигнатурой.

3. Считаются ли перегруженными два метода, которые имеют одинаковые параметры но возвращают разные значения?

Нет. Это ошибка. Согласно синтаксису C# перегруженные методы обязательно должны отличаться параметрами.

4. Примеры перегруженных методов

Пример 1. В классе MaxMethods объявляется перегруженный метод Max() , имеющий 3 реализации. Метод реализует поиск максимума между разным количеством параметров типа int .

Ниже приведено использование перегруженного метода Max() класса MaxMethods

Пример 2. В классе AverageMethods объявляется метод Average(), получающий массив чисел и определяющий среднее арифметическое массива. Метод имеет 2 перегруженные реализации для типов int и float . Количество параметров в перегруженных методах Average() есть одинаковым, однако, типы параметров отличаются.

Ниже продемонстрировано использование перегруженного метода Average()

Как видно из примера, компилятор автоматически определяет метод, который нужно вызвать, по типу его параметров.

5. Какие особенности перегрузки методов, которые получают параметры с модификаторами ref и out ?

Если попробовать объявить разные реализации метода, который содержит одинаковые параметры отличающиеся между собой только модификаторами ref и out , то компилятор выдаст ошибку.

Например. Пусть нужно реализовать два метода GetPi() , которые возвращают число p типа double. При нижеследующем объявлении этих методов в некотором классе

и попытке компиляции, компилятор выдаст сообщение об ошибке

Это означает, что модификаторы ref и out не изменяют общую сигнатуру метода.

6. Какие преимущества дает перегрузка методов?

Перегрузка методов дает следующие преимущества:

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

7. Перегрузка конструкторов в классе. Примеры

Конструкторы классов могут перегружаться точно также как и методы.

Пример 1. Задан класс CPoint , который реализует точку на координатной плоскости. В классе объявляется два перегруженных конструктора.

Ниже продемонстрировано использование перегруженных конструкторов класса

Пример 2. Задан класс Circle , реализующий окружность на координатной плоскости заданного радиуса. В классе реализованы 4 конструктора, которые получают разное количество параметров. Программный код класса следующий

Ниже продемонстрировано использование перегруженных конструкторов класса

8. Каким образом осуществляется вызов перегруженного конструктора с использованием ключевого слова this ? Общая форма

Язык C# позволяет вызвать один конструктор из другого в случае, когда в классе есть несколько перегруженных реализаций конструкторов. Чтобы из заданного конструктора вызвать другой перегруженный конструктор применяется ключевое слово this . Общая форма такого вызова следующая

  • constructor_name – имя конструктора, который осуществляет вызов перегруженного конструктора;
  • parameters1 – параметры конструктора, который осуществляет вызов перегруженного конструктора. Этот конструктор вызывается вторым после вызова конструктора, обозначенного ключевым словом this ;
  • parameters2 – параметры перегруженного конструктора, который обозначен ключевым словом this . Этот конструктор вызывается первым.

9. Пример вызова перегруженного конструктора с использованием ключевого слова this

Пример. Реализация класса CPixel , который описывает пиксел на экране монитора. Пиксел характеризуется следующими параметрами:

  • целочисленные координаты x , y ;
  • цвет ( color ).

Класс содержит 2 перегруженных конструктора. В классе продемонстрирован вызов перегруженного конструктора.

Вызов перегруженного конструктора осуществляется с помощью кода

Конструктор с 3 параметрами вызывает конструктор с 1 параметром. При таком вызове параметр, который есть в вызывающем конструкторе, должен быть обязательно в вызываемом конструкторе (конструкторе this ). В нашем случае, обязательным есть параметр _color .

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

И еще одна новая и интересная тема на сегодня — перегрузка методов . Или как ее еще часто называют, overloading (переопределение — это overriding).

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

Дело в том, что все методы внутри класса должны иметь уникальные имена. Так вот, это не совсем так. Вернее, совсем не так. Метод не должен иметь уникальное имя. Уникальным должно быть объединение имени и типов параметров этого метода. Такие объединения еще называют сигнатурами методов

Код Описание
Так можно. Два метода имеют уникальные имена.
И так можно. Два метода имеют уникальные имена (сигнатуры).
Все еще уникальные методы
А так нельзя . Методы — не уникальные . Хоть и возвращают разные типы.
А так — можно . Параметры методов уникальные .

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

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

Согласно принципам ООП , полиморфизм — это сокрытие различных реализаций за одним интерфейсом. А т.к. при вызове метода System.out.println() , например, в зависимости от параметров вызываются различные методы, формально это подпадает под определение полиморфизма.

Именно поэтому разные методы с одинаковыми именами, находящиеся в одном классе, принято считать хоть и слабой, но все же формой полиморфизма.

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.

  1. Создайте новый файл Queue. j ava.
  2. Добавьте к массиву q, а также к переменным putloc и getloc модификатор доступа private в классе Queue. В результате код этого класса должен выглядеть так, как показано ниже.

  1. Теперь, когда массив 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. Как видите, процесс рекурсивен по своей сути, поэтому рекурсивный способ лучше всего подходит для реализации быстрой сортировки.

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

  1. Создайте новый файл QSDemo. j ava.
  2. Создайте сначала класс 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 Challengers #1: Перегрузка методов в JVM

Добро пожаловать в серию статей Java Challengers! Этот серия статей посвящена особенностям программирования на Java. Их освоение — это ваш путь к становлению высококвалифицированным программистом на Java.

Освоение техник, рассматриваемых в этой серии статей требует некоторых усилий, но они будут иметь большое значение в вашем повседневном опыте в качестве java — разработчика. Избежать ошибок проще когда вы знаете как правильно применять основные техники программирования Java и отслеживать ошибки намного проще, когда вы точно знаете, что происходит в вашем java — коде.

Готовы ли вы приступить к освоению основных концепций программирования на Java? Тогда давайте начнем с нашей первой задачки!

widening-boxing-varargs

Термин «Перегрузка методов»

Про термин перегрузка разработчики склонны думать, что речь идет о перезагрузке системы, но это не так. В программировании, перегрузка метода означает использование одинакового имени метода с разными параметрами.

Что такое перегрузка методов?

Перегрузка методов — это приём программирования, который позволяет разработчику в одном классе для методов с разными параметрами использовать одно и то же имя. В этом случае мы говорим, что метод перегружен.

В Листинге 1 показаны методы с разными параметрами, которые различаются количеством, типом и порядком.

Листинг 1. Три варианта перегрузки методов.

Перегрузка методов и примитивные типы

В Листинге 1 вы видели примитивные типы int и double . Давайте отвлечёмся на минуту и вспомним примитивные типы в Java.

Таблица 1. Примитивные типы в Java

Тип Диапазон Значение по умолчанию Размер Примеры литералов
boolean true или false false 1 бит true, false
byte -128… 127 0 8 бит 1, -90, -128
char Символ юникода или от 0 до 65 536 \u0000 16 бит ‘a’, ‘\u0031’, ‘\201’, ‘\n’, 4
short -32,768… 32,767 0 16 бит 1, 3, 720, 22,000
int -2 147 483 648… 2 147 483 647 0 32 бит -2, -1, 0, 1, 9
long -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 0 64 бит -4000L, -900L, 10L, 700L
float 3.40282347 x 1038, 1.40239846 x 10-45 0.0 32 бит 1.67e200f, -1.57e-207f, .9f, 10.4F
double 1.7976931348623157 x 10308, 4.9406564584124654 x 10-324 0.0 64 бит 1.e700d, -123457e, 37e1d

Зачем мне использовать перегрузку методов?

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

В противоположность Листингу 1 представьте программу, где у вас будет много методов calculate() с именами похожими на calculate1 , calculate2 , calculate3 … не хорошо, правда? Перегрузка метода calculate() позволяет использовать одно и то же имя и изменять только то, что необходимо — параметры. Также очень легко найти перегруженные методы, поскольку они сгруппированы в коде.

Чем перегрузка не является

Помните, что изменение имени переменной не является перегрузкой. Следующий код не скомпилируется:

Вы также не можете перегрузить метод, изменяя возвращаемое значение в сигнатуре метода. Этот код также не скомпилируется:

Перегрузка конструктора

Вы можете перегрузить конструктор таким же способом, как и метод:

Решите задачку по перегрузке методов

Готовы ли вы к первому испытанию? Давайте выясним!

Начните с внимательного изучения следующего кода.

Листинг 2. Сложная задача по перегрузке методов

Хорошо. Вы изучили код. Какой будет вывод?

  1. befe
  2. bfce
  3. efce
  4. aecf

Правильный ответ приведён в конце статьи.

Что сейчас произошло? Как JVM компилирует перегруженные методы

Для того чтобы понять что произошло в Листинге 2, вам нужно знать несколько вещей о том, как JVM компилирует перегруженные методы.

Прежде всего, JVM разумно ленива: она всегда будет прилагать наименьшие усилия для выполнения метода. Таким образом, когда вы думаете о том, как JVM обрабатывает перегрузку, имейте в виду три важных особенности компилятора:

  1. Расширение (widening)
  2. Упаковка (autoboxing and unboxing)
  3. Аргументы переменной длины (varargs)

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

Вот пример расширения:

Это порядок расширения примитивных типов:

Порядок расширения примитивных типов

(Прим. переводчика — В JLS расширение примитивов описано с большими вариациями, например, long может быть расширен во float или в double.)

Обратите внимание, что происходит за кулисами при компиляции кода:

А вот пример распаковки:

Вот что происходит за кулисами при компиляции этого кода:

И вот пример метода с аргументами переменной длины. Обратите внимание, что методы переменной длины всегда являются последними для выполнения.

Что такое аргументы переменной длины?

Аргументы переменной длины — это просто массив значений, заданный трёмя точками (. ). Мы можем передать сколько угодно чисел int этому методу.

Аргументы переменной длины (varargs) очень удобны тем, что значения могут передаваться непосредственно в метод. Если бы мы использовали массивы, нам пришлось бы создать экземпляр массива со значениями.

Расширение: практический пример

Когда мы передаем число 1 прямо в метод executeAction() , JVM автоматически интерпретирует его как int . Вот почему это число не будет передано в метод executeAction(short var) .

Аналогично, если мы передаём число 1.0 JVM автоматически распознает, что это double.

Конечно число 1.0 также может быть и float , но тип таких литералов предопредопределен. Поэтому в Листинге 2 выполняется метод executeAction(double var) .

Когда мы используем обёртку Double , есть два варианта: либо число может быть распаковано в примитивный тип, либо оно может быть расширено в Object . (Помните, что каждый класс в Java расширяет класс Object .) В этом случае JVM выбирает расширение типа Double в Object , потому что это требует меньше усилий, чем распаковка.

Последним мы передаём 1L и так как, мы указали тип — это long .

Распространенные ошибки с перегрузкой

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

Автоупаковка с обёртками (autoboxing with wrappers)

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

Автоупаковка будет работать только с типом double потому что, когда вы скомпилируете код, он будет эквивалентен этому:

Этот код скомпилируется. Первый int будет расширен до double и потом будет упакован в Double . Но при автоупаковке нет расширения типов и конструктор Double.valueof ожидает double , а не int . В этом случае автоупаковка будет работать, если мы сделаем явное приведение типа, например:

Помните, что Integer не может быть Long и Float и не может быть Double . Здесь нет наследования. Каждый из этих типов ( Integer , Long , Float , и Double ) — Number и Object .

Если Вы сомневаетесь, просто помните, что обёртки чисел (wrapper numbers) могут быть расширены до Number или Object . (Есть еще много чего, что можно сказать про обёртки, но оставим это для другой статьи.)

Литералы чисел в коде

Когда мы не указываем тип числа-литерала, JVM вычислит тип за нас. Если напрямую используем число 1 в коде, то JVM создаст его как int . Если мы попытаемся передать 1 напрямую в метод, который принимает short , то он не скомпилируется.

Такое же правило будет применяться, когда используется число 1.0 . Хотя это может быть и float , JVM будет считать его double .

Другой распространенной ошибкой является предположение, что Double или любая другая обертка лучше подойдет для метода, получающего double .

Факт в том, что JVM требуется меньше усилий для расширения обертки Double в Object вместо её распаковки в примитивный тип double .

Подводя итог, при использовании непосредственно в java-коде, 1 будет int и 1.0 будет double . Расширение — это самый лёгкий путь к выполнению, далее идёт упаковка или распаковка и последней операцией всегда будут методы переменной длины.

Как любопытный факт. Знаете ли вы, что тип char принимает числа?

Что необходимо помнить о перегрузке

Перегрузка — это очень мощная техника для случаев, когда вам нужно одинаковое имя метода с разными параметрами. Это полезная техника, потому что использование правильных имён делает код более удобным для чтения. Вместо того, чтобы дублировать имя метода и добавлять беспорядок в ваш код, вы можете просто перегрузить его.

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

Что следует иметь в виду: при перегрузке метода JVM сделает наименьшее усилие из возможных.

Вот порядок самого ленивого пути к исполнению:

  • Первое — расширение (widening)
  • Второе — упаковка (boxing)
  • Третье — аргументы переменной длины (varargs)

Что следует учитывать: сложные ситуации возникают при объявлении чисел напрямую: 1 будет int и 1.0 будет double .

Также помните, что вы можете объявить эти типы явно, используя синтаксис 1F или 1f для float и 1D или 1d для double .

На этом мы закончим о роли JVM в перегрузке методов. Важно понимать, что JVM по своей сути ленива, и всегда будет следовать по самому ленивому пути.

Ответ

Ответ к Листингу 2 — Вариант 3. efce.

Подробнее о перегрузке методов в Java

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

Узнайте больше о том, почему важно, что Java является строго типизированным языком и изучите примитивные типы Java.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *