Подтвердить что ты не робот

Есть ли причина, по которой Java использует позднюю/статическую привязку для перегруженных методов в одном классе?

Есть ли какая-то конкретная причина, по которой Java использует раннее связывание для перегруженных методов? Нельзя ли использовать для этого позднюю привязку?

Пример:

public class SomeClass {

    public void doSomething(Integer i) {
        System.out.println("INTEGER");
    }

    public void doSomething(Object o) {
        System.out.println("OBJECT");
    }

    public static void main (String[] args) {
        Object i = new Integer(2);
        Object o = new Object(); 
        SomeClass sc = new SomeClass();
        sc.doSomething(i);
        sc.doSomething(o); 
    } 
}

Отпечатки: ОБЪЕКТ ОБЪЕКТА

Я бы предпочел: INTEGER OBJECT

4b9b3361

Ответ 1

Мне кажется, что наиболее очевидная причина заключается в том, что он позволяет компилятору гарантировать, что на самом деле будет функция, которую нужно вызвать.

Предположим, что Java выбрала функцию, основанную на типе времени выполнения, и вы написали это:

public class MyClass
{
  public void foo(Integer i)
  {
    System.out.println("Integer");
  }
  public void foo(String s)
  {
    System.out.println("String");
  }
  public static void main(String[] args)
  {
    Object o1=new String("Hello world");
    foo(o1);
    Object o2=new Double(42);
    foo(o2);
  }
}

Какой результат? Первый вызов foo предположительно печатает "String", но второй вызов некуда. Я предполагаю, что это может привести к ошибке во время выполнения. Это похоже на аргумент строго типизированного или слабо типизированного. Если он выбрал функцию во время выполнения, она может быть более гибкой в ​​некотором смысле. Но, выбирая функцию во время компиляции, мы получаем сообщения об ошибках во время компиляции, вместо того, чтобы ждать времени выполнения и быть уверенным, что мы использовали каждый возможный путь с каждой соответствующей комбинацией данных.

Ответ 2

Корген, я согласен с вами в том, что такая функция потенциально может быть весьма полезной, поэтому я нашел этот вопрос очень интересным.

Быть ясным; При выполнении этого фрагмента кода:

Object i = new Integer(2);
SomeClass sc = new SomeClass();
sc.doSomething(i);

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

Integer i = new Integer(2);         // Notice the different static type.
SomeClass sc = new SomeClass();
sc.doSomething(i);

(Заметьте, что я не собираюсь делать так, как это должно быть сделано внутренне, я просто фиксирую гипотетическую семантику, желаемую в этой конкретной ситуации.)

Фон: (Korgen, вы можете пропустить это). Выбор функции для вызова иногда называется "отправка". При учете одного типа (тип o в o.m()) при выборе функции, которую вы вызываете, она называется отдельной отправкой. Мы после этого называем несколько диспетчеров, так как мы решили бы, какую функцию вызывать на основе нескольких типов (т.е. как тип времени выполнения вызываемого абонента, так и тип времени выполнения аргументы).

Возможная причина не включать поддержку множественной отправки в Java:

  • Эффективность. Отдельная диспетчеризация может быть реализована гораздо эффективнее с помощью таблицы виртуальных методов. Чтобы процитировать статью Википедии о двойной отправке:

    В языке, поддерживающем двойную отправку, это немного более дорого, потому что компилятор должен сгенерировать код для вычисления смещения метода в таблице методов во время выполнения, тем самым увеличивая общую длину пути инструкции.

  • Legacy. Это поведение на языках, которые вдохновили Java, таких как С++, Smalltalk и Eiffel. По крайней мере, единственная отправка следует принципу наименьшего удивления.

  • Сложность. Спецификация как определить, какой метод для вызова является довольно интересным. Это удивительно сложно, и то, как переложить сложность с компилятора на JVM, отнюдь не очевидно. Рассмотрим, например, следующий фрагмент:

    class A {                          .--------------.
        void foo(Object o) {}          |      A       |
    }                                  '--------------'
                                              |
    class B extends A {                .--------------.
        void foo(Integer i) {}         |      B       |
    }                                  '--------------'
                                              |
    class C extends B {                .--------------.
        void foo(Number n) {}          |      C       |
    }                                  '--------------'
    

    теперь, какой метод следует вызывать здесь:

    A c = new C();
    Object i = new Integer(0);
    c.foo(i);
    

    В соответствии с типом времени выполнения вызываемого вызова C.foo следует вызывать, тогда как в соответствии с типом времени выполнения аргумент B.foo должен быть вызван.

    Один из вариантов заключается в том, чтобы разрешить это так же, как вызов staticMethod(c, i) будет разрешен в присутствии staticMethod(A, Object), staticMethod(B, Integer) и staticMethod(C, Number). (Обратите внимание, однако, что в этом случае вызывается номер B.foo и C.foo, как было предложено выше.)

    Другим вариантом будет выбор метода, в основном основанного на типе вызываемого абонента и вторично основанного на типах аргументов, и в этом случае C.foo будет вызываться.

    Я не говорю, что невозможно определить четко определенную семантику, но я бы, возможно, сделал бы правила еще более сложными и, возможно, даже интуитивно понятными в некоторых аспектах. В случае раннего связывания, по крайней мере, компилятор и/или IDE могут помочь разработчику, предоставляя гарантии того, что на самом деле произойдет во время выполнения.

Ответ 3

Это очень просто. Используемый метод выбирается компилятором, а не системой времени выполнения. Это то, что позволяет проверять тип в компиляторе в первую очередь.

Итак, если вы заполняете Integer в Object, вы должны сообщить компилятору, что вы знаете, что он содержит Integer, поэтому можно выбрать соответствующий метод.

