Обобщения Java в форме <T extends A <T>> и Eclipse Compiler - PullRequest
0 голосов
/ 15 апреля 2019

У меня есть еще одна интересная проблема с Java Generics.Я уверен, что некоторые из вас могли бы иметь правильный ответ на него.

Контекст. Сначала, чтобы понять контекст, давайте посмотрим на определение объектов Animal, Mammal, иКошка, которая определяется друг с другом:

public abstract class Animal<T extends Animal<T>> {
    private final int age;

    public Animal(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    public abstract T setAge(int age);

    public static void main(String[] args) {
        List<Mammal<?>> mammals = new ArrayList<Mammal<?>>();
        mammals = Animal.increaseAge(mammals, 1);
        Map<Integer, List<Mammal<?>>> mammalsByAge = Animal.groupByAge(mammals);
    }

    public static <T extends Animal<T>> List<T> increaseAge(List<T> animals, int augment) {
        List<T> list = new LinkedList<T>();
        for (T animal : animals) {
            list.add(animal.setAge(animal.getAge() + augment));
        }
        return list;
    }

    public static <T extends Animal<T>> Map<Integer, List<T>> groupByAge(List<T> animals) {
        Map<Integer, List<T>> animalsPerAge = new TreeMap<Integer, List<T>>();
        animals.forEach((animal) -> {
            int age = animal.getAge();
            animalsPerAge.putIfAbsent(age, new LinkedList<T>());
            animalsPerAge.get(age).add(animal);
        });
        return animalsPerAge;
    }

};

abstract class Mammal<T extends Mammal<T>> extends Animal<Mammal<T>> {
    public Mammal(int age) {
        super(age);
    }
};

class Cat extends Mammal<Cat> {
    public Cat(int age) {
        super(age);
    }

