Java, привязка статического метода и дженерики все свернуты с некоторой перегрузкой метода

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

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

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 но когда я запускаю его, я получаю следующее исключение класса cast:

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. два списка печати являются не переопределение, но перегрузка друг друга, как ожидалось(они имеют разные типы, потому что общие типы их аргументов разные). Это можно проверить с помощью аннотации @Override
  2. изменение void printList(List<?>) метод клетка класс чтобы быть нестатическим, генерирует соответствующую ошибку времени компиляции
  3. изменение метода void <U extends Dog> printList(List<U>) на класс BigCage до void <U> printList(List<U>) выдает ошибку.
  4. на main () вызов printList() до класс BigCage (ie BigCage.printList(...)) генерирует ту же ошибку выполнения
  5. на main () вызов printList() до класс Кейдж!--20--> (клетки т. е..printList(...)) работает, как и ожидалось, только вызывая версию printList на клетка
  6. если я скопирую определение printList(List<?>) to класс BigCage С клетка класс, который скроет определение в клетка класс, я получаю соответствующую ошибку компилятора

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

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

* * EDIT**

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

4 ответов


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

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() to 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(список) в клетке есть же стирания, но и не скрывает другое

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


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

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


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

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());
        }
    }
}

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

    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

(или sth о столкновении имя с тем же errasure), но это не так.
После dissasembling (javap и -с 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 compiller версия

BigCage#printList
BigCage: class Cat
BigCage: class Dog

IMHO эти результаты оба неверны.


IMHO этот код может быть неверным. метод printList в классе BigCage должен вызвать имя clash coz printList в клетке имеют одинаковое стирание, но ни один не переопределяет другой. странно, что compiller компилирует его :)

полученный байт-код (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();
    }
}

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