Невозможно воспроизвести результат примера Type Erasure

Я читаю раздел 8.4 "Java Generics and Collections". Автор определяет следующий код, пытаясь объяснить двоичную совместимость:

interface Name extends Comparable {
    public int compareTo(Object o);
}
class SimpleName implements Name {
    private String base;
    public SimpleName(String base) {
        this.base = base;
    }
    public int compareTo(Object o) {
        return base.compareTo(((SimpleName)o).base);
    }
}
class ExtendedName extends SimpleName {
    private String ext;
    public ExtendedName(String base, String ext) {
        super(base); this.ext = ext;
    }
    public int compareTo(Object o) {
        int c = super.compareTo(o);
        if (c == 0 && o instanceof ExtendedName)
        return ext.compareTo(((ExtendedName)o).ext);
        else
        return c;
    }
}
class Client {
    public static void main(String[] args) {
        Name m = new ExtendedName("a","b");
        Name n = new ExtendedName("a","c");
        assert m.compareTo(n) < 0;
    }
}

а затем говорит о том, чтобы сделать интерфейс Name и класс SimpleName универсальным и оставить ExtendedName как есть. В результате новый код:

interface Name extends Comparable<Name> {
    public int compareTo(Name o);
}
class SimpleName implements Name {
    private String base;
    public SimpleName(String base) {
        this.base = base;
    }
    public int compareTo(Name o) {
        return base.compareTo(((SimpleName)o).base);
    }
}
// use legacy class file for ExtendedName
class Test {
    public static void main(String[] args) {
        Name m = new ExtendedName("a","b");
        Name n = new ExtendedName("a","c");
        assert m.compareTo(n) == 0; // answer is now different!
    }
}

Автор описывает результат такого действия следующим образом:

Скажем, мы генерируем Name и SimpleName так, чтобы они определяли compareTo(Name), но у нас нет источника для ExtendedName. Поскольку он определяет только compareTo(Object), клиентский код, который вызывает compareTo(Name), а не compareTo(Object), вызовет метод для SimpleName (где он определен), а не для ExtendedName (где он не определен), поэтому базовые имена будут сравниваться, но расширения игнорируются.

Однако, когда я делаю только имя и простое имя общим, я получаю ошибку времени компиляции, а не то, что автор описывает выше. Ошибка:

конфликт имен: compareTo(Object) в NameHalfMovedToGenerics.ExtendedName и compareTo(T) в Comparable имеют одно и то же стирание, но ни один из них не переопределяет другой

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

Я ошибся в понимании того, что автор пытается сказать?

Любая помощь будет высоко ценится.

Заранее спасибо.


person Mustafa    schedule 26.11.2013    source источник
comment
Опубликуйте код, в котором вы делаете общими только Name и SimpleName.   -  person PM 77-1    schedule 27.11.2013
comment
У меня уже есть - во втором блоке кода выше. Вы просите что-то еще? 1. Comparable преобразуется в Comparable‹Name› и 2. В классе SimpleName метод compareTo(Object) становится compareTo(Name).   -  person Mustafa    schedule 27.11.2013
comment
Вы сделали источник для ExtendedName недоступным, как подразумевал автор?   -  person Michael Butscher    schedule 27.11.2013
comment
Нет, я этого не сделал. У меня есть Name, SimpleName и ExtendedName как внутренние классы моего основного класса. То есть вы имеете в виду, что я просто сохраняю файл .class для ExtendedName и переписываю универсальные эквиваленты для остальных классов, верно?   -  person Mustafa    schedule 27.11.2013
comment
@Mustafa Да, это то, что имел в виду автор.   -  person Michael Butscher    schedule 27.11.2013
comment
@Майкл Спасибо. Позвольте мне попробовать это. Я мог бы узнать что-то новое после вашего объяснения. Скоро опубликую свой результат.   -  person Mustafa    schedule 27.11.2013
comment
@Mustafa Может быть, вы также должны сделать это для класса Test, содержащего main()   -  person Michael Butscher    schedule 27.11.2013
comment
@Майкл, я пытаюсь сделать то, что ты сказал, и мне трудно. Я разберусь, что нужно сделать, чтобы добиться этого, и вернусь. Прямо сейчас я скопировал файлы классов во временный каталог, удалил исходные файлы для ExtendedName и класса Test и скопировал их старые файлы классов в место сборки и попытался запустить класс Test через командную строку, и это не удалось. В любом случае я собираюсь добавить +1 к вашему предложению, так как я полностью пропустил эту часть.   -  person Mustafa    schedule 27.11.2013
comment
@Micheal - мне удалось удалить исходные файлы, как вы сказали, и снова запустить программу. И, к моему удивлению, compareTo(Object) из ExtendedName все еще вызывается, и я получаю тот же результат, что и устаревшая версия. Я совсем не уверен, как это сделать. Я пытаюсь понять это сейчас.   -  person Mustafa    schedule 27.11.2013
comment
Возможно, книга относится к другой версии виртуальной машины Java, и ваша ведет себя по-другому (но на самом деле я в этом сомневаюсь)   -  person Michael Butscher    schedule 27.11.2013


