Почему общие методы и общие типы имеют различный синтаксис ввода типа?

При изучении дженериков я заметил разницу в синтаксисе ввода типа между общие методы и общие типы (класс или интерфейс), которые меня смутили.

Синтаксис для общего метода:

<T> void doStuff(T t) {
    // Do stuff with T
}

Документы говорят

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

Синтаксис для общего типа

class Stuff<T> {
    // Do stuff with T
    T t;
}

Документы говорят

Раздел параметров типа, ограниченный угловыми скобками (< > ), , следует за именем класса. Он задает параметры типа

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


Чтобы быть совместимыми друг с другом, я ожидал, что синтаксис метода будет void doStuff<T>(T t) {} или синтаксис типа (для класса) как class <T>Stuff {}, но это, очевидно, не так.

Почему это нужно вводить раньше, а другое после?

Я использовал generics в основном в форме List<String> и утверждал, что <String>List может выглядеть странно, но это субъективный аргумент, кроме того, для методов это похоже на это. Вы можете вызвать doStuff как this.<String>doStuff("a string");

В поисках технического объяснения я подумал, что, возможно, перед тем, как указать тип возврата, должен быть введен метод <T> для метода, поскольку T может быть типом возвращаемого значения, и, возможно, компилятор не может так заглядывать, звучало нечетно, потому что компиляторы умны.

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

Ответ 1

Ответ действительно лежит в Спецификация GJ, которая уже была связана, цитата из документа, стр. 14:

Согласование параметров передачи перед именем метода становится необходимым с помощью ограничений синтаксического анализа: с более условным соглашением типа "параметры после имени метода" выражение f (a<b,c>(d)) имеет два возможных анализа.

Реализация из комментариев:

f(a<b,c>(d)) может анализироваться как один из f(a < b, c > d) (два логических значения из сравнений, переданных в f) или f(a<B, C>(d)) (вызов аргументов типа B и C и аргумент d, переданный в f). Я думаю, что это также может быть почему Scala решил использовать [] вместо <> для дженериков.

Ответ 2

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

Это формальный ответ на ваш вопрос, но не ответ на ваш вопрос в контексте GJ. Но ясно, что это не имеет никакого отношения к синтаксису С++, потому что в разделе параметров С++ предшествует как ключевое слово class, так и тип возвращаемого метода.

Ответ 3

Мое сильное предположение заключается в том, что, поскольку, как вы сказали для метода, общий параметр также может быть возвращаемым типом функции:

public <RETURN_TYPE> RETURN_TYPE getResult();

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

Если у вас был синтаксис типа

public RETURN_TYPE getResult<RETURN_TYPE>();

для анализа потребуется вторая развертка.

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

Ответ 4

Там нет какой-то глубокой теоретической причины для этого - это, по-видимому, случай "разработчики языка сделали это именно так". Например, С# использует именно тот синтаксис, о котором вам интересно, почему Java не реализует. Следующий код:

private T Test<T>(T abc)
{
    throw new NotImplementedException();
}

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

Преимущество синтаксиса Java, как и сейчас, заключается в том, что он немного упрощен для реализации анализатора LL (1) для методов с использованием текущего синтаксиса.

Ответ 5

Причина в том, что во время компиляции тип generic и параметризованный тип обрабатываются по-разному. Один из них - это параметры типа Eliding, а другой - аргументы типа Eliding во время стирания.

Generics добавляется в Java в 2004 году в официальной версии J2SE 5.0. В документации Oracle "" Использование и программирование дженериков в J2SE 5.0" указано

За кулисами

Дженерики реализуются компилятором Java как интерфейсный преобразование называется стиранием, которое является процессом перевода или переписывающий код, который использует generics в не общий код (то есть карты новый синтаксис текущей спецификации JVM). Другими словами, это преобразование стирает всю информацию общего типа; вся информация между угловыми скобками стирается. Например, перевозка грузов по львову станет LinkedList. Использование переменных другого типа заменяется на верхняя граница переменной типа (например, Object) и когда результирующий код не соответствует правилу, приведение к соответствующему типу вставляется.