То, что вы хотите выполнить, обычно выполняется с помощью методов объекта, так что "this.doSomething()" делает то, что вы хотите.

Ответ 4

На самом деле это поздняя привязка, а не раннее связывание. Раннее связывание происходит только для неприменимых методов.

С учетом этого кода:

public class Test
{
        void foo()
        {
                System.out.println("foo");
        }

        final void bar()
        {
                System.out.println("bar");
        }

        void car(String s)
        {
                System.out.println("car String");
        }

        void car(Object o)
        {
                System.out.println("car Object");
        }

        static void star()
        {
                System.out.println("star");
        }

        public static void main(final String[] argv)
        {
                Test test;
                Object a;
                Object b;

                test = new Test();
                a    = "Hello";
                b    = new Object();
                test.foo();
                test.bar();
                test.car(a);
                test.car(b);
                Test.star();
        }
}

Явак, который я использовал, генерирует это для main:

public static void main(java.lang.String[]);
  Code:
   0:   new #9; //class Test
   3:   dup
   4:   invokespecial   #10; //Method "<init>":()V
   7:   astore_1
   8:   ldc #11; //String Hello
   10:  astore_2
   11:  new #12; //class java/lang/Object
   14:  dup
   15:  invokespecial   #1; //Method java/lang/Object."<init>":()V
   18:  astore_3
   19:  aload_1
   20:  invokevirtual   #13; //Method foo:()V
   23:  aload_1
   24:  invokevirtual   #14; //Method bar:()V
   27:  aload_1
   28:  aload_2
   29:  invokevirtual   #15; //Method car:(Ljava/lang/Object;)V
   32:  aload_1
   33:  aload_3
   34:  invokevirtual   #15; //Method car:(Ljava/lang/Object;)V
   37:  invokestatic    #16; //Method star:()V
   40:  return    
}

invokevirtual означает позднюю привязку, invokestatic и invokespecial означает раннее связывание.

Строка:

24: invokevirtual # 14;//Панель методов:() V

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

Итак, ваш вопрос в том, почему у java нет так называемой Multiple Dispatch (ссылка на wikipedia здесь), в которой среда исполнения решает, что метод для вызова на основе значения в переменной вместо того, чтобы основывать его на объявлении переменной.

Как работает компилятор, нужно сказать что-то вроде:

  • Я вызываю SomeClass.doSomething на объявленная как объект.
  • Есть ли у SomeClass метод, называемый doSomething, который принимает объект?
  • Если да, то выведите invokevirtual вызов этого метода.

То, что вы хотите, - это дополнительный шаг, который происходит во время выполнения (это не может произойти во время компиляции):

  • Переменная указывает на Integer.
  • Я звоню Метод SomeClass.doSomething.
  • Вызовите лучшее совпадение Метод SomeClass.doSomething, который принимает целое число, число, объект (вызов, который он найдет первым).

Java не работает во время выполнения, а просто вызывает метод, который компилятор решил вызвать.

Вы можете имитировать несколько диспетчеров в Java, например.

Ответ 5

Есть ли какая-то конкретная причина, по которой Java использует раннее связывание для перегруженных методов? Нельзя ли использовать для этого позднюю привязку?

Одна проблема с динамической привязкой перегруженных методов заключается в том, что она не будет работать, если разные перегрузки имеют разные типы возврата. Поведение в режиме исполнения будет сложнее понять, и приложение может столкнуться с новым классом исключений во время выполнения, вызванным отказом динамической перегрузки.

Вторая проблема заключается в том, что выбор метода для динамического использования на основе фактических типов аргументов будет дорогостоящим. Вы не можете реализовать это путем простой диспетчеризации метода vtable.

Кроме того, это необязательно, так как вы можете получить динамическое связывание методов с помощью переопределения/полиморфизма методов.

Ответ 6

Это возможно. И даже больше, есть такой код в стандартной библиотеке (класс - TreeSet, автор (sic!) Джош Блох).

В одной из своих лекций он говорит, что это была ошибка.

Из Джошуа Блох Как создать хороший API и почему это имеет значение

Перегрузка с заботой

  • Избегайте неоднозначных перегрузок
    • Множественные перегрузки, применимые к тем же действиям
    • Консервативный: нет двух с одинаковым количеством аргументов
  • Просто потому, что вы можете это не значит, что вы должны
    • Часто лучше использовать другое имя
  • Если вы должны предоставить неоднозначную перегрузку, убедитесь в том, что те же аргументы

    public TreeSet (коллекция c);//Игнорирует порядок

    public TreeSet (SortedSet s);//Уважает порядок

Ответ 7

void doSomething(Comparable c) {..}
void doSomething(Iterable i) {..}

class Foo implements Comparable, Iterable { ..}

doSomething(new Foo()); // which one??

Ответ 8

Вы видите OBJECT OBJECT, а не INTEGER OBJECT, потому что объявлено i как Object, а не Integer. Если вы сделаете это вместо этого:

public class SomeClass {

    public void doSomething(Integer i) {
        System.out.println("INTEGER");
    }

    public void doSomething(Object o) {
        System.out.println("OBJECT");
    }

    public static void main (String[] args) {
        Integer i = new Integer(2);
        Object o = new Object(); 
        SomeClass sc = new SomeClass();
        sc.doSomething(i);
        sc.doSomething(o); 
    } 
}

Вы получите INTEGER OBJECT.

http://ideone.com/sEZrP


Как Ответ Thorbjørn объясняется тем, что вызов метода неоднозначен во время компиляции, а не во время выполнения.

Ответ 9

Другие люди объяснили "почему" лучше, чем я мог.

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