Java, связывание статических методов и обобщенные методы объединены с некоторой перегрузкой методов - PullRequest
9 голосов
/ 11 июля 2011

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

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

import java.util.*;

public class GenericTestsClean 
{
    public static void test2()
    {
        BigCage<Animal> animalCage=new BigCage<Animal>();
        BigCage<Dog> dogCage=new BigCage<Dog>();
        dogCage.add(new Dog());
        animalCage.add(new Cat());
        animalCage.add(new Dog());
        animalCage.printList(dogCage);
        animalCage.printList(animalCage);
    }


    public static void main(String [] args)
    {
        //What will this print
        System.out.println("\nTest 2");
        test2();
    }

}

class BigCage<T> extends Cage<T>
{

    public static <U extends Dog> void printList(List<U> list)
    {
        System.out.println("*************"+list.getClass().toString());
        for(Object obj : list)
            System.out.println("BigCage: "+obj.getClass().toString());
    }

}
class Cage<T> extends ArrayList<T>
{
    public static void printList(List<?> list)
    {
        System.out.println("*************"+list.getClass().toString());
        for(Object obj : list)
            System.out.println("Cage: "+obj.getClass().toString());
    }
}

class Animal
{
}
class Dog extends Animal
{
}
class Cat extends Animal
{
}

Теперь меня смущает то, что это прекрасно компилируется с javac 1.6.0_26 , но когда я запускаю его, я получаю следующее исключение приведения класса:

Test 2
*************class BigCage
BigCage: class Dog
*************class BigCage
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
        at BigCage.printList(GenericTestsClean.java:31)
        at GenericTestsClean.test2(GenericTestsClean.java:13)
        at GenericTestsClean.main(GenericTestsClean.java:21)

Здесь нужно отметить несколько вещей:

  1. Два printList НЕ переопределяют, но перегружают друг друга, как и ожидалось (они имеют разные типы, потому что универсальные типы их аргументов различны).Это можно проверить с помощью аннотации @Override
  2. Изменение метода void printList(List<?>) в классе Cage на нестатическое приводит к соответствующей ошибке времени компиляции
  3. Изменениеметод void <U extends Dog> printList(List<U>) в классе BigCage до void <U> printList(List<U>) генерирует соответствующую ошибку.
  4. В main () вызывает printList () через класс BigCage (т.е. BigCage.printList (...)) генерирует ту же ошибку времени выполнения
  5. В main () , вызывая printList () через класс Cage (т.е. Cage.printList (...)) работает как положено, вызывая только версию printList в Cage
  6. Если я скопирую определение для printList(List<?>) в класс BigCage из класс Cage , который скроет определение в класс Cage , я получу соответствующую ошибку компилятора

Теперь, если бы мне пришлось сделать снимок в темноте относительно того, что здесь происходит, я бы сказал, что компилятор испортит, потому чтоПо сути, он работает в несколько этапов: Проверка типа и Перегрузка Разрешение метода .На этапе проверки типа мы проходим через строку, вызывающую ошибку, потому что класс BigCage унаследовал void printList(List<?>) от class Cage, что будет соответствовать любому старому списку, который мы к нему добавляем, поэтому убедитесь, что у нас есть метод, который будет работать.Однако, как только приходит время решить с помощью метода фактического вызова, у нас возникает проблема из-за стирания типа, из-за которой у BigCage.printList и Cage.printList одинаковая подпись.Это означает, что когда компилятор ищет совпадение для animalCage.printList(animalCage);, он выберет первый метод, с которым он сопоставляется (и если мы предположим, что он начинается снизу с BigCage и работает, почему до Object), он сначала найдет void <U extends Dog> printList(List<U>)о правильном соответствии void printList(List<?>)

Теперь мой реальный вопрос : Насколько я близок к истине?Это известная ошибка?Это ошибка вообще?Я знаю, как обойти эту проблему, это скорее академический вопрос.

** РЕДАКТИРОВАТЬ **

Как мало кто писал ниже,этот код будет работать в Eclipse.Мой конкретный вопрос касается версии javac 1.6.0_26.Кроме того, я не уверен, полностью ли я согласен с Eclipse в этом случае, даже если он работает, потому что добавление printList(List<?>) к BigCage приведет к ошибке времени компиляции в Eclipse, и я не могусм. причину, по которой он должен работать, когда тот же метод наследуется, стихи добавляются вручную (см. Примечание 6 выше).

Ответы [ 4 ]

8 голосов
/ 11 июля 2011

Рассмотрим эту тривиальную проблему:

class A
{
    static void foo(){ }
}
class B extends A
{
    static void foo(){ }
}
void test()
{
    A.foo();
    B.foo();
}

Предположим, мы удалили метод foo из B и перекомпилируем только сам B, что может произойти, если мы запустим test()?Должен ли он выбросить ошибку связи, потому что B.foo() не найден?

Согласно JLS3 # 13.4.12 удаление B.foo не нарушает бинарную совместимость, потому что A.foo все еще определен.Это означает, что при выполнении B.foo() вызывается A.foo().Помните, что нет перекомпиляции test(), поэтому эта пересылка должна обрабатываться JVM.

