Java-дженерики вида <T расширяет > и компилятор Eclipse

#java #eclipse #maven #generics #compiler-errors

#java #затмение #мавен #дженерики #ошибки компилятора

Вопрос:

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

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

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

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

Наконец, мне нужно реализовать две вспомогательные функции, 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 generics, т.Е. Средство проверки типов 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

Комментарии:

1. Действительно используете Maven 3.1. ? Я рекомендую обновить вашу версию Maven (Maven 3.6.1) и, возможно, другие инструменты .. например, Eclipse до последней версии 2019-03… что не устранит проблему, потому что это связано с другими вещами, как показано в ответе.

2. К сожалению, да, исторические версии задаются (определяются) проектом. Но более новые версии не решили проблему.

3. Какова диаграмма классов ваших классов? Было бы здорово, если бы вы могли его добавить.

Ответ №1:

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

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

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

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

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

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


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

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

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

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

Комментарии:

1. Нет, я не согласен. Программа синтаксически и семантически корректна: нет синтаксической ошибки и нет ошибки времени выполнения. Но средство проверки типов Java не может выполнить проверку типа программы. Это связано с приближением статических контролеров типов. Таким образом, программа не является корректной с точки зрения ввода, т.е. проверки типов Java.

2. Что касается определения дженериков: Java Generics — это своего рода полиморфизм типов, позволяющий программам работать с различными типами, обеспечивая при этом безопасность типов. Итак, почему определение класса должно быть неуместным? Целью данного определения типа является указание абстрактных методов в терминах конкретных (дочерних) типов, например, setAge метода, который определен в Animal, и при вызове метода для Cat он возвращает Cat . Это хорошо известный пример использования полиморфизма типов. Но я открыт для улучшения определения класса!

3. «Синтаксически неверный», возможно, был неудачным выбором слов. Ваша программа не является допустимой Java — и система типов является частью этого. Проблемы с компилятором нет. Семантически правильно? Извините, но нет.

4. Извините, но это неверно. Существует четкое определение синтаксиса и семантики. Моя программа семантически корректна. Вы можете выполнить его без ошибок! Более того, большая часть информации о типе удаляется после компиляции исходного кода, поэтому она не нужна для выполнения. И синтаксической ошибки нет, поскольку код подтверждает синтаксическое определение Java.

5. Вы правы, программа не является допустимой Java из-за проверки типов, которая является частью Java. Так что это неправильно! Однако Java знает, что его средство проверки типов в какой-то момент дает сбой и отклоняет «правильные» программы. Поэтому они ввели некоторые языковые конструкции, чтобы сделать проверку типов счастливой. Одним из них является операция приведения .. и с помощью приведения типов вы можете сделать мой пример «type»-допустимым.

Ответ №2:

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

Чтобы подвести итог возможным решениям:

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

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

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

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

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

Очевидно, что код также проверяет типы, используем ли мы конкретные списки, например, список Cat ‘s или список Dog ‘s, например 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 не может скомпилировать этот код, среда IDE Eclipse скомпилировала код и не показывает никакой ошибки типа.