Почему Java обеспечивает совместимость возвращаемого типа для переопределенных статических методов?

Per этот ответ и этот ответ, статические методы Java не являются виртуальными и не могут быть переопределены. Поэтому интуитивно это должно работать (даже если в 99% случаев это опасное программирование):

class Foo
{
    public static String frob() {
        return "Foo";
    }
}

class Bar extends Foo
{
    public static Number frob() {
        return 123;
    }
}

Однако на практике это вам:

Foo.java:10: frob() in Bar cannot override frob() in Foo; attempting to use incompatible return type
found   : java.lang.Number
required: java.lang.String
    public static Number frob() {
                         ^

Наивно, кажется, что Foo.frob() и Bar.frob() не должны иметь ничего общего друг с другом; но Java настаивает на том, что они это делают. Почему?

(Nb: Я не хочу слышать, почему было бы плохой идеей кодировать этот путь, я хочу услышать, что он на Java и/или дизайн JVM делает это ограничение необходимым.)


Обновлено для добавления:. Для тех, кто думает, что компилятор запутается, вызвав статические методы в экземплярах, если вы разрешите это: это не будет. Это уже нужно понять в случае совместимости сигнатур метода:

class Foo
{
    static String frob() {
        return "Foo";
    }
}

class Bar extends Foo
{
    static String frob() {
        return "Bar";
    }
}

class Qux {
    public static void main(String[] args) {
        Foo f = new Foo();
        Foo b = new Bar();
        Bar b2 = new Bar();

        System.out.println(f.frob());
        System.out.println(b.frob());
        System.out.println(b2.frob());
    }
}

получает вас:

Foo
Foo
Bar
Вопрос: какая конкретная причина, почему это не могло так легко (в случае с несовместимыми сигнатурами) получить вас:
Foo
Foo
123

Ответ 1

Рассмотрим следующее:

public class Foo {
  static class A {
    public static void doThing() {
      System.out.println("the thing");
    }
  }

  static class B extends A {

  }

  static class C extends B {
    public static void doThing() {
      System.out.println("other thing");
    }
  }

  public static void main(String[] args) {
    A.doThing();
    B.doThing();
    C.doThing();
  }
}

Запустите его! Он компилирует и распечатывает

the thing
the thing
other thing

Статические методы вроде наследуют - в том смысле, что B.doThing преобразуется в вызов A.doThing - и может сортировать переопределение.

Это похоже на то, что это в основном призыв к суду для JLS. Наиболее конкретный способ, по мнению JLS, состоит в том, чтобы раздел 8.2, который просто не говорит о том, что статические методы не наследуются.

Ответ 2

JLS 8.4.2 Подпись метода, кратко:

Два метода имеют одну и ту же подпись, если они имеют одинаковые имена и типы аргументов.

Ничего не говорится о статичности. Статические методы могут быть вызваны через экземпляр (или нулевую ссылку) - как должен разрешаться метод, если подкласс ссылается через объявление суперкласса?

Ответ 3

Это связано с тем, что в Java конкретный метод вызывается на основе типа времени выполнения объекта, а не его типа времени компиляции. Однако статические методы являются методами класса и, следовательно, доступ к ним всегда разрешается во время компиляции только с использованием информации типа времени компиляции. То есть, что произойдет, если вы сможете скомпилировать приведенное выше и использовать код, подобный этому

Foo bar = new Bar();
bar.frob();

Ответ 4

Ну, вероятно, JVM можно было бы разрешить, но позвольте говорить о том, почему это довольно плохая идея с точки зрения компилятора.

Данные экземпляра (включая тип экземпляра) не являются чем-то, что является простой проблемой для решения во время компиляции. Очевидно, это хорошо известно во время работы. Если у меня есть переменная строка типа Bar, и я вызываю s = bar.frob(), компилятору нужно будет обработать какой-либо тип, чтобы увидеть, допустимо ли возвращаемое значение. Если определение типа во время компиляции является сложной проблемой, это делает компилятор неэффективным в лучшем случае. В худшем случае ответ неверный, и вы получаете ошибки времени выполнения, которые должны были быть пойманы во время компиляции.