В сегодняшней статье речь пойдет об одном из самых интересных языковых средств Java — перегрузке методов класса. Несколько методов одного класса могут иметь одно и то же имя, отличаясь лишь набором параметров.
Перегрузка методов является одним из способов реализации принципа полиморфизма в Java.
Для того чтобы перегрузить метод, достаточно объявить его новый вариант, отличающийся от уже существующих, а все остальное сделает компилятор. Нужно лишь соблюсти одно условие: тип и/или число параметров в каждом из перегружаемых методов должны быть разными.
Одного лишь различия в типах возвращаемых значений для этой цели недостаточно. (Информации о возвращаемом типе не всегда будет хватать Java для принятия решения о том, какой именно метод должен использоваться.)
Конечно, перегружаемые методы могут иметь разные возвращаемые типы, но при вызове метода выполняется лишь тот его вариант, в котором параметры соответствуют передаваемым аргументам.
Ниже приведен простой пример программы, демонстрирующий перегрузку методов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
// Перезагрузка методов class Overload { // Первая версия void ovlDemo() { System.out.println("Без параметров"); } // Перезагрузка метода ovlDemo для одного параметра типа int // Вторая версия void ovlDemo(int a) { System.out.println("Один параметр: " + a); } // Перезагрузка метода ovlDemo для двух параметров типа int // Третья версия int ovlDemo(int a, int b) { System.out.println("Два параметра: " + a + " " + b); return a + b; } // Перезагрузка метода ovlDemo для двух параметров типа double // Четвертая версия double ovlDemo(double a, double b) { System.out.println("Два параметра типа double: " + a + " " + b); return a + b; } } class OverloadDemo { public static void main(String args[]) { Overload ob = new Overload(); int resI; double resD; // Поочередно вызвать все версии метода ovlDemo() ob.ovlDemo(); System.out.println(); ob.ovlDemo(2); System.out.println(); resI = ob.ovlDemo(4, 6); System.out.println("Результат вызова ob.ovlDemo(4, 6): " + resI); System.out.println(); resD = ob.ovlDemo(1.1, 2.32); System.out.println("Результат вызова ob.ovlDemo(1.1, 2.32): " + resD); } } |
Выполнение этой программы дает следующий результат:
1 2 3 4 5 6 7 8 9 |
Без параметров Один параметр: 2 Два параметра: 4 6 Результат вызова ob.ovlDemo(4, 6): 10 Два параметра типа double: 1.1 2.32 Результат вызова ob.ovlDemo(1.1, 2.32): 3.42 |
Как видите, метод ovlDemo() перегружается четырежды. В первой его версии параметры не предусмотрены, во второй — определен один целочисленный параметр, в третьей — два целочисленных параметра, в четвертой — два параметра типа double.
Обратите внимание на то, что первые два варианта метода ovlDemo() имеют тип void, а два другие возвращают значение. Тип возвращаемого значения не учитывается при перегрузке методов. Следовательно, попытка определить два варианта метода ovlDemo() так, как показано ниже, приводит к ошибке.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* Возможен лишь один вариант метода ovlDemo(int) Возвращаемое значение нельзя использовать для различения перезагружаемых методов */ void ovlDemo(int a) { System.out.println("One parameter: " + a); } /* Ошибка! Невозможно существование двух версий перезагруженного метода ovlDemo(int), отличающихся лишь типом возвращаемого значения. */ int ovlDemo(int a) { System.out.println("Один параметр: " + a); return a * a * a; } |
Как поясняется в комментариях к приведенному выше фрагменту кода, отличия возвращаемых типов недостаточно для перегрузки методов.
Как мы знаем, в Java применяется автоматическое приведение типов. Это приведение распространяется и на типы параметров перегружаемых методов. В качестве примера рассмотрим следующий фрагмент кода.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
/* Автоматическое преобразование типов может влиять на выбор перезагружаемого метода. */ class Overload2 { void f(int x) { System.out.println("Внутри f(int): " + x); } void f(double x) { System.out.println("Внутри f(double): " + x); } } class TypeConv { public static void main(String args[]) { Overload2 ob = new Overload2(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i); // вызов метода ob.f(int) ob.f(d); // вызов метода ob.f(double) ob.f(b); // вызов метода ob.f(int) с преобразованием типов ob.f(s); // вызов метода ob.f(int) с преобразованием типов ob.f(f); // вызов метода ob.f(double) с преобразованием типов } } |
Выполнение этого фрагмента кода дает следующий результат:
1 2 3 4 5 |
Внутри f(int): 10 Внутри f(double): 10.1 Внутри f(int): 99 Внутри f(int): 10 Внутри f(double): 11.5 |
В данном примере определены только два варианта метода f(): один принимает параметр типа int, а второй — параметр типа double.
Но передать методу f() можно также значение типа byte, short и float. Значения типа byte и short исполняющая система Java автоматически преобразует в тип int. В результате будет вызван вариант метода f(int). А если параметр имеет значение типа float, то оно преобразуется в тип doube, и далее вызывается вариант метода f(double).
Важно понимать, что автоматическое преобразование типов выполняется лишь в отсутствие прямого соответствия типов параметра и аргумента. В качестве примера ниже представлена другая версия предыдущей программы, в которой добавлен вариант метода f() с параметром типа byte.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
// Добавление версии метода f(byte) class Overload3 { void f(byte x) { System.out.println("Внутри f(byte): " + x); } void f(int x) { System.out.println("Внутри f(int): " + x); } void f(double x) { System.out.println("Внутри f(double): " + x); } } class TypeConv { public static void main(String args[]) { Overload3 ob = new Overload3(); int i = 10; double d = 10.1; byte b = 99; short s = 10; float f = 11.5F; ob.f(i); // вызов метода ob.f(int) ob.f(d); // вызов метода ob.f(double) ob.f(b); // вызов метода ob.f(byte) без преобразования типов ob.f(s); // вызов метода ob.f(int) с преобразованием типов ob.f(f); // вызов метода ob.f(double) с преобразованием типов } } |
Выполнение этой версии программы дает следующий результат.
1 2 3 4 5 |
Внутри f(int): 10 Внутри f(double): 10.1 Внутри f(byte): 99 Внутри f(int): 10 Внутри f(double): 11.5 |
Поскольку в данной программе предусмотрена версия метода f(), которая принимает параметр типа byte, то при вызове этого метода с аргументом типа byte выполняется вызов f (byte), и автоматического преобразования типа byte в тип int не происходит.
Перегрузка методов поддерживает полиморфизм, поскольку она является одним из способов реализации парадигмы «один интерфейс — множество методов». Для того чтобы стало понятнее, как и для чего это делается, необходимо принять во внимание следующее соображение: в языках программирования, не поддерживающих перегрузку методов, каждый метод должен иметь уникальное имя.
Но в ряде случаев требуется выполнять одну и ту же последовательность операций над разными типами данных. В качестве примера рассмотрим функцию, определяющую абсолютное значение.
В языках, не поддерживающих перегрузку методов, приходится создавать несколько вариантов данной функции с именами, отличающимися хотя бы одним символом.
Например,в языке С функция abs() возвращает абсолютное значение числа типа int, функция labs() — абсолютное значение числа типа long, а функция fabs() — абсолютное значение числа с плавающей точкой.
Объясняется это тем, что в С не поддерживается перегрузка, и поэтому каждая из функций должна обладать своим собственным именем,несмотря на то что все они выполняют одинаковые действия. Это приводит к неоправданному усложнению процесса написания программ.
Разработчику приходится не только представлять себе действия, выполняемые функциями, но и помнить все три их имени. Такая ситуация не возникает в Java, потому что все методы, вычисляющие абсолютное значение, имеют одно и то же имя.
В стандартной библиотеке Java для вычисления абсолютного значения предусмотрен метод abs(). Его перегрузка осуществляется в классе Math для обработки значений всех числовых типов.
Решение о том, какой именно вариант метода abs() должен быть вызван, исполняющая система Java принимает, исходя из типа аргумента.
Главная ценность перегрузки заключается в том, что она обеспечивает доступ к группе родственных методов по общему имени. Следовательно, имя abs обозначает общее выполняемое действие, а компилятор сам выбирает конкретный вариант метода по обстоятельствам.
Благодаря полиморфизму несколько имен сводятся к одному. Несмотря на всю простоту рассматриваемого здесь примера, продемонстрированный в нем принцип полиморфизма можно расширить, чтобы выяснить, каким образом перегрузка помогает справляться с более сложными ситуациями в программировании.
Когда метод перегружается, каждая его версия может выполнять какое угодно действие. Для установления взаимосвязи перегружаемых методов не существует какого-то твердого правила, но с точки зрения правильного стиля программирования перегрузка методов подразумевает подобную взаимосвязь. Следовательно, использовать одно и тоже имя для несвязанных друг с другом методов не следует, хотя это и возможно.
Например, имя sqr можно было бы выбрать для методов, возвращающих квадрат и квадратный корень числа с плавающей точкой. Но ведь это принципиально разные операции.
Такое применение перегрузки методов противоречит ее первоначальному назначению.На практике перегружать следует только тесно связанные операции.
Стоит отметить, что, программисты на Java часто употребляют термин сигнатура. Что это такое?
Применительно к языку Java сигнатура обозначает имя метода и список его параметров. При перегрузке методов действует следующее правило: никакие два метода из одного класса не могут иметь одинаковые сигнатуры.
Следует, однако, иметь в виду, что сигнатура не включает в себя тип возвращаемого значения, поскольку он не используется в Java при принятии решения о перегрузке.