Ключ находится в процессе Тип Erasure. Никаких изменений JVM не было сделано для поддержки генериков, поэтому Java не помнит обобщенную компиляцию типа.

В названной Cost of Erasure, выпущенной Университетом Нового Орлеана, сломал шаги Erasure для нас:

Шаги, выполняемые во время стирания стилей, включают в себя:

  • Параметры типа Eliding: когда компилятор находит определение родового типа или метода, он удаляет все вхождения каждого типа параметр, заменяющий его левой стороной, или Object, если нет ограничений.

  • Аргументы типа Eliding: когда компилятор находит параметризованный тип, экземпляр типового типа, он удаляет тип аргументы. Например, тип List<String> переводится на List.

Для общего метода компилятор ищет определение общего типа, которое находится в слева, наиболее ограниченном. И это буквально означает более далекое левое, и именно поэтому Ограниченные типизированные параметры появляется перед тем, как метод возвращает тип. Для универсального класса или интерфейса компилятор ищет параметризованный тип, который, в отличие от типичного типа, не является левым, самым связанным с определением класса, но вместо этого следует за классом. Затем компилятор удаляет аргументы типа, поэтому JVM может его понять.

Если вы просматриваете раздел Приложения "Стоимость Erasure" . Он прекрасно демонстрирует, как компилятор обрабатывает интерфейс и методы генериков.

Мостовые методы

При компиляции класса или интерфейса, который расширяет параметризованный класс или реализует параметризованный интерфейс, компилятору может потребоваться создать синтетический метод, называемый мостиковым методом, как часть типа стирание. Вам обычно не нужно беспокоиться о мосту методы, но вы можете быть озадачены, если они появляются в трассировке стека.

Примечание. Кроме того, компилятору иногда может понадобиться вставить синтетические мостовые методы. Bridge Methods является частью процесса стирания типа. Мостовые методы отвечают за то, чтобы убедиться, что сигнатуры методов совпадают после стирания типа. Подробнее об этом читайте в Влияние методов стирания и моста типов


Изменить: Как указывает OP, мой вывод о "левой стороне" означает буквально, что средства, расположенные дальше слева, недостаточно прочны. (ОП задал в своем вопросе, что он не интересуется типом ответа "Я думаю" ), поэтому я немного погубил и нашел это GenericsFAQ. Из примера кажется, что порядок параметров типа имеет значение. т.е. <T extends Cloneable & Comparable<T>> становится Cloneable после типа enrasure, но не Comparable

введите описание изображения здесь

вот еще один пример непосредственно из Oracle Erasure of Generic Type

В следующем примере общий Node класс использует параметр ограниченного типа:

public class Node<T extends Comparable<T>> {
   ...
}

Компилятор Java заменяет параметр ограниченного типа T первым связанным классом Comparable.

Я думаю, что более технически правильный способ заключается в том, чтобы сказать, что стирание типа заменяет связанный тип первым связанным классом (или Object, если T неограничен), просто случается, что первый связанный класс является самым связанным слева из-за синтаксиса в Java.

Ответ 6

Я думаю, это потому, что вы можете объявить его как возвращаемый тип:

 <T> T doStuff(T t) {
     // Do stuff with T
    return t;
}

Вам нужно объявить тип перед объявлением возвращаемого типа, потому что вы не можете использовать то, что еще не определено. Например, вы не можете использовать переменную x, прежде чем объявлять ее где-нибудь. Мне нравится (любой) язык, чтобы следовать некоторым логическим правилам, тогда его легче использовать, и в какой-то момент, когда вы его знаете, вы просто знаете, чего вы можете ожидать от него. Так обстоит дело с java, у него есть некоторые шансы, но в целом они следуют некоторым правилам. И тот, который вы не можете использовать, прежде чем объявлять, что это очень сильное правило в java, и для меня это очень приятно, потому что он создает меньше WTF, когда вы пытаетесь понять код Java, поэтому я думаю, что это рассуждения позади него. Но я не знаю, кто именно отвечает за это решение, цитата из википедии:

В 1998 году Гилад Брача, Мартин Одерски, Дэвид Стаутамир и Филипп Wadler создал Generic Java, расширение языка Java для поддержка общих типов. [3] Generic Java была включена в Java (2004, Java 5) с добавление подстановочных знаков.

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

Я не считаю, что это связано с обратной совместимостью с предыдущей версией java.

Ответ 7

Java Generics были представлены с Java 1.5. Идея новых функций языка - никогда не нарушать предыдущие версии. Мы должны помнить, что Generics - это функция безопасности типов для языка/разработчика. При этом были введены два новых типа parameterized types и type variables.

JLS 4.3 Reference Types и Values ​​ предлагает следующий синтаксис для TypeArgument и TypeVariable.

ReferenceType:     ClassOrInterfaceType     TypeVariable     ArrayType

ClassOrInterfaceType:     ClassType     InterfaceType

ClassType:     ТипDeclSpecifier ТипArgumentsopt

InterfaceType:     ТипDeclSpecifier ТипArgumentsopt

TypeDeclSpecifier:     TypeName
    ClassOrInterfaceType. Идентификатор

TypeName:     Идентификатор     ТипName. Идентификатор

TypeVariable:     Идентификатор

ArrayType:     Тип []

Примеры подобны этим

Vector<String>
Seq<Seq<A>>
Seq<String>.Zipper<Integer>
Collection<Integer>
Pair<String,String>

и для параметризованных типов

Vector<String> x = new Vector<String>();
Vector<Integer> y = new Vector<Integer>();
return x.getClass() == y.getClass();

Всякий раз, когда не задана никакая оценка, он будет считать ее как java.lang.Object и с стиранием типа это будет, например, Vector<Object>, поэтому он обратно совместим с предыдущими версиями Java.


Синтаксис для общих методов, когда сам класс не является общим, имеет следующий синтаксис.

Из JLs 8.4 Объявления методов

MethodDeclaration:     МетодHeader MethodBody

MethodHeader:     МетодModifiersopt TypeParametersopt Метод результатаDeclarator Throwsopt

MethodDeclarator:     Идентификатор (FormalParameterListopt)

Пример выглядит так:

public class GenericMethod {
    public static <T> T aMethod(T anObject) {
        return anObject;
    }
    public static void main(String[] args) {
        String greeting = "Hi";
        String reply = aMethod(greeting);
    }
}

Какие результаты при стирании типа

public class GenericMethod {
    public static Object aMethod(Object anObject) {
        return anObject;
    }
    public static void main(String[] args) {
        String greeting = "Hi";
        String reply = (String) aMethod(greeting);
    }
}

И снова он обратно совместим с предыдущими версиями Java. См. Обе документы заявки для более глубоких рассуждений

Добавление дженериков к языку программирования Java: Спецификация проекта участника

Специализация общих типов Java


О технической части. Шаги по созданию Java-программы состоят в компиляции файла .java. Это можно сделать с помощью команды javac для генерации файлов классов. JavacParser анализирует весь файл с указанной выше спецификацией и генерирует байт-код. См. здесь для исходного кода JavacParser.

Возьмем следующий файл Test.java

class Things{}

class Stuff<T>{
    T t;

    public <U extends Things> U doStuff(T t, U u){
        return u;
    };
    public <T> T doStuff(T t){
        return t;
    };
}

Чтобы поддерживать обратную совместимость, JVM не изменила предыдущие атрибуты для файлов классов. Они добавили новый атрибут и назвали его Signature. Из прокси-бумаги

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

JVM Spec 4.3.4 определяет следующий синтаксис

MethodTypeSignature:     FormalTypeParametersopt (TypeSignature *) ReturnType ThrowsSignature *

ReturnType:     TypeSignature     VoidDescriptor