Ответы (1)


Это пример проблемы, которая может возникнуть при отдельной компиляции.

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

В примере Name и SimpleName были изменены и перекомпилированы, но старый, скомпилированный двоичный файл ExtendedName все еще используется. Вот что на самом деле означает фраза «исходный код для ExtendedName недоступен». Когда программа компилируется с измененной иерархией классов, она записывает другую информацию, чем если бы она была скомпилирована с использованием старой иерархии.

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

В пустом каталоге я создал два подкаталога v1 и v2. В v1 я поместил классы из первого примера кода в отдельные файлы Name.java, SimpleName.java и ExtendedName.java.

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

Кроме того, я переименовал основную программу в Test1.java и изменил ее следующим образом:

class Test1 {
    public static void main(String[] args) {
        Name m = new ExtendedName("a","b");
        Name n = new ExtendedName("a","c");
        System.out.println(m.compareTo(n));
    }
}

В v1 я все скомпилировал и запустил Test1:

$ ls
ExtendedName.java  Name.java  SimpleName.java  Test1.java
$ java -version
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)
$ javac *.java
$ java Test1
-1

Теперь в v2 я поместил файлы Name.java и SimpleName.java, модифицированные с помощью дженериков, как показано во втором примере блока кода. Я также скопировал v1/Test1.java в v2/Test2.java и соответственно переименовал класс, но в остальном код тот же.

$ ls
Name.java  SimpleName.java  Test2.java
$ javac -cp ../v1 *.java
$ java -cp .:../v1 Test2
0

Это показывает, что результат m.compareTo(n) отличается после изменения Name и SimpleName при использовании старого двоичного файла ExtendedName. Что случилось?

Мы можем увидеть разницу, взглянув на дизассемблированный вывод класса Test1 (скомпилированного для старых классов) и класса Test2 (скомпилированного для новых классов), чтобы увидеть, какой байт-код генерируется для вызова m.compareTo(n). Еще в v2:

$ javap -c -cp ../v1 Test1
...
29: invokeinterface #8,  2     // InterfaceMethod Name.compareTo:(Ljava/lang/Object;)I
...

$ javap -c Test2
...
29: invokeinterface #8,  2     // InterfaceMethod Name.compareTo:(LName;)I
...

При компиляции Test1 информация, скопированная в файл Test1.class, представляет собой вызов compareTo(Object), потому что на данный момент этот метод используется интерфейсом Name. С измененными классами компиляция Test2 приводит к байт-коду, который вызывает compareTo(Name), так как это то, что теперь имеет измененный интерфейс Name. Когда Test2 запускается, он ищет метод compareTo(Name) и таким образом обходит метод compareTo(Object) в классе ExtendedName, вместо этого вызывая SimpleName.compareTo(Name). Вот почему поведение отличается.

Обратите внимание, что поведение старого двоичного файла Test1 не изменилось:

$ java -cp .:../v1 Test1
-1

Но если Test1.java перекомпилировать с учетом новой иерархии классов, его поведение изменится. По сути, это то же самое, что и Test2.java, но с другим именем, чтобы мы могли легко увидеть разницу между запуском старого двоичного файла и перекомпилированной версии.

person Stuart Marks    schedule 03.12.2013
comment
Я бы пошел в горы, отказавшись от всего остального, чтобы изучать java, если бы кто-то сказал мне, что я могу быть таким, как ты. Спасибо чувак, ты классный. Я успешно воспроизвел проблему, теперь мне интересно, должен ли я попытаться понять мелкие детали всего, что вы сказали выше, но я перейду к некоторым другим более простым темам, которых я не знаю, и вернусь к этому немного позже. Но ты сделал дело, мужик! Спасибо - person Mustafa; 05.12.2013
comment
Вы мне льстите. Правда я не специалист. (Вспомните, например, ребят, написавших эту книгу.) Но спасибо, я рад, что помог. Обязательно учитесь понемногу каждый день и продолжайте в том же духе в течение длительного времени. - person Stuart Marks; 05.12.2013
comment
Я сделаю все возможное. Спасибо за этот совет. - person Mustafa; 05.12.2013