Универсальные методы 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
  • Есть ли веская причина, по которой вызов универсального метода в нетипизированном универсальном классе не должен работать?

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

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

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

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

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

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

РЕДАКТИРОВАТЬ - обсуждение JLS:

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

Тип конструктора (§8.8), метода экземпляра (§8.4, §9.4) или нестатического поля (§8.3) M необработанного типа C, который не наследуется от его суперклассов или суперинтерфейсов, является исходным типом, который соответствует к стиранию его типа в универсальном объявлении, соответствующем 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> в предыдущем примере) не является нетипизированным, поскольку его тип определяется параметрами метода - только класс является нетипизированным / необработанным.


person amaidment    schedule 01.08.2013    source источник
comment
возможный дубликат объединения исходных типов и общих методов. См. Также: Почему этот общий код Java не компилируется?   -  person Paul Bellora    schedule 01.08.2013
comment
stackoverflow.com/questions/ 14882003 / :)   -  person Affe    schedule 01.08.2013
comment
@PaulBellora - спасибо. Первый очень во многом связан - хотя мой обильный поиск SO не нашел его, поэтому спасибо за ссылку. Второй вариант менее актуален, поскольку он охватывает только общие классы, а не общие методы.   -  person amaidment    schedule 02.08.2013
comment
Извините, слегка ОТ. Но я попытался найти его, статический класс - это новая конструкция? AFAIK он не будет компилироваться на Java 6 (будет ли он на Java 7?).   -  person H.Rabiee    schedule 02.08.2013
comment
@hajder - извините, я изначально написал их как вложенные классы в тестовом классе (следовательно, сделал их статическими) и не позаботился об их удалении, когда я C&P ответил на вопрос. Сейчас удалено.   -  person amaidment    schedule 02.08.2013
comment
адаптация JLS 4.8 к этому конкретному случаю: тип универсального метода - это необработанный тип, который соответствует стиранию его типа в универсальном объявлении, соответствующем C., если вы интерпретируете «стирание» как «стирание всех универсальных методов», тогда это действительно похоже, соответствует наблюдаемому поведению, хотя и не является интуитивно понятным или очень полезным. Это почти похоже на чрезмерную последовательность, чтобы стереть все дженерики, а не только общие параметры класса (хотя кто я такой, чтобы предположить, что дизайнеры)   -  person Graham Griffiths    schedule 06.08.2013


Ответы (6)


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

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

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

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

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

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

Использование необработанных типов разрешено только в качестве уступки совместимости унаследованного кода. Настоятельно не рекомендуется использовать необработанные типы в коде, написанном после введения универсальности в язык программирования 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>).

person Graham Griffiths    schedule 06.08.2013
comment
Я бы согласился, что обратная совместимость - веская причина - просто непонятно, зачем для этого необходимо стирание типов всех дженериков. +1 за отчет об ошибке, в котором эта проблема признана ошибкой, но ее исправление влияет на совместимость (что не совсем то же самое, что сказать, что это было необходимо для совместимости ...). К сожалению, кажется, что исправление еще не вошло в выпущенную версию ... - person amaidment; 06.08.2013
comment
есть также такой запрос: bugs.sun.com/bugdatabase/view_bug.do? bug_id = 6256320, где кто-то, кажется, запрашивает описанное вами поведение - стирает только общие параметры класса, а не другие общие параметры - но оно было отклонено - person Graham Griffiths; 06.08.2013
comment
+1 Хорошее исследование - рассмотрите возможность цитирования оценок связанной ошибки / запроса в своем ответе. @amaidment Я рекомендую этот ответ за вознаграждение, потому что он ближе всего к пониманию намерений разработчиков языка. - person Paul Bellora; 08.08.2013
comment
Действительно - заслуженная награда, теперь присуждаемая. - person amaidment; 12.08.2013

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

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

