Подтвердить что ты не робот

Общие методы Java в классах дженериков

Если вы создаете общий класс в Java (у класса есть параметры типового типа), можете ли вы использовать общие методы (метод принимает общие параметры типа)?

Рассмотрим следующий пример:

public class MyClass {
  public <K> K doSomething(K k){
    return k;
  }
}

public class MyGenericClass<T> {
  public <K> K doSomething(K k){
    return k;
  }

  public <K> List<K> makeSingletonList(K k){
    return Collections.singletonList(k);
  }
}

Как и следовало ожидать с помощью общего метода, я могу вызвать doSomething(K) в экземплярах MyClass с любым объектом:

MyClass clazz = new MyClass();
String string = clazz.doSomething("String");
Integer integer = clazz.doSomething(1);

Однако, если я попытаюсь использовать экземпляры MyGenericClass без указания общего типа, Я называю doSomething(K) возвращает Object, вне зависимости от того, что было передано K:

MyGenericClass untyped = new MyGenericClass();
// this doesn't compile - "Incompatible types. Required: String, Found: Object"
String string = untyped.doSomething("String");

Как ни странно, он будет компилироваться, если тип возвращаемого значения является общим классом - например. List<K> (На самом деле это можно объяснить - см. ответ ниже):

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String"); // this compiles

Кроме того, он будет скомпилирован, если типичный класс будет напечатан, даже если только с подстановочными знаками:

MyGenericClass<?> wildcard = new MyGenericClass();
String string = wildcard.doSomething("String"); // this compiles
  • Есть ли веская причина, почему вызов универсального метода в нетипизированном родовом классе не должен работать?

  • Есть ли какой-нибудь умный трюк, относящийся к общим классам и общим методам, которые мне не хватает?

EDIT:

Чтобы уточнить, я ожидал бы, что нетипизированный или не типичный типичный класс не будет соблюдать общие параметры типа класса (поскольку они не были предоставлены). Однако мне непонятно, почему нетипизированный или не типичный типичный класс будет означать, что общие методы не соблюдаются.

Понятно, что этот вопрос уже поднят на SO, c.f. этот вопрос. Ответы на это объясняют, что, когда класс является нетипизированным/в его исходной форме, все дженерики удаляются из класса, включая типизацию общих методов.

Однако на самом деле нет объяснений, почему это так. Поэтому позвольте мне уточнить мой вопрос:

  • Почему Java удаляет типичный метод ввода на нетипизированных или нестандартных классах? Есть ли веская причина для этого, или это просто недосмотр?

EDIT - обсуждение JLS:

Было высказано предположение (в ответ на предыдущий вопрос SO и на этот вопрос), что это рассматривается в JLS 4.8, в котором говорится

Тип конструктора (§8.8), метод экземпляра (§8.4, п. 9.4) или нестатического поля (§8.3) М необработанного типа С, который не унаследован от его суперклассов или суперинтерфейсов, является исходным тип, соответствующий стиранию его типа в общем объявлении, соответствующем C.

Мне ясно, как это относится к нетипизированному классу - типовые типы класса заменяются типами стирания. Если обобщенные обобщенные классы связаны, то тип стирания соответствует этим границам. Если они не связаны, то тип стирания является объектом - например.

// unbound class types
public class MyGenericClass<T> {
  public T doSomething(T t) { return t; }
}
MyGenericClass untyped = new MyGenericClass();
Object t = untyped.doSomething("String");

// bound class types
public class MyBoundedGenericClass<T extends Number> {
  public T doSomething(T t) { return t; }
}
MyBoundedGenericClass bounded = new MyBoundedGenericClass();
Object t1 = bounded.doSomething("String"); // does not compile
Number t2 = bounded.doSomething(1); // does compile

Хотя общие методы являются методами экземпляра, мне не ясно, что JLS 4.8 применяется к общим методам. Тип обобщенного метода (<K> в более раннем примере) не является нетипизированным, так как его тип определяется параметрами метода - только класс нетипирован/необработанный.

4b9b3361

Ответ 1

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

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

Адаптация JLS 4.8 к этому конкретному случаю:

Тип общего метода - это тип raw, который соответствует стирание его типа в общей декларации, соответствующей C.

(здесь "тип" метода будет включать все типы параметров и возвратов). Если вы интерпретируете "стирание" как "стирание всех дженериков", то это, похоже, соответствует наблюдаемому поведению, хотя оно не очень интуитивно или даже полезно. Это почти похоже на чрезмерную согласованность, чтобы стереть все дженерики, а не только общие параметры класса (хотя кто я, чтобы догадаться дизайнеров).

Возможно, могут возникнуть проблемы, когда общие параметры класса взаимодействуют со стандартными параметрами метода - в вашем коде они полностью независимы, но вы можете представить себе другие случаи, когда они назначаются/смешиваются вместе. Я считаю, что стоит отметить, что использование исходных типов не рекомендуется, как в JLS:

