В чем разница между компоновкой во время компиляции и компоновкой во время выполнения?

Я сейчас читаю книгу и застрял на следующем коде:

public class TestAnimals {
    public static void main (String [] args ) {
        Animal a = new Animal();
        Animal b = new Horse();

        a.eat(); // Runs the Animal version of eat()
        b.eat(); // Runs the Horse version of eat()

    }
}

class Animal {
    public void eat() {
        System.out.println("Generic animal eating generically");
    }
}

class Horse extends Animal {
    public void eat() {
        System.out.println("Horse eating hay, oats, horse treats");
    }

    public void buck() {
    }
}

Обратите внимание на закомментированные строки.

В книге говорится: «Повторюсь, компилятор смотрит только на ссылочный тип, а не на тип экземпляра». Действительно? Если бы это было так, и a.eat(), и b.eat() дали бы одинаковый результат, поскольку они (a и b) имеют один и тот же ссылочный тип (это Animal).

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


person Sandeep Gupta    schedule 13.01.2016    source источник
comment
Возможное дублирование http://stackoverflow.com/questions/2055840/difference-between-load-time-dynamic-linking-and-run-time-dynamic-linking   -  person Abdelhak    schedule 13.01.2016
comment
Возможный дубликат Зависимости времени компиляции от времени выполнения - Java   -  person Zia    schedule 13.01.2016
comment
//RESULT комментарии в книге тоже? В таком случае текст противоречит комментариям. Теперь, если бы методы были static, тогда я действительно считаю, что оба вызова будут выполнять superclass method   -  person Miserable Variable    schedule 13.01.2016
comment
@MiserableVariable Обновлено для точных комментариев к книге. Да, это мой вопрос.   -  person Sandeep Gupta    schedule 13.01.2016
comment
Я думаю, двусмысленность заключается в том, что компилятор ищет. 2h3n i5 смотрит только на ссылочный тип. Как объясняет @Theodoros в своем ответе, книга имеет в виду проверку совместимости вызовов, которая действительно выполняется с помощью ссылочного типа. В дальнейшем это может помочь вам взглянуть на результат javap, чтобы класс понял.   -  person Miserable Variable    schedule 13.01.2016


Ответы (3)


Компилятор действительно смотрит только на статически известный тип, а не на фактический тип среды выполнения экземпляра - в конце концов, Java - это статически типизированный язык. Фактически, во всех случаях, кроме самых тривиальных, компилятор даже не может знать тип времени выполнения ссылки на объект (чтобы решить эту проблему для общего случая, ему пришлось бы решить неразрешимые проблемы).

Суть книги заключается в том, что этот фрагмент не может быть скомпилирован:

b.buck();

Поскольку b относится к типу (времени компиляции) Animal, а Animal не имеет метода buck(). Другими словами, Java (как и C ++) проверяет во время компиляции, имеет ли вызов метода какой-либо смысл, на основе имеющейся информации о типе переменной.

Теперь причина, по которой результаты книги соответствуют привязке времени выполнения, заключается именно в том, что у вас есть привязка времени выполнения в этом месте вызова: в Java (в отличие от C ++) все нестатические методы по умолчанию являются виртуальными.