    @Override
    public Cat setAge(int age) {
        return new Cat(age);
    }
}

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

Наконец, мне нужнореализовать две вспомогательные функции, groupByAge и increaseAge.

Проблема. Вначале я понял, что Eclipse IDE может компилировать и выполнять программу, тогда как компилятор Mavenне удалось с ошибкой компилятора.

[ERROR] Failed to execute goal
org.apache.maven.plugins:maven-compiler-plugin:3.1:compile
(default-compile) on project BCBS248Calculator: Compilation failure:
Compilation failure:

[ERROR] Animal.java:[25,33] method increaseAge in class
com.ibm.ilm.bcbs248.Animal<Tcannot be applied to given types;

[ERROR] required: java.util.List<T>,int

[ERROR] found: java.util.List<com.ibm.ilm.bcbs248.Mammal<?>>,int

[ERROR] reason: inferred type does not conform to equality
constraint(s)

[ERROR] inferred: com.ibm.ilm.bcbs248.Mammal<capture#1 of ?>

[ERROR] equality constraints(s): com.ibm.ilm.bcbs248.Mammal<capture#1
of ?>,com.ibm.ilm.bcbs248.Mammal<?>

[ERROR] Animal.java:[27,68] method groupByAge in class
com.ibm.ilm.bcbs248.Animal<Tcannot be applied to given types;

[ERROR] required: java.util.List<T>

[ERROR] found: java.util.List<com.ibm.ilm.bcbs248.Mammal<?>>

[ERROR] reason: inferred type does not conform to equality
constraint(s)

[ERROR] inferred: com.ibm.ilm.bcbs248.Mammal<capture#2 of ?>

[ERROR] equality constraints(s): com.ibm.ilm.bcbs248.Mammal<capture#2
of ?>,com.ibm.ilm.bcbs248.Mammal<?>

На первый взгляд, я думал, что это проблема Maven (потому что Eclipse может компилировать и выполнять код), но в действительности это, похоже, ошибка (?) в компиляторе Eclipse.Даже обычный Java-компилятор не может скомпилировать программу, он заканчивается ошибкой типа, поскольку программа не может быть проверена.Это хорошо известная проблема с обобщениями Java, то есть средство проверки типов Java не может проверить программу на данном этапе.Это просто из-за приближения статической проверки типов.

Насколько я знаю, Eclipse использует собственный компилятор Java, то есть оболочку вокруг оригинального компилятора Java, которая позволяла компилировать вредоносные программы?Тем не менее, в этой ситуации, это не показывает сообщение об ошибке?Кто-нибудь может проверить, является ли это проблемой с компилятором Eclipse?

Кроме того, у кого-нибудь есть еще одна изящная идея о том, как решить проблему кодирования?Проще говоря, я могу изменить сигнатуру типа обеих вспомогательных функций следующим образом:

public static <T extends Animal<?>> List<T> increaseAge(List<T> animals, int augment) {
    List<T> list = new LinkedList<T>();
    for (T animal : animals) {
        list.add((T) animal.setAge(animal.getAge() + augment));
    }
    return list;
}

public static <T extends Animal<?>> Map<Integer, List<T>> groupByAge(List<T> animals) {
    Map<Integer, List<T>> animalsPerAge = new TreeMap<Integer, List<T>>();
    animals.forEach((animal) -> {
        int age = animal.getAge();
        animalsPerAge.putIfAbsent(age, new LinkedList<T>());
        animalsPerAge.get(age).add(animal);
    });
    return animalsPerAge;
}

К сожалению, мне также нужно добавить приведение типа в строку list.add((T) animal.setAge(animal.getAge() + augment));.Я могу жить с измененной сигнатурой типа, но я избавлюсь от трансляции типов, поскольку она заканчивается неопределенностью.

Повторное добавление элемента того же типа, похоже, не является проблемой с изменениями сигнатуры типа,Однако после вызова setAge некоторая информация, похоже, теряется.Может быть, я могу изменить тип функции или объекта таким образом, чтобы это работало?

-

Maven версия 3.1

Java версия 1.8.0_201 Java (TM)Среда выполнения SE (сборка 1.8.0_201-b09) 64-разрядная серверная виртуальная машина Java HotSpot (TM) (сборка 25.201-b09, смешанный режим)

Eclipse IDE для разработчиков Java Версия: выпуск Oxygen.3a (4.7.3a) Идентификатор сборки: 20180405-1200

Ответы [ 2 ]

0 голосов
/ 24 апреля 2019

Подводя итог, проблема не в теле метода. Ошибка типа говорит о том, что список, указанный в качестве аргумента, не соответствует сигнатуре типа метода (ов). В итоге все сводится к стандартной задаче ковариации и контравариантности списков с генериками. Каким-то образом я подумал, что мог бы быть умным и обойти неопределенности, используя вложенные обобщения, такие как Animal<Mammal<T>> и сигнатуры конкретных методов, но, похоже, это только перевело проблему в другое место.

Суммируя возможные решения:

Второй вызов метода (groupByAge) можно исправить с помощью подстановочного знака ?, например <T extends Animal<?>>. Это работает, поскольку мы перемещаем только элемент типа T в другой список типа T.

Первый вызов метода (increaseByAge) сложнее. Мы также можем добавить подстановочный знак ? к сигнатуре типа метода, однако тогда мы потеряем информацию о том, что у нас есть элемент типа T после вызова setAge (setAge возвращает затем элемент типа Animal<?>). * * тысячу двадцать-один

У кого-то другая идея, как решить эту проблему, может быть, с помощью другой подписи типа для Animal?

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

Другим возможным решением было бы полное удаление универсальных типов из Mammal в определении списка, например List<Mammal> mammals = new ArrayList<Mammal>();. Тем не менее, это также приводит к непроверенным операциям и, следовательно, отсутствует.

Очевидно, что код также проверяет типы, если мы используем конкретные списки, такие как список Cat или список Dog, например List<Cat> cats = new ArrayList<Cat>();. Тем не менее, ситуация требует наличия общего списка для кошек и собак в этой ситуации.

Наконец, осталось использовать специфичные для типа increaseByAge -методы, например, increaseByAge методы для списков млекопитающих, возможно, также реализованные в классе Mammal.

public static  List<Mammal<?>> increaseByAge(List<Mammal<?>> mammals, int augment) {
     List<Mammal<?>> list = new LinkedList<Mammal<?>>();
     for (Mammal<?> mammal : mammals) {
         mammal = mammal.setAge(mammal.getAge() + augment);
         list.add(mammal);
     }
     return list;
 }

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

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

Несмотря на все обсуждения общих типов, остается упомянуть, что это, похоже, ошибка в контроллере типов Eclipse. В то время как стандартный компилятор Java не может скомпилировать этот код, Eclipse IDE скомпилировал этот код и не выдает никаких ошибок типа.

0 голосов
/ 15 апреля 2019

Я думал, что это проблема Maven

Maven - это просто инструмент для сборки. Если вы получаете ошибку компилятора, это не имеет ничего общего с Maven.

Eclipse использует собственный Java-компилятор, то есть обертку вокруг оригинала. Java-компилятор, позволяющий компилировать вредоносные программы

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

Кто-нибудь может убедиться, что это проблема с компилятором Eclipse?

Как вы сказали сами: «Даже обычный компилятор Java не может скомпилировать программу». Так что ответ - нет. Ваша программа просто неверна.


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

Animal<T extends Animal<T>>
Mammal<T extends Mammal<T>>

Когда я вижу параметр универсального типа, я пытаюсь вставить слова «to» или «of» и посмотреть, имеет ли это смысл.

List<String> - это список из строк. Comparable<String> - это то, что можно сравнить с строками. A Listener<Message> прослушивает до сообщений.

Итак, что же такое Mammal<Cat>? Млекопитающее из кошек? Млекопитающее до кошек? Вы понимаете, как это не имеет смысла таким же образом?

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


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

abstract class Animal {
    private final int age;

    public Animal(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    public abstract Animal setAge(int age);

    public static <T extends Animal> List<T> increaseAge(List<T> animals, int augment) {
        List<T> list = new LinkedList<>();
        for (T animal : animals) {
            list.add((T) animal.setAge(animal.getAge() + augment));
        }
        return list;
    }

    public static <T extends Animal> Map<Integer, List<T>> groupByAge(List<T> animals) {
        Map<Integer, List<T>> animalsPerAge = new TreeMap<>();
        animals.forEach((animal) -> {
            int age = animal.getAge();
            animalsPerAge.putIfAbsent(age, new LinkedList<>());
            animalsPerAge.get(age).add(animal);
        });
        return animalsPerAge;
    }

    public static void main(String[] args) {
        List<Mammal> mammals = new ArrayList<>();
        mammals = Animal.increaseAge(mammals, 1);
        Map<Integer, List<Mammal>> mammalsByAge = Animal.groupByAge(mammals);
    }
};

abstract class Mammal extends Animal {
    public Mammal(int age) {
        super(age);
    }
}

class Cat extends Mammal {
    public Cat(int age) {
        super(age);
    }

    @Override
    public Cat setAge(int age) {
        return new Cat(age);
    }
}
...