ThrowsSignature:     ^ ClassTypeSignature     ^ TypeVariableSignature

Разберите файл Test.class с помощью javap -v, получив следующее:

class Stuff<T extends java.lang.Object> extends java.lang.Object
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#20         // java/lang/Object."<init>":()V
   #2 = Class              #21            // Stuff
   #3 = Class              #22            // java/lang/Object
   #4 = Utf8               t
   #5 = Utf8               Ljava/lang/Object;
   #6 = Utf8               Signature
   #7 = Utf8               TT;
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               doStuff
  #13 = Utf8               (Ljava/lang/Object;LThings;)LThings;
  #14 = Utf8               <U:LThings;>(TT;TU;)TU;
  #15 = Utf8               (Ljava/lang/Object;)Ljava/lang/Object;
  #16 = Utf8               <T:Ljava/lang/Object;>(TT;)TT;
  #17 = Utf8               <T:Ljava/lang/Object;>Ljava/lang/Object;
  #18 = Utf8               SourceFile
  #19 = Utf8               Test.java
  #20 = NameAndType        #8:#9          // "<init>":()V
  #21 = Utf8               Stuff
  #22 = Utf8               java/lang/Object
{
  T t;
    descriptor: Ljava/lang/Object;
    flags:
    Signature: #7                           // TT;

  Stuff();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 4: 0

  public <U extends Things> U doStuff(T, U);
    descriptor: (Ljava/lang/Object;LThings;)LThings;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=3
         0: aload_2
         1: areturn
      LineNumberTable:
        line 8: 0
    Signature: #14                          // <U:LThings;>(TT;TU;)TU;

  public <T extends java.lang.Object> T doStuff(T);
    descriptor: (Ljava/lang/Object;)Ljava/lang/Object;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: areturn
      LineNumberTable:
        line 11: 0
    Signature: #16                          // <T:Ljava/lang/Object;>(TT;)TT;
}
Signature: #17                          // <T:Ljava/lang/Object;>Ljava/lang/Object;
SourceFile: "Test.java"

Метод

public <U extends Things> U doStuff(T t, U u){
        return u;
    };

переводит на подпись, чтобы указать ее общий метод

   Signature: #14                          // <U:LThings;>(TT;TU;)TU;

Если мы использовали не-общий класс для предыдущих версий Java 1.5, например.

public String doObjectStuff(Object t, String u){
        return u;
    }

переводится на

 public java.lang.String doObjectStuff(java.lang.Object, java.lang.String);
    descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=3, args_size=3
         0: aload_2
         1: areturn
      LineNumberTable:
        line 12: 0

Единственное различие между ними состоит в том, что у одного есть поле атрибута Signature, указывающее, что это действительно общий метод, в то время как другие предыдущие версии Java 1.5 его не имеют. Но оба имеют одинаковый атрибут descriptor

Non-Generic method
 descriptor: (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String;
Generic method 
 descriptor: (Ljava/lang/Object;LThings;)LThings;

Это делает его обратно совместимым. Таким образом, ответ будет таким, каким вы предложили

"разработчики языка просто сделали это так"

с добавлением

"разработчики языка просто сделали это, , чтобы сделать его обратно совместимым, не добавляя много кода"


EDIT: О комментировании того, что должно быть легко справиться с различным синтаксисом, я нашел отрывок из книги "Дженерики и коллекции" Филиппа Вадлера, Мориса Нафталина

Обобщения в Java напоминают шаблоны в С++. Есть только две важные вещи, которые нужно помнить о взаимосвязи между генериками Java и шаблонами С++: синтаксис и семантика. Синтаксис преднамеренно подобен, и семантика преднамеренно отличается.
Синтаксически угловые скобки были выбраны потому, что они знакомы с пользователями С++, и потому, что квадратные скобки будут трудно разобрать. Однако в синтаксисе есть одна разница. В С++ вложенные параметры требуют дополнительных пробелов, поэтому вы видите такие вещи: List < Список > [...] и т.д.

Смотрите здесь