И наоборот, давайте удалим метод foo из B и перекомпилируем все.Хотя компилятор знает статически, что B.foo() на самом деле означает A.foo(), он все равно генерирует B.foo() в байт-коде.На данный момент JVM будет пересылать B.foo() на A.foo().Но если в будущем B получит новый метод foo, новый метод будет вызываться во время выполнения, даже если test() не перекомпилируется.

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

В вашем примере, когда компилятор видит BigCage.printList(animalCage), он правильно выведетчто это на самом деле звонит Cage.printList(List<?>).Поэтому необходимо скомпилировать вызов в байт-код как BigCage.printList(List<?>) - целевой класс должен быть здесь BigCage вместо Cage.

Упс!Формат байт-кода не был обновлен для обработки сигнатуры метода.Общая информация сохраняется в байт-коде как вспомогательная информация, но для вызова метода это старый способ.

Стирание происходит.Вызов фактически скомпилирован в BigCage.printList(List).Жаль, BigCage также имеет printList(List) после стирания.Во время выполнения этот метод вызывается!

Эта проблема возникает из-за несоответствия между спецификацией Java и спецификацией JVM.

Java 7 немного напрягается;Понимая, что байт-код и JVM не могут справиться с такими ситуациями, он больше не компилирует ваш код:

ошибка: конфликт имен: printList (Список) в BigCage и printList (Список) в Cage имеют одинаковое стирание,но ни один из них не скрывает

Еще один забавный факт: если два метода имеют разные типы возврата, ваша программа будет работать правильно.Это связано с тем, что в байтовом коде сигнатура метода включает тип возвращаемого значения.Таким образом, нет никакой путаницы между Dog printList(List) и Object printList(List).См. Также Стирание и перегрузка типов в Java: почему это работает? Этот прием разрешен только в Java 6. Java 7 запрещает его, возможно, по причинам, отличным от технических.

2 голосов
/ 11 июля 2011

Это самая простая версия этого кода с той же проблемой:

import java.util.*;

public class GenericTestsClean {
    public static void main(String[] args) {
        List<Animal> animalCage = new ArrayList<Animal>();
        animalCage.add(new Cat());
        animalCage.add(new Dog());
        BigCage.printList(animalCage);
    }
}

class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}

class BigCage extends Cage {
    public static <U extends Dog> void printList(List<U> list) {
        System.out.println("BigCage#printList");
        for (Object obj : list) {
            System.out.println("BigCage: " + obj.getClass().toString());
        }
    }
}

class Cage {
    public static void printList(List list) {
        System.out.println("Cage#printList");
        for (Object obj : list) {
            System.out.println("Cage: " + obj.getClass().toString());
        }
    }
}

Я думаю, что компилятор должен вернуть ошибку:

    GenericTestsClean.java:8: <U extends Dog>printList(java.util.List<U>) in BigCage cannot be applied to (java.util.List<Animal>)
        BigCage.printList(animalCage);
               ^
1 error

(или о конфликте имен с той же ошибкой), но это не так.
После разборки (javap -c GenericTestsClean) мы получили:

invokestatic    #9; //Method BigCage.printList:(Ljava/util/List;)V

Звонок java GenericTestsClean:

Javac 1.6.0_10 версия

BigCage#printList
Exception in thread "main" java.lang.ClassCastException: Cat cannot be cast to Dog
        at BigCage.printList(GenericTestsClean.java:19)
        at GenericTestsClean.main(GenericTestsClean.java:8)

Версия компилятора Eclipse

BigCage#printList
BigCage: class Cat
BigCage: class Dog

ИМХО оба эти результата неверны.

2 голосов
/ 11 июля 2011

Это не ошибка.Метод статический.Вы не можете переопределить статические методы, вы только скрываете их.

Когда вы вызываете "printList" в bigCage, вы действительно вызываете printList для класса BigCage, а не для объекта, который всегда будет вызывать статический метод, объявленный в классе BigCage.

1 голос
/ 11 июля 2011

ИМХО этот код может быть неверным. Метод printList в классе BigCage должен вызывать конфликт имен, поскольку printList в Cage имеет такое же стирание, но ни один не переопределяет другой. Странно, что компилятор компилирует его :)

Полученный байт-код (javac 1.6.0_10) эквивалентен этому:

class BigCage extends Cage {

    public static void printList(List list){
        System.out.println((new StringBuilder()).append("*************").append(list.getClass().toString()).toString());
        Dog dog;
        for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println((new StringBuilder()).append("BigCage: ").append(dog.getClass().toString()).toString()))
            dog = (Dog)iterator.next();
    }
}

Приведение в цикле вызывает исключение . Встроенный компилятор Eclipse генерирует такой код (который работает без исключения):

class BigCage extends Cage{

    public static void printList(List list){
        System.out.println((new StringBuilder("*************")).append(list.getClass().toString()).toString());
        Object obj;
        for(Iterator iterator = list.iterator(); iterator.hasNext(); System.out.println((new StringBuilder("BigCage: ")).append(obj.getClass().toString()).toString()))
            obj = iterator.next();
    }
}

Или, возможно, источник в порядке, но компилятор создает неверный байт-код? Дело в том, что мы вызываем метод <U extends Dog> void printList(List<U> list) с параметром BigCage<Animal> animalCage, а Animal не расширяет Dog.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...