Рубрики
Без рубрики

Включает ли Сигнатура метода тип возвращаемого значения в Java?

Узнайте, почему сигнатуры методов состоят из имени и списка типов параметров в Java.

Автор оригинала: Cristian Stancalau.

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 {

    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 на верхнюю границу, Сериализуемую, из-за стирания типов. Таким образом, он конфликтует с методом, явно использующим Сериализуемый .

Мы увидим тот же результат с базовым типом 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. А затем, с помощью полиморфизма, Double соответствует Числу. Идентично, Float соответствует Номеру для второго вызова.

Давайте обратим внимание на тот факт, что оба Float и Double наследуются от Числа и Объекта. Однако привязка по умолчанию-к Номеру . Это связано с тем, что Java автоматически будет соответствовать ближайшим супертипам, которые соответствуют сигнатуре метода.

Теперь рассмотрим следующий вызов метода:

obj.sum(2, "John");

В этом примере у нас есть int to Integer auto-box для первого параметра. Однако для этого имени метода нет перегрузки sum(целое число, строка) . Следовательно, Java будет проходить через все супертипы параметров для приведения от ближайшего родителя к Объекту , пока не найдет совпадение. В этом случае он привязывается к sum(Object, Object).

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

obj.sum((Object) 2, (Object) 3);
obj.sum((Number) 2, (Number) 3);

5. Параметры Vararg

Теперь давайте обратим наше внимание на то, как 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(Объект, Объект) является сигнатурой для первого. Переменные аргументы по сути являются массивами, поэтому эффективной сигнатурой для второго после компиляции является 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 .