Использование типов raw допускается только в качестве концессии на совместимость устаревшего кода. Использование исходных типов в коде, написанном после введение универсальности в язык программирования Java сильно обескуражен. Возможно, что будущие версии Java язык программирования запретит использование исходных типов

Некоторые из мнений разработчиков Java очевидны здесь:

http://bugs.sun.com/view_bug.do?bug_id=6400189

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

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

Запрос заключается в изменении стирания типа, так что в объявлении типа Foo<T> стирание удаляет только T из параметризованных типов. Затем происходит так, что в объявлении Map<K,V> Set<Map.Entry<K,V>> стирается до Set<Map.Entry>.

Но если Map<K,V> имел метод, который взял тип Map<String,V>, его стирание было бы просто Map<String>. Для стирания типа для изменения количества параметров типа это ужасно, особенно для разрешения метода компиляции. Мы абсолютно не собираемся принимать этот запрос.

Слишком много ожидать, что вы сможете использовать необработанные типы (Map), сохраняя при этом некоторую безопасность типов дженериков (Set<Map.Entry>).

Ответ 2

В Java Generics, если вы используете необработанную форму универсального класса, тогда все дженерики класса, даже не связанные с ними общие методы, такие как ваши методы makeSingletonList и doSomething, становятся необработанными. Причина, по которой я понимаю, заключается в том, чтобы обеспечить обратную совместимость с Java-кодом, написанными с предварительными генериками.

Если вам не нужен ваш общий параметр типа T, просто удалите его из MyGenericClass, оставив ваши методы с помощью K. Иначе вам придется жить с тем фактом, что параметр типа класса T должен быть присвоен вашему классу для использования дженериков в чем-либо еще в классе.

Ответ 3

Я нашел одну причину полностью отказаться от дженериков (однако это не очень хорошо). Причина в том, что дженерики могут быть ограничены. Рассмотрим этот класс:

public static class MyGenericClass<T> {
    public <K extends T> K doSomething(K k){
        return k;
    }

    public <K> List<K> makeSingletonList(K k){
        return Collections.singletonList(k);
    }
}

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

makeSingletonList компилируется, потому что Java выполняет неконтролируемый отбор от List до List<K> (однако компилятор выводит предупреждение).

Ответ 4

Это не отвечает на фундаментальный вопрос, но решает вопрос о том, почему makeSingletonList(...) компилируется, а doSomething(...):

В нетипизированной реализации MyGenericClass:

MyGenericClass untyped = new MyGenericClass();
List<String> list = untyped.makeSingletonList("String");

... эквивалентно:

MyGenericClass untyped = new MyGenericClass();
List list = untyped.makeSingletonList("String");
List<String> typedList = list;

Это будет скомпилировано (с несколькими предупреждениями), но затем будет открыто для ошибок времени выполнения.

Действительно, это также аналогично:

MyGenericClass untyped = new MyGenericClass();
List<Integer> list = untyped.makeSingletonList("String"); // this compiles!!!
Integer a = list.get(0);

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

Ответ 5

Причиной этого является обратная совместимость с кодом pre-generics. Код pre-generics не использовал общие аргументы, вместо этого использовал то, что кажется сегодня сырым типом. В коде с предварительным генериком вместо ссылок использовались бы ссылки Object вместо общего типа, а типы raw использовали тип Object для всех общих аргументов, поэтому код действительно был обратно совместим.

В качестве примера рассмотрим этот код:

List list = new ArrayList();

Это предварительный код, и после введения дженериков это интерпретировалось как исходный общий тип, эквивалентный:

List<?> list = new ArrayList<>();

Потому что? не имеет ключевого слова extends или super после него, оно преобразуется в это:

List<Object> list = new ArrayList<>();

Версия List, которая использовалась перед генериками, использовала Object для ссылки на элемент списка, и эта версия также использует Object для ссылки на элемент списка, поэтому обратная совместимость сохраняется.

Ответ 6

Из моего комментария на вопрос MAnyKeys:

Я думаю, что полное стирание стилей имеет смысл в сочетании с упомянутой обратной совместимостью. Когда объект создается без общих аргументов типа, он может быть (или shoudl be?) Предполагать, что использование объекта происходит в несеричном коде.

Рассмотрим этот устаревший код:

public class MyGenericClass {
    public Object doSomething(Object k){
        return k;
    }
}

называется так:

MyGenericClass foo = new MyGenricClass();
NotKType notKType = foo.doSomething(new NotKType());

Теперь класс и его метод становятся универсальными:

public class MyGenericClass<T> {
    public <K extends KType> K doSomething(K k){
        return k;
    }
}

Теперь вышеупомянутый код вызывающего абонента больше не компилируется, поскольку NotKType не является типом KType. Чтобы избежать этого, общие типы заменяются на Object. Хотя бывают случаи, когда это не будет иметь никакого значения (например, ваш пример), это, по крайней мере, очень сложно для компилятора, чтобы анализировать, когда. Возможно даже невозможно.

Я знаю, что этот сценарий кажется немного сконструированным, но я уверен, что это происходит время от времени.