1. Обзор
Сигнатура метода является лишь подмножеством всего определения метода в Java. Таким образом, точная анатомия подписи может вызвать путаницу.
В этом руководстве мы изучим элементы сигнатуры метода и их применение в программировании на Java.
2. Подпись метода
Методы в Java поддерживают перегрузку, что означает, что несколько методов с одним и тем же именем могут быть определены в одном и том же классе или иерархии классов. Следовательно, компилятор должен иметь возможность статически связать метод, на который ссылается клиентский код. По этой причине сигнатура метода однозначно идентифицирует каждый метод .
Согласно Oracle , сигнатура метода состоит из имени и типов параметров . Поэтому все остальные элементы объявления метода, такие как модификаторы, возвращаемый тип, имена параметров, список исключений и тело, не являются частью подписи.
Давайте подробнее рассмотрим перегрузку методов и ее связь с сигнатурами методов.
3. Ошибки перегрузки
Рассмотрим следующий код :
public void print() {
System.out.println("Signature is: print()");
}
public void print(int parameter) {
System.out.println("Signature is: print(int)");
}
Как мы видим, код компилируется, так как методы имеют разные списки типов параметров. По сути, компилятор может детерминировано связать любой вызов с одним или другим.
Теперь давайте проверим, допустима ли перегрузка, добавив следующий метод:
public int print() {
System.out.println("Signature is: print()");
return 0;
}
Когда мы компилируем, мы получаем ошибку «метод уже определен в классе». Это доказывает, что возвращаемый тип метода не является частью сигнатуры метода .
Попробуем то же самое с модификаторами:
private final void print() {
System.out.println("Signature is: print()");
}
Мы по-прежнему видим ту же ошибку «метод уже определен в классе». Поэтому сигнатура метода не зависит от модификаторов .
Перегрузку путем изменения выброшенных исключений можно протестировать, добавив:
public void print() throws IllegalStateException {
System.out.println("Signature is: print()");
throw new IllegalStateException();
}
Снова мы видим ошибку «метод уже определен в классе», указывающую на то, что объявление throw не может быть частью подписи .
Последнее, что мы можем проверить, — позволяет ли изменение имен параметров перегрузку. Добавим следующий метод:
public void print(int anotherParameter) {
System.out.println("Signature is: print(int)");
}
Как и ожидалось, мы получаем ту же ошибку компиляции. Это означает, что имена параметров не влияют на сигнатуру метода .
3. Общие сведения и стирание типов
При использовании универсальных параметров стирание типа изменяет действующую сигнатуру . По сути, это может привести к конфликту с другим методом, который использует верхнюю границу универсального типа вместо универсального токена.
Рассмотрим следующий код:
public class OverloadingErrors<T extends Serializable> {
public void printElement(T t) {
System.out.println("Signature is: printElement(T)");
}
public void printElement(Serializable o) {
System.out.println("Signature is: printElement(Serializable)");
}
}
Несмотря на то, что сигнатуры кажутся разными, компилятор не может статически связать правильный метод после стирания типа.
Мы видим, как компилятор заменяет T
на верхнюю границу Serializable
из-за стирания типа. Таким образом, он конфликтует с методом, явно использующим Serializable
.
Мы увидим тот же результат с базовым типом Object
, когда общий тип не имеет границ.
4. Списки параметров и полиморфизм
Сигнатура метода учитывает точные типы. Это означает, что мы можем перегрузить метод, тип параметра которого является подклассом или суперклассом.
Однако мы должны обратить особое внимание, поскольку статическая привязка будет пытаться сопоставляться с использованием полиморфизма, автоупаковки и продвижения типов .
Давайте посмотрим на следующий код:
public Number sum(Integer term1, Integer term2) {
System.out.println("Adding integers");
return term1 + term2;
}
public Number sum(Number term1, Number term2) {
System.out.println("Adding numbers");
return term1.doubleValue() + term2.doubleValue();
}
public Number sum(Object term1, Object term2) {
System.out.println("Adding objects");
return term1.hashCode() + term2.hashCode();
}
Приведенный выше код совершенно легален и скомпилируется. При вызове этих методов может возникнуть путаница, поскольку нам нужно знать не только точную сигнатуру метода, который мы вызываем, но и то, как Java статически связывается на основе фактических значений.
Давайте рассмотрим несколько вызовов методов, которые в конечном итоге связаны с sum(Integer, Integer)
:
StaticBinding obj = new StaticBinding();
obj.sum(Integer.valueOf(2), Integer.valueOf(3));
obj.sum(2, 3);
obj.sum(2, 0x1);
Для первого вызова у нас есть точные типы параметров Integer, Integer.
При втором вызове Java автоматически преобразует int
в Integer
.
Наконец, Java преобразует байтовое значение 0x1
в int
посредством повышения типа, а затем автоматически упаковывает его в Integer.
Точно так же у нас есть следующие вызовы, которые связываются с sum(Number, Number)
:
obj.sum(2.0d, 3.0d);
obj.sum(Float.valueOf(2), Float.valueOf(3));
При первом вызове у нас есть двойные
значения, которые автоматически упаковываются в Double.
И тогда с помощью полиморфизма Double
соответствует Number.
Точно так же Float
соответствует Number
для второго вызова.
Заметим тот факт, что и Float
, и Double
наследуются от Number
и Object.
Однако по умолчанию используется привязка к Number
. Это связано с тем, что Java автоматически сопоставляет ближайшие супертипы, соответствующие сигнатуре метода.
Теперь рассмотрим следующий вызов метода:
obj.sum(2, "John");
В этом примере у нас есть авто-поле int
to Integer
для первого параметра. Однако для этого имени метода нет перегрузки sum(Integer, String) .
Следовательно, Java будет проходить через все супертипы параметров, чтобы привести их от ближайшего родителя к Object
, пока не найдет совпадение. В этом случае он привязывается к сумме (объект, объект).
Чтобы изменить привязку по умолчанию, мы можем использовать явное приведение параметров следующим образом:
obj.sum((Object) 2, (Object) 3);
obj.sum((Number) 2, (Number) 3);
5. Параметры варарга
Теперь давайте обратим наше внимание на то, как varargs
влияет на эффективную подпись метода и статическую привязку.
Здесь у нас есть перегруженный метод, использующий varargs
:
public Number sum(Object term1, Object term2) {
System.out.println("Adding objects");
return term1.hashCode() + term2.hashCode();
}
public Number sum(Object term1, Object... term2) {
System.out.println("Adding variable arguments: " + term2.length);
int result = term1.hashCode();
for (Object o : term2) {
result += o.hashCode();
}
return result;
}
Итак, каковы эффективные сигнатуры методов? Мы уже видели, что sum(Object, Object)
является сигнатурой для первого. Переменные аргументы по сути являются массивами, поэтому эффективной сигнатурой для второго после компиляции является sum(Object, Object[]).
Сложный вопрос: как мы можем выбрать привязку метода, когда у нас всего два параметра?
Рассмотрим следующие вызовы:
obj.sum(new Object(), new Object());
obj.sum(new Object(), new Object(), new Object());
obj.sum(new Object(), new Object[]{new Object()});
Очевидно, что первый вызов будет привязан к sum(Object, Object),
а второй — к sum(Object, Object[]).
Чтобы заставить Java вызвать второй метод с двумя объектами, мы должны обернуть его в массив, как в третьем вызове.
Последнее, на что следует обратить внимание, это то, что объявление следующего метода будет конфликтовать с версией vararg:
public Number sum(Object term1, Object[] term2) {
// ...
}
6. Заключение
В этом руководстве мы узнали, что сигнатуры методов состоят из имени и списка типов параметров. Модификаторы, тип возвращаемого значения, имена параметров и список исключений не могут различать перегруженные методы и, следовательно, не являются частью сигнатуры.
Мы также рассмотрели, как стирание типов и varargs скрывают эффективную сигнатуру метода и как мы можем переопределить привязку статического метода Java.
Как обычно, все примеры кода, показанные в этой статье, доступны на GitHub .