person rgettman    schedule 01.08.2013
comment
Спасибо - в моем случае мне нужны параметры типа универсального класса. В приведенном выше примере они не используются, поскольку мне нужен лаконичный SSCCE. Кроме того, мне не ясно, почему стирание всех дженериков в необработанном классе обеспечивает обратную совместимость. - person amaidment; 02.08.2013
comment
@amaidment предположим, что у вас есть этот интерфейс: interface C<T> { <K> K foo(); }. Поведение, описанное в ответе, гарантирует, что некоторые предварительные общие C можно безопасно автоматически генерировать без изменений старого кода. В противном случае пользователи интерфейса C должны вызывать foo следующим образом: c.<SomeType>foo() - person MAnyKey; 06.08.2013
comment
@MAnyKey - вы можете опубликовать свой ответ в качестве ответа ... в любом случае, я думаю, вы упустили суть. Если у вас есть нетипизированная реализация C, не только универсальные шаблоны классов (T) будут обрабатываться как Object (чего и следовало ожидать), но и общий метод (<K> K foo()) будет рассматриваться как нетипизированный, поэтому реализация foo() будет должно быть public Object foo(){...}, что возвращает нас к исходному вопросу. - person amaidment; 06.08.2013

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

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> (однако компилятор отображает предупреждение).

person MAnyKey    schedule 06.08.2013
comment
Нет - типы методов (<K>) не зависят от типов классов (<T>) (если иное не ограничено, например, <K extends T> в соответствии с вашим примером) и не зависят друг от друга. Таким образом, у нас может быть класс C<T extends Number> с методом public <T> T doSomething(T t){ return t; }, и он может называться new C<Integer>().doSomething("String"). Это будет компилироваться (но будет не очень понятно для будущих разработчиков ...), поскольку общий тип метода берется вместо универсального типа класса, и они независимы. - person amaidment; 06.08.2013
comment
Это не противоречит всему, что я написал. Да, типы методов не зависят от типов классов. Но если типы методов ограничены типами классов, компилятор должен отбросить все это, когда класс используется без обобщений. - person MAnyKey; 06.08.2013
comment
+1 один пример использования типа метода, зависящего от типа класса, является достаточной причиной для исключения обобщений в любом месте класса, когда используется необработанный тип. - person yair; 06.08.2013
comment
Я не согласен - компилятор не должен отбрасывать все общие типы (хотя, похоже, это было сделано). Скорее, компилятор мог обрабатывать только нетипизированные универсальные шаблоны как Object. Это то, что я ожидал, но этого не происходит, и, следовательно, остается вопрос ... почему удалены все общие типы? - person amaidment; 06.08.2013
comment
Я думаю, что полное стирание типа имеет смысл в сочетании с упомянутой обратной совместимостью. Когда объект создается без аргументов универсального типа, можно предположить, что использование объекта происходит в неуниверсальном коде, и поэтому все универсальные типы сразу заменяются на Object. Я предполагаю, что компилятор не может угадать при каждом вызове, был ли он задуман как общий. Что произойдет, если вы сделаете методы статическими? - person André Stannek; 06.08.2013

Это не отвечает на фундаментальный вопрос, но решает вопрос, почему 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.

person amaidment    schedule 06.08.2013

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

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

List list = new ArrayList();

Это предварительный универсальный код, и после того, как были введены универсальные шаблоны, он был интерпретирован как необработанный универсальный тип, эквивалентный:

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

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

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

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

person tbodt    schedule 06.08.2013
comment
Ваш ответ дает хорошее объяснение того, почему Java игнорирует универсальные типы классов в нетипизированном классе. Однако на самом деле вопрос касается общих методов, для которых типизация также игнорируется в нетипизированном классе и которые вы не рассмотрели. - person amaidment; 06.08.2013
comment
Фрагмент JLS из 4.8 (из вопроса, который вы ссылаетесь) охватывает конструкторы, методы экземпляра и поля-члены - общие методы - это просто частный случай методов экземпляра в целом. Мне кажется, что ответ tbodt охватывает вашу ситуацию? - person Graham Griffiths; 06.08.2013
comment
Разве «обратная совместимость» не является достаточной причиной? Некоторые мысли здесь очевидны: bugs.sun.com/view_bug.do?bug_id= 6400189 (ошибка + исправление, показывающее, что возвращаемый тип метода обрабатывается как часть типа метода для целей стирания этого типа) - person Graham Griffiths; 06.08.2013

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

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

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

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. Хотя есть случаи, когда это не имеет никакого значения (например, ваш пример), компилятору, по крайней мере, очень сложно проанализировать, когда. Может быть даже невозможно.

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

person André Stannek    schedule 06.08.2013