Зачем нужна перегрузка методов
Иногда возникает необходимость создать один и тот же метод, но с разным набором параметров. И в зависимости от имеющихся параметров применять определенную версию метода. Такая возможность еще называется перегрузкой методов (method overloading).
И в языке C# мы можем создавать в классе несколько методов с одним и тем же именем, но разной сигнатурой. Что такое сигнатура? Сигнатура складывается из следующих аспектов:
- Имя метода
- Количество параметров
- Типы параметров
- Порядок параметров
- Модификаторы параметров
Но названия параметров в сигнатуру НЕ входят. Например, возьмем следующий метод:
public int Sum(int x, int y)
У данного метода сигнатура будет выглядеть так: Sum(int, int)
И перегрузка метода как раз заключается в том, что методы имеют разную сигнатуру, в которой совпадает только название метода. То есть методы должны отличаться по:
- Количеству параметров
- Типу параметров
- Порядку параметров
- Модификаторам параметров
Например, пусть у нас есть следующий класс:
class Calculator < public void Add(int a, int b) < int result = a + b; Console.WriteLine($"Result is "); > public void Add(int a, int b, int c) < int result = a + b + c; Console.WriteLine($"Result is "); > public int Add(int a, int b, int c, int d) < int result = a + b + c + d; Console.WriteLine($"Result is "); return result; > public void Add(double a, double b) < double result = a + b; Console.WriteLine($"Result is "); > >
Здесь представлены четыре разных версии метода Add, то есть определены четыре перегрузки данного метода.
Первые три версии метода отличаются по количеству параметров. Четвертая версия совпадает с первой по количеству параметров, но отличается по их типу. При этом достаточно, чтобы хотя бы один параметр отличался по типу. Поэтому это тоже допустимая перегрузка метода Add.
То есть мы можем представить сигнатуры данных методов следующим образом:
Add(int, int) Add(int, int, int) Add(int, int, int, int) Add(double, double)
После определения перегруженных версий мы можем использовать их в программе:
Calculator calc = new Calculator(); calc.Add(1, 2); // 3 calc.Add(1, 2, 3); // 6 calc.Add(1, 2, 3, 4); // 10 calc.Add(1.4, 2.5); // 3.9
Result is 3 Result is 6 Result is 10 Result is 3.9
Также перегружаемые методы могут отличаться по используемым модификаторам. Например:
void Increment(ref int val) < val++; Console.WriteLine(val); >void Increment(int val)
В данном случае обе версии метода Increment имеют одинаковый набор параметров одинакового типа, однако в первом случае параметр имеет модификатор ref. Поэтому обе версии метода будут корректными перегрузками метода Increment.
А отличие методов по возвращаемому типу или по имени параметров не является основанием для перегрузки. Например, возьмем следующий набор методов:
int Sum(int x, int y) < return x + y; >int Sum(int number1, int number2) < return number1 + number2; >void Sum(int x, int y)
Сигнатура у всех этих методов будет совпадать:
Sum(int, int)
Поэтому данный набор методов не представляет корректные перегрузки метода Sum и работать не будет .
Зачем нужна перегрузка операторов
avp, на java программируете? иногда писать MyFirstObject+=MySecondObject; удобнее, чем MyFirstObject=MyFirstObject.Add(MySecondObject); хоть реализуется оно практически одинково, даже возможно одно через другое
10 сен 2011 в 19:03
(Оффтопя) К слову, ad-hoc полимфоризм (который, в частности, используется при перегрузке операторов) не является «примочкой» ООП и прекрасно существует безотносительно этой парадигмы. ООП — это, в основном, к полимфоризму через подтипы (который, обычно, и называют просто «полиморфизмом»).
10 сен 2011 в 19:47
Сейчас еще остались не ООП языки в которых можно перегружать операции . Не помню, в АДЕ это есть ? Кстати, вообще-то я имел в виду перегружаемые пользователем операции, что к java не относится.
10 сен 2011 в 22:41
1. АДА — первый международно стандартизованный язык ООП. 2. Смотрите перегрузку в haskell.
10 сен 2011 в 23:47
3 ответа 3
Сортировка: Сброс на вариант по умолчанию
Чтобы Вы могли совершать с классами те же операции, что и со стандартными типами данных. Например:
class CMoneys < int iDol; int iCen; public: CMoneys(int Dollars, int Cents) < iDol=Dollars; iCen=Cents; >CMoneys(const CMoneys& Money); //////Some code////// CMoneys operator +(CMoneys mon) < return (iDol + Money.iDol, iCen + Money.iCen); >>
CMoneys M1(1, 2), M2(2, 3), M; M = M1 + M2; /////////////// Понимаем, как М (M1.iDol + M2.iDol, M1.iCen + M2.iCen)
То есть благодаря перегрузке операторов мы можем выполнять со своими классами те же операции, что и со встроенными типами данных. О перегрузке хорошо написано Здесь
Отслеживать
14k 34 34 серебряных знака 46 46 бронзовых знаков
ответ дан 10 сен 2011 в 17:38
912 6 6 серебряных знаков 13 13 бронзовых знаков
Stop Cclass madness!
10 сен 2011 в 23:46
Why «madness»? You not like this programming style? I always write ‘C’ before class name, such as letter ‘i’ before int variable or ‘ch’ before char variable.
11 сен 2011 в 18:59
11 сен 2011 в 19:30
Не убедительно. Если Вы хороший программист, то Вы должны знать, когда класс, имя которого начинается на ‘C’ будет конфликтовать с разработанным Вами. Тем более, что в библиотеках языка С++ я ни разу не встречал классы, конфликтующие с моими собственными. И никто ещё из моего окружения их не встречал. Так что это дело сугуб личное. Вот если компилятор будет ругаться — тогда да, тогда префикс надо будет сменить.
12 сен 2011 в 13:22
Классы в С. Кажется, я что-то пропустил, когда учил этот язык
12 сен 2011 в 13:29
В некоторых случаях перегрузка все-таки удобнее своих функций с именами, особенно если подобные операторы введены вне программирования — например действия с полиномами (надеюсь, никто не будет спорить что два полинома можно сложить, вычесть и умножить и получить новый полином), сложение интенсивностей звука, действия с комплексными числами, приведенные выше действия с суммами денег или любыми другими величинами, записанными в виде X рублей Y копеек/ X футов Y дюймов и т.п.
Отслеживать
ответ дан 11 сен 2011 в 10:46
4,152 13 13 серебряных знаков 19 19 бронзовых знаков
Желательно бы увидеть пример кода для комплексных чисел. Сталоб понятнее)
11 сен 2011 в 10:52
дай дураку стеклянный . кхм, так он и . кхм разобъёт и руки порежет. А если правильно и с умом использовать, то никаких проблем не должно возникать.
11 сен 2011 в 11:19
Но я считаю, что настоящие ф-ции должны предшествовать операторам. Т.е. если необходисо сложение, то реализуйте ф-цию add(), а затем уже начинайте думать о перегрузке операторов. При этом перегруженный оператор + легко реализовать через уже написанную и, надеюсь, отлаженную ф-цию add().
11 сен 2011 в 12:40
@avp Так я и написал в некоторых случаях, перегрузкой не стоит сильно увлекаться и пихать её всюду, что, впрочем, верно и для любого действия. В подобных творениях не просили, но написать все через пень-колоду чтобы потом не найти маленькую ошибку можно и в самой простой процедурной программе без всякого ООП.
11 сен 2011 в 13:07
@Alerr например, вот: dmtsoft.ru/bn/370/as/oneaticleshablon
11 сен 2011 в 13:14
Зачем нужна? А затем, что удобно. На самом деле, честно говоря, перегрузка операторов только запутывает.
Понятно откуда она растет — из обобщения принципа перегрузки функций, ведь перегруженные операторы — суть те же пользовательские функции. Ну, и программисты жуткие лентяи и не любят писать стопицот букв в коде программы.
Касательно того когда перегружать и зачем. Это гораздо более интересный вопрос. Могу сказать следующее: выгодно перегружать операторы = (практически обязательно, если пользовательский класс содержит указатели куда-либо и нужно корректно отрабатывать ситуацию создания копии существующего или временнего объекта класса), оператор -> (если реализуете класс с семантикой классического указателя), оператор [] (если реализуете класс массива или коллекции и есть необходимость доступа к элементу этой коллекции поиндексу), оператор () ( если реализуете концепцию функтора). К остальным перегрузкам следует относиться с осторожностью. Во-первых, сутьопераций должна быть ясна. Например, оператор + должен складывать объекты, а не вычитать или, скажем, считать налоги. Т.е. его стоит реализовывать для пользовательских строковых типов или численных типов. А вот для объектов бизнес-логики или моделирующих физические процессы использование этого оператора нерационально — становится неясно, что хотел сказать автор кода. Поэтому вызовы функций с говорящими именами — наш метод, правильный и надежный. Во-вторых, нет возможности переопределить приоритеты операторов. В третьих, не стоит плодить кучу перегрузок одного и того же оператора для случая разных комбинаций типов. Это вкупе с операторами приведения типов очень быстро приводит к бардаку. И может в некоторых случаях удивить компилятор, ну, и программиста тоже.
Java Challengers #1: Перегрузка методов в JVM
Добро пожаловать в серию статей Java Challengers! Этот серия статей посвящена особенностям программирования на Java. Их освоение — это ваш путь к становлению высококвалифицированным программистом на Java.
Освоение техник, рассматриваемых в этой серии статей требует некоторых усилий, но они будут иметь большое значение в вашем повседневном опыте в качестве java — разработчика. Избежать ошибок проще когда вы знаете как правильно применять основные техники программирования Java и отслеживать ошибки намного проще, когда вы точно знаете, что происходит в вашем java — коде.
Готовы ли вы приступить к освоению основных концепций программирования на Java? Тогда давайте начнем с нашей первой задачки!
Термин «Перегрузка методов»
Про термин перегрузка разработчики склонны думать, что речь идет о перезагрузке системы, но это не так. В программировании, перегрузка метода означает использование одинакового имени метода с разными параметрами.
Что такое перегрузка методов?
Перегрузка методов — это приём программирования, который позволяет разработчику в одном классе для методов с разными параметрами использовать одно и то же имя. В этом случае мы говорим, что метод перегружен.
В Листинге 1 показаны методы с разными параметрами, которые различаются количеством, типом и порядком.
Листинг 1. Три варианта перегрузки методов.
// Количество параметров public class Calculator < void calculate(int number1, int number2) < >void calculate(int number1, int number2, int number3) < >> // Типы параметров public class Calculator < void calculate(int number1, int number2) < >void calculate(double number1, double number2) < >> // Порядок параметров public class Calculator < void calculate(double number1, int number2) < >void calculate(int number1, double number2) < >>
Перегрузка методов и примитивные типы
В Листинге 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() позволяет использовать одно и то же имя и изменять только то, что необходимо — параметры. Также очень легко найти перегруженные методы, поскольку они сгруппированы в коде.
Чем перегрузка не является
Помните, что изменение имени переменной не является перегрузкой. Следующий код не скомпилируется:
public class Calculator < void calculate(int firstNumber, int secondNumber)<>void calculate(int secondNumber, int thirdNumber)<> >
Вы также не можете перегрузить метод, изменяя возвращаемое значение в сигнатуре метода. Этот код также не скомпилируется:
public class Calculator < double calculate(int number1, int number2)long calculate(int number1, int number2) >
Перегрузка конструктора
Вы можете перегрузить конструктор таким же способом, как и метод:
public class Calculator < private int number1; private int number2; public Calculator(int number1) < this.number1 = number1; >public Calculator(int number1, int number2) < this.number1 = number1; this.number2 = number2; >>
Решите задачку по перегрузке методов
Готовы ли вы к первому испытанию? Давайте выясним!
Начните с внимательного изучения следующего кода.
Листинг 2. Сложная задача по перегрузке методов
public class AdvancedOverloadingChallenge3 < static String x = ""; public static void main(String. doYourBest) < executeAction(1); executeAction(1.0); executeAction(Double.valueOf("5")); executeAction(1L); System.out.println(x); >static void executeAction(int . var) static void executeAction(Integer var) static void executeAction(Object var) static void executeAction(short var) static void executeAction(float var) static void executeAction(double var) >
Хорошо. Вы изучили код. Какой будет вывод?
Правильный ответ приведён в конце статьи.
Что сейчас произошло? Как JVM компилирует перегруженные методы
Для того чтобы понять что произошло в Листинге 2, вам нужно знать несколько вещей о том, как JVM компилирует перегруженные методы.
Прежде всего, JVM разумно ленива: она всегда будет прилагать наименьшие усилия для выполнения метода. Таким образом, когда вы думаете о том, как JVM обрабатывает перегрузку, имейте в виду три важных особенности компилятора:
- Расширение (widening)
- Упаковка (autoboxing and unboxing)
- Аргументы переменной длины (varargs)
Если вы никогда не сталкивались с этими техниками, то несколько примеров должны вам помочь их понять. Обратите внимание, что JVM выполняет их в том порядке, в котором они указаны.
Вот пример расширения:
int primitiveIntNumber = 5; double primitiveDoubleNumber = primitiveIntNumber ;
Это порядок расширения примитивных типов:
(Прим. переводчика — В JLS расширение примитивов описано с большими вариациями, например, long может быть расширен во float или в double.)
int primitiveIntNumber = 7; Integer wrapperIntegerNumber = primitiveIntNumber;
Обратите внимание, что происходит за кулисами при компиляции кода:
Integer wrapperIntegerNumber = Integer.valueOf(primitiveIntNumber);
А вот пример распаковки:
Integer wrapperIntegerNumber = 7; int primitiveIntNumber= wrapperIntegerNumber;
Вот что происходит за кулисами при компиляции этого кода:
int primitiveIntNumber = wrapperIntegerNumber.intValue();
И вот пример метода с аргументами переменной длины. Обратите внимание, что методы переменной длины всегда являются последними для выполнения.
execute(int. numbers)<>
Что такое аргументы переменной длины?
Аргументы переменной длины — это просто массив значений, заданный трёмя точками (. ). Мы можем передать сколько угодно чисел int этому методу.
execute(1,3,4,6,7,8,8,6,4,6,88. ); // Можно продолжать.
Аргументы переменной длины (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 — это строго типизированный язык программирования и, когда мы используем автоупаковку с обёртками, есть несколько вещей, которые мы должны учитывать. Во-первых, следующий код не компилируется:
int primitiveIntNumber = 7; Double wrapperNumber = primitiveIntNumber;
Автоупаковка будет работать только с типом double потому что, когда вы скомпилируете код, он будет эквивалентен этому:
Double number = Double.valueOf(primitiveIntNumber);
Этот код скомпилируется. Первый int будет расширен до double и потом будет упакован в Double . Но при автоупаковке нет расширения типов и конструктор Double.valueof ожидает double , а не int . В этом случае автоупаковка будет работать, если мы сделаем явное приведение типа, например:
Double wrapperNumber = (double) primitiveIntNumber;
Помните, что Integer не может быть Long и Float и не может быть Double . Здесь нет наследования. Каждый из этих типов ( Integer , Long , Float , и Double ) — Number и Object .
Если Вы сомневаетесь, просто помните, что обёртки чисел (wrapper numbers) могут быть расширены до Number или Object . (Есть еще много чего, что можно сказать про обёртки, но оставим это для другой статьи.)
Литералы чисел в коде
Когда мы не указываем тип числа-литерала, JVM вычислит тип за нас. Если напрямую используем число 1 в коде, то JVM создаст его как int . Если мы попытаемся передать 1 напрямую в метод, который принимает short , то он не скомпилируется.
class Calculator < public static void main(String. args) < // Вызов этого метода не скомпилируется // Да, может быть char, short, byte, но JVM создает его как int calculate(1); >void calculate(short number) <> >
Такое же правило будет применяться, когда используется число 1.0 . Хотя это может быть и float , JVM будет считать его double .
class Calculator < public static void main(String. args) < // Вызов этого метода не скомпилируется // Да, может быть float, но JVM создает его как double calculate(1.0); >void calculate(float number) <> >
Другой распространенной ошибкой является предположение, что Double или любая другая обертка лучше подойдет для метода, получающего double .
Факт в том, что JVM требуется меньше усилий для расширения обертки Double в Object вместо её распаковки в примитивный тип double .
Подводя итог, при использовании непосредственно в java-коде, 1 будет int и 1.0 будет double . Расширение — это самый лёгкий путь к выполнению, далее идёт упаковка или распаковка и последней операцией всегда будут методы переменной длины.
Как любопытный факт. Знаете ли вы, что тип char принимает числа?
char anyChar = 127; // Да, это странно, но это компилируется
Что необходимо помнить о перегрузке
Перегрузка — это очень мощная техника для случаев, когда вам нужно одинаковое имя метода с разными параметрами. Это полезная техника, потому что использование правильных имён делает код более удобным для чтения. Вместо того, чтобы дублировать имя метода и добавлять беспорядок в ваш код, вы можете просто перегрузить его.
Это позволяет сохранять код чистым и удобным для чтения, а также снижает риск того, что дублирующие методы сломают часть системы.
Что следует иметь в виду: при перегрузке метода JVM сделает наименьшее усилие из возможных.
Вот порядок самого ленивого пути к исполнению:
- Первое — расширение (widening)
- Второе — упаковка (boxing)
- Третье — аргументы переменной длины (varargs)
Что следует учитывать: сложные ситуации возникают при объявлении чисел напрямую: 1 будет int и 1.0 будет double .
Также помните, что вы можете объявить эти типы явно, используя синтаксис 1F или 1f для float и 1D или 1d для double .
На этом мы закончим о роли JVM в перегрузке методов. Важно понимать, что JVM по своей сути ленива, и всегда будет следовать по самому ленивому пути.
Ответ
Ответ к Листингу 2 — Вариант 3. efce.
Подробнее о перегрузке методов в Java
Введение в классы и объекты для абсолютных новичков, включая небольшие разделы о методах и перегрузке методов.
Узнайте больше о том, почему важно, что Java является строго типизированным языком и изучите примитивные типы Java.
Изучите ограничения и недостатки перегрузки методов, а также способы их устранения путем использования пользовательских типов и объектов параметров.
- Блог компании OTUS
- Программирование
- Java
Перегрузка методов в Java
Представим, что у нас есть такой код, в котором есть два метода с одинаковым названием:
private String name ;
private String poroda ;
private int age ;
public Dog ( String x ) <
public Dog ( String x , String y , int z ) <
Как Вы видите, тут есть два конструктора (если впервые слышите слово «конструктор», Вам следует почитать статью «Конструкторы в Java»). У них разные параметры, но называются они одинаково. У Вас выдает ошибку?
Нет.
Почему?
Дело в том, что у Java есть одна особенность — она позволяет производить перегрузку методов. Перегрузка методов — это возможность создавать несколько методов с одинаковым названием, но разными параметрами. Не все языки программирования позволяют это делать. Перегрузка методов — это часть такой составляющей ООП, как полиморфизм. (Если Вы не знаете, что такое ООП и полиморфизм — вернитесь к этой статье).
Обратите внимание: если Вы захотите создать два метода с одинаковым названием, которые будут требовать одни и те же параметры, возникнет ошибка .
Зачем?
Вот, есть у меня в программе один метод print или один конструктор Dog. Зачем мне еще один?
Дело в том, что перегрузка методов выполняет несколько функций:
1. Вы можете создать несколько конструкторов — с одним, двумя параметрами, и вообще без параметров. Программа будет выбирать тот, который подходит под заданные параметры. Например: