Что такое перегрузка методов
Иногда возникает необходимость создать один и тот же метод, но с разным набором параметров. И в зависимости от имеющихся параметров применять определенную версию метода. Такая возможность еще называется перегрузкой методов (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 и работать не будет .
Перегрузка методов
В C# допускается совместное использование одного и того же имени двумя или более методами одного и того же класса, при условии, что их параметры объявляются по-разному. В этом случае говорят, что методы перегружаются, а сам процесс называется . Перегрузка методов относится к одному из способов реализации полиморфизма в C#.
В общем, для перегрузки метода достаточно объявить разные его варианты, а об остальном позаботится компилятор. Но при этом необходимо соблюсти следующее важное условие: тип или число параметров у каждого метода должны быть разными.
Совершенно недостаточно, чтобы два метода отличались только типами возвращаемых значений. Они должны также отличаться типами или числом своих параметров. (Во всяком случае, типы возвращаемых значений дают недостаточно сведений компилятору C#, чтобы решить, какой именно метод следует использовать.) Разумеется, перегружаемые методы могут отличаться и типами возвращаемых значений. Когда вызывается перегружаемый метод, то выполняется тот его вариант, параметры которого соответствуют (по типу и числу) передаваемым аргументам.
Давайте рассмотрим пример использования перегрузки методов:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 < class UserInfo < // Перегружаем метод ui public void ui() < Console.WriteLine("Пустой метод\n"); >public void ui(string Name) < Console.WriteLine("Имя пользователя: ",Name); > public void ui(string Name, string Family) < Console.WriteLine("Имя пользователя: \nФамилия пользователя: ",Name,Family); > public void ui(string Name, string Family, byte Age) < Console.WriteLine("Имя пользователя: \nФамилия пользователя: \nВозраст: ", Name, Family, Age); > > class Program < static void Main(string[] args) < UserInfo user1 = new UserInfo(); // Разные реализации вызова перегружаемого метода user1.ui(); user1.ui("Ерохин", "Александр", 26); Console.ReadLine(); >> >
Как видите метод ui перегружается три раза. Модификаторы параметров ref и out также учитываются, когда принимается решение о перегрузке метода. Несмотря на то что модификаторы параметров ref и out учитываются, когда принимается решение о перегрузке метода, отличие между ними не столь существенно. Давайте добавим еще одну перегрузку в вышеуказанный пример:
// Используем модификатор параметров public void ui(string Name, string Family, ref byte Age)
Перегрузка методов поддерживает свойство полиморфизма, поскольку именно таким способом в C# реализуется главный принцип полиморфизма: один интерфейс — множество методов. Для того чтобы стало понятнее, как это делается, обратимся к конкретному примеру. В языках программирования, не поддерживающих перегрузку методов, каждому методу должно быть присвоено уникальное имя. Но в программировании зачастую возникает потребность реализовать по сути один и тот же метод для обработки разных типов данных.
Допустим, что требуется функция, определяющая абсолютное значение. В языках, не поддерживающих перегрузку методов, обычно приходится создавать три или более вариантов такой функции с несколько отличающимися, но все же разными именами. Например, в С функция abs() возвращает абсолютное значение целого числа, функция labs() — абсолютное значение длинного целого числа, а функция fabs () — абсолютное значение числа с плавающей точкой обычной (одинарной) точности.
В С перегрузка не поддерживается, и поэтому у каждой функции должно быть свое, особое имя, несмотря на то, что все упомянутые выше функции, по существу, делают одно и то же — определяют абсолютное значение. Но это принципиально усложняет положение, поскольку приходится помнить имена всех трех функций, хотя они реализованы по одному и тому же основному принципу. Подобные затруднения в C# не возникают, поскольку каждому методу, определяющему абсолютное значение, может быть присвоено одно и то же имя. И действительно, в состав библиотеки классов для среды .NET Framework входит метод Abs(), который перегружается в классе System.Math для обработки данных разных числовых типов. Компилятор C# сам определяет, какой именно вариант метода Abs() следует вызывать, исходя из типа передаваемого аргумента.
В C# определено понятие сигнатуры, обозначающее имя метода и список его параметров; Применительно к перегрузке это понятие означает, что в одном классе не должно существовать двух методов с одной и той же сигнатурой. Следует подчеркнуть, что в сигнатуру не входит тип возвращаемого значения, поскольку он не учитывается, когда компилятор C# принимает решение о перегрузке метода. В сигнатуру не входит также модификатор params.
Чтобы закрепить понятие перегрузки методов, давайте рассмотрим перегрузку встроенного метода IndexOf класса String пространства имен System:
using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 < class Program < static void Main(string[] args) < string s = "Всем привет, это сайт professorweb.ru :)"; char ch = 'е'; string smile = ":)"; Console.WriteLine("Исходная строка: \n\n----------------------\n",s); // Первая перегрузка if (s.IndexOf(ch) != -1) Console.WriteLine("Символ '' находится на позиции ",ch,s.IndexOf(ch)); // Вторая перегрузка if (s.IndexOf(ch, s.IndexOf(ch)+1) != -1) Console.WriteLine("Далее, этот символ встречается на позиции ", s.IndexOf(ch, s.IndexOf(ch) + 1)); // Третья перегрузка if (s.IndexOf(smile, 0, s.Length) != -1) Console.WriteLine("Смайл найден на позиции ", smile, s.IndexOf(smile, 0, s.Length)); // Четвертая перегрузка if (s.IndexOf(smile, StringComparison.Ordinal) != -1) Console.WriteLine("Теперь смайл найден другим способом"); Console.ReadLine(); > > >
В данном примере используется только часть доступных перегрузок метода IndexOf, если бы C# не поддерживал перегрузки, то пришлось бы присваивать каждому методу свое имя, что конечно же очень неудобно. В данном случае метод IndexOf реализует несколько перегрузок, для поиска символов и подстрок в исходной строке.
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
Узнайте о перегрузке методов в Java, мощном инструменте для создания чистого и краткого кода, с нашим подробным руководством для новичков!
Алексей Кодов
Автор статьи
9 июня 2023 в 16:31
Перегрузка методов (method overloading) – это возможность создания в классе нескольких методов с одинаковым именем, но с разными параметрами. В Java, перегрузка методов позволяет упростить код и увеличить его читаемость, так как одно имя метода может выполнять разные функции, в зависимости от переданных аргументов.
Как работает перегрузка методов
Рассмотрим пример класса Calculator , который будет выполнять сложение двух и трех чисел:
public class Calculator < public int add(int a, int b) < return a + b; >public int add(int a, int b, int c) < return a + b + c; >>
В данном случае, у нас есть два метода add с одинаковым именем, но разными параметрами. Java определяет, какой метод вызвать на основе количества и типа переданных аргументов.
public class Main < public static void main(String[] args) < Calculator calc = new Calculator(); System.out.println("Сумма двух чисел: " + calc.add(2, 3)); // Выводит 5 System.out.println("Сумма трех чисел: " + calc.add(2, 3, 4)); // Выводит 9 >>
Java-разработчик: новая работа через 11 месяцев
Получится, даже если у вас нет опыта в IT
Правила перегрузки методов
Существует несколько принципов, которые нужно учитывать при перегрузке методов:
- Имя метода должно быть одинаковым – перегружаемые методы должны иметь одно и то же имя, чтобы Java знала, что они являются вариантами одного метода.
- Параметры должны быть разными – перегружаемые методы должны иметь разные параметры, такие как разное количество аргументов, разные типы данных или разный порядок аргументов.
- Возвращаемый тип метода не имеет значения – перегрузка методов не зависит от возвращаемого типа, поэтому два метода с одинаковым именем и параметрами, но разными возвращаемыми типами, не будут считаться перегруженными.
- Область видимости метода не имеет значения – перегрузка методов не зависит от модификаторов доступа (public, private, protected). Два метода с одинаковым именем и параметрами, но разными модификаторами доступа, не будут считаться перегруженными.
Заключение
Перегрузка методов является мощным инструментом в Java, позволяющим создавать более чистый и краткий код. Она позволяет использовать одно имя метода для выполнения различных действий в зависимости от переданных параметров, упрощая тем самым чтение и написание кода.