Таким образом, нет необходимости в ключевом слове virtual, которое позволило бы вам явно выбрать семантику полиморфизма (как, например, в C ++ и C #). Вместо этого вы можете предотвратить любые дальнейшие переопределения ваших методов, только индивидуально пометив их как final или пометив их содержащий класс как final (если последнее применимо в вашем случае).

person Theodoros Chatzigiannakis    schedule 13.01.2016
comment
Строго говоря, final не делает метод не виртуальным, а скорее предотвращает дальнейшие переопределения. Метод может быть переопределен и объявлен окончательным, и тогда дальнейшие переопределения невозможны, но все предыдущие переопределения вплоть до последнего будут работать так же нормально. С таким же успехом можно было бы сказать, что вы можете отказаться от виртуальности, создав метод final, но опять же, это другое. В Java просто нет невиртуальных методов. - person Sergei Tachenov; 13.01.2016
comment
в Java все нестатические методы по умолчанию являются виртуальными. Благодарим за ответ. Я только что перешел с C ++ и поэтому столкнулся с такими проблемами. Означает ли это, что в Java нет ничего, что называется компоновкой во время компиляции? - person Sandeep Gupta; 13.01.2016
comment
@SandeepGupta Насколько мне известно, вызовы static методов разрешаются статически. См. Вспомогательные классы, такие как Math. Хотя я лично назвал бы это статическим связыванием, поскольку я резервирую термин связывание для связывания библиотек (в смысле C и C ++). - person Theodoros Chatzigiannakis; 13.01.2016
comment
Я действительно извиняюсь, что не спросил об этом в своем предыдущем комментарии. Если в Java все нестатические методы являются виртуальными по умолчанию, почему в книгах сказано: «Повторюсь, компилятор смотрит только на ссылочный тип, а не на тип экземпляра?» Разве это утверждение не является привязкой времени компиляции? Кажется, есть противоречие. - person Sandeep Gupta; 13.01.2016
comment
@SandeepGupta Я немного отредактировал ответ, чтобы включить его. По сути, Java по-прежнему будет выдавать вам ошибки времени компиляции (как это сделал бы C ++), когда у вас есть ссылочная переменная и вы пытаетесь вызвать для нее метод, которого нет у ссылочного типа (даже если он есть у типа времени выполнения). Это то, что имеет в виду книга, когда говорит, что компилятор смотрит на ссылочный тип. Это означает, что проверка ошибок происходит с учетом ссылочного типа. После проверки (во время компиляции) того, что сам вызов имеет смысл, фактический выбор правильного переопределения - единственное, что остается на усмотрение среды выполнения. - person Theodoros Chatzigiannakis; 13.01.2016
comment
@TheodorosChatzigiannakis Теперь это имеет смысл. Привязка времени компиляции для проверки ошибок и времени выполнения для последующей части (поскольку все нестатические методы по умолчанию являются виртуальными). Я бы проголосовал за ваш ответ, но для этого вам потребуются минимальные очки. В любом случае, спасибо вам большое. - person Sandeep Gupta; 13.01.2016
comment
@SandeepGupta Не проблема! Я могу только предложить вам рассмотреть возможность пометить его как принятый, если вы считаете, что он полностью отвечает на ваш вопрос. - person Theodoros Chatzigiannakis; 13.01.2016
comment
@SandeepGupta, я бы также добавил, что привязка времени компиляции влияет не только на проверку ошибок, но также на определение типа и разрешение перегрузки. Если существует несколько методов с разными сигнатурами, тогда существует довольно загадочный алгоритм, который выбирает метод для вызова (или сообщает об ошибке, если вызов неоднозначен), а также использует привязку времени компиляции. - person Sergei Tachenov; 14.01.2016

@Sandeep - по поводу вашего последнего комментария (на момент написания) ...

Если в Java все нестатические методы являются виртуальными по умолчанию, почему в книгах говорится: «Повторюсь, компилятор смотрит только на ссылочный тип, а не на тип экземпляра»? Разве это утверждение не является привязкой времени компиляции?

Думаю, книга немного неполная ...

Под «типом ссылки» в книге говорится о том, как объявляется данная переменная; мы можем назвать это классом переменной. Одна вещь, которая поможет вам перейти от C ++, - это рассматривать всю Java как переменные как указатели на конкретный экземпляр (кроме примитивных типов, таких как 'int'). Достаточно легко сказать, что все в Java - это «передача по значению», но поскольку переменные всегда являются указателями, это значение указателя, которое помещается в стек при каждом вызове метода ... сами экземпляры объекта остаются в том же самом разместить в куче.


Это то, что я писал изначально, прежде чем заметил комментарий ...

Идеи «время компиляции» и «время выполнения» не так полезны (для меня) для прогнозирования поведения.

Я говорю это, потому что более полезный вопрос (для меня) - «Как мне узнать, какой метод будет вызван во время выполнения?»

И под «Откуда мне знать» я имею в виду «Как мне предсказать»?

Методы экземпляра Java управляются тем, чем на самом деле является экземпляр (виртуальные функции в C ++). Экземпляр класса Horse всегда будет экземпляром Horse. Ниже приведены три разные переменные («ссылочные типы», если использовать формулировку из книг), которые относятся к одному и тому же экземпляру Horse.

Horse  x = new Horse();
Animal y = x;
Object z = x;

Методы класса Java (в основном любой метод со статическим перед ним) менее интуитивно понятны и в значительной степени ограничены точным классом, на который они ссылаются в исходном коде, что означает «привязка во время компиляции».

Обратите внимание на результаты теста (ниже) при чтении следующего:

Я добавил еще одну переменную в ваш класс TestAnimals и немного поиграл с форматированием ... В main () теперь у нас есть 3 переменные:

  Animal a = new Animal();
  Animal b = new Horse();
  Horse  c = new Horse(); // 'c' is a new variable.

Я немного изменил вывод eat ().
Я также добавил метод класса xyz () как в Animal, так и в Horse.

Из распечаток видно, что все это разные экземпляры. На моем компьютере «a» указывает на Animal @ 42847574 (ваш скажет Animal @ some_number, фактическое число будет варьироваться от одного запуска к другому).

'a' points to Animal@42847574
'b' points to Horse@63b34ca.
'c' points to Horse@1906bcf8.

Итак, в начале main () у нас есть один экземпляр Animal и два разных экземпляра Horse.

Большая разница, которую следует наблюдать, заключается в том, как ведет себя .eat () и как ведет себя .xyz (). Методы экземпляра, такие как .eat (), обращают внимание на класс экземпляра. Не имеет значения, в каком классе находится переменная, указывающая на экземпляр.

С другой стороны, методы класса всегда следуют за объявленной переменной. В приведенном ниже примере, хотя Animal 'b' относится к экземпляру Horse, b.xyz () вызывает Animal.xyz (), а не Horse.xyz ().

Сравните это с Horse 'c', что заставляет c.xyz () вызывать метод Horse.xyz ().

Это сводило меня с ума, когда я изучал Java; по моему скромному мнению, это был дешевый способ сохранить поиск метода во время выполнения. (И, честно говоря, в середине 1990-х, когда создавалась Java, возможно, было важно использовать такие сокращения производительности).

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

a = c;
Now a and c point to same instance: 
Animal a=Horse@1906bcf8
Horse  c=Horse@1906bcf8

Рассмотрим поведение животного «а» и лошади «с» после этого. Методы экземпляра по-прежнему делают то, что на самом деле является экземпляром. Методы класса по-прежнему следуют, однако переменная объявляется.

=== начать пример запуска TestAnimals ===

$ ls
Animal.java  Horse.java  TestAnimals.java
$ javac *.java
$ java TestAnimals
Animal a=Animal@42847574
Animal b=Horse@63b34ca
Horse  c=Horse@1906bcf8
calling a.eat(): Hello from Animal.eat()
calling b.eat(): Hello from Horse.eat()
calling c.eat(): Hello from Horse.eat()
calling a.xyz(): Hello from Animal.xyz()
calling b.xyz(): Hello from Animal.xyz()
calling c.xyz(): Hello from Horse.xyz()
Now a and c point to same instance: 
Animal a=Horse@1906bcf8
Horse  c=Horse@1906bcf8
calling a.eat(): Hello from Horse.eat()
calling c.eat(): Hello from Horse.eat()
calling a.xyz(): Hello from Animal.xyz()
calling c.xyz(): Hello from Horse.xyz()
$ 

=== конец примера запуска TestAnimals ===

public class TestAnimals {
   public static void main( String [] args ) {
      Animal a = new Animal( );
      Animal b = new Horse( );
      Horse  c = new Horse( );
      System.out.println("Animal a="+a);
      System.out.println("Animal b="+b);
      System.out.println("Horse  c="+c);
      System.out.print("calling a.eat(): "); a.eat();
      System.out.print("calling b.eat(): "); b.eat();
      System.out.print("calling c.eat(): "); c.eat();
      System.out.print("calling a.xyz(): "); a.xyz();
      System.out.print("calling b.xyz(): "); b.xyz();
      System.out.print("calling c.xyz(): "); c.xyz();
      a=c;
      System.out.println("Now a and c point to same instance: ");
      System.out.println("Animal a="+a);
      System.out.println("Horse  c="+c);
      System.out.print("calling a.eat(): "); a.eat();
      System.out.print("calling c.eat(): "); c.eat();
      System.out.print("calling a.xyz(): "); a.xyz();
      System.out.print("calling c.xyz(): "); c.xyz();

   }
}

public class Animal {
   public void eat() {
      System.out.println("Hello from Animal.eat()");
   }

   static public void xyz() {
      System.out.println("Hello from Animal.xyz()");
   }
}


class Horse extends Animal {
   public void eat() {
      System.out.println("Hello from Horse.eat()");
   }

   static public void xyz() {
      System.out.println("Hello from Horse.xyz()");
   }
}
person jgreve    schedule 13.01.2016

Этот вопрос можно перефразировать как разницу между статической привязкой и динамической привязкой.

  1. Статическая привязка разрешается во время компиляции, а динамическая привязка разрешается во время выполнения.
  2. Статическая привязка использует type of "Class" (reference согласно вашему примеру), а динамическая привязка использует type of "Object" (instance согласно вашему примеру). private, final, static методы разрешаются во время компиляции.

  3. Метод overloadingis an example ofStatic binding&Method overridingis example ofDynamic binding`.

В вашем примере

Animal b = new Horse();
b.eat();

разрешение объекта, для которого должен быть вызван метод "eat()", происходит во время выполнения для Animal b. Во время выполнения Animal b был преобразован в тип Horse, и была вызвана версия Horse метода eat ().

Прочтите эту статью для лучшего понимания.

Взгляните на связанный вопрос SE: Полиморфизм против переопределения против перегрузки

person Ravindra babu    schedule 13.01.2016