Чем отличаются дженерики в Java и C#

Чем отличаются дженерики в Java и C#

12 января 2023 г.

Обобщения очень широко используются как в программировании на Java, так и в C#, особенно во фреймворках и библиотеках. Универсальные шаблоны в первую очередь обеспечивают безопасность типов и повышенную производительность за счет устранения необходимости приведения переменных.

Java и C# Generics очень похожи на синтаксическом уровне, но работают по-разному. Разница в поведении связана с тем, как реализована поддержка Generics в обоих этих языках. Мы рассмотрим это в этом посте.

Реализация универсальных шаблонов

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

Библиотека с именем common-lib объявляет универсальный тип, как показано ниже. Эта библиотека создается и публикуется, а затем используется в других программах.

public class GenericTest<T> {
    private T _ref;
}

Приложение с именем demo-app использует common-lib.

public class App{
    public static void main(String[] args){
        GenericTest<MyClass> t = new GenericTest<MyClass>();
        GenericTest<SomeClass> s = new GenericTest<SomeClass>();
        //s = t; //allowed? type safety?
    }
}

common-lib и demo-app — разные артефакты. При компиляции demo-app компилятору необходимо знать, что GenericTest является универсальным типом, поэтому его следует обрабатывать по-другому. Поэтому при компиляции common-lib скомпилированный вывод должен содержать информацию о универсальном типе. Это позволит компилятору обеспечить безопасность типов при компиляции демо-приложения. И Java, и C# гарантируют безопасность типов во время компиляции, поэтому это важно.

Отражение поддерживается как Java, так и C#. API-интерфейсы Reflection позволяют получать доступ к информации о типах во время выполнения. Reflection также поддерживает создание новых объектов, вызов методов объекта и т. д. во время выполнения. Для поддержки всех этих операций с универсальными типами во время выполнения также должна присутствовать некоторая поддержка универсальных типов.

Жизненный цикл универсального кода Java

Время компиляции

Java использует концепцию стирания типов для поддержки обобщений в Java. С помощью Type Erasure компилятор Java преобразует все ссылки на универсальный тип в неуниверсальный тип во время компиляции. Подход типа Erasure использовался для обеспечения обратной совместимости, чтобы неуниверсальные типы могли передаваться в более новый код с использованием дженериков. Давайте разберемся на примере.

Ниже приведен простой универсальный класс.

public class GenericTest<T> {

    private T _ref;

    public <T1 extends Comparable<T>> boolean isEqual(T1 obj){
        return obj.compareTo(this._ref) == 0 ? true : false;
    }
}

Когда этот класс компилируется, параметры универсального типа удаляются и заменяются неуниверсальными эквивалентами. Ниже приведен сгенерированный байт-код, показанный Bytecode Viewer:

Byte Code for a Generic Type in Java

В следующем фрагменте перечислены различия между исходным кодом и скомпилированной версией — см. комментарии в коде:

//source code
public class GenericTest<T>
//compiled code - GenericTest<T> became just GenericTest
public class generics/example/application/GenericTest

//source code
private T _ref;
//compiled code - T was replaced with Object
private java.lang.Object _ref;

//source code
public <T1 extends Comparable<T>> boolean isEqual(T1 obj)
//compiled code - T1 became Comparable because
//of constraint that T1 should be subtype of Comparable<T>
public isEqual(java.lang.Comparable arg0)

Скомпилированный код Java не содержит никаких следов универсальных типов. Все сопоставляется с необработанным типом Java. Один из побочных эффектов Type Erasure заключается в том, что GenericTest и GenericTest совпадают после Type Erasure компилятором, поэтому невозможно иметь оба в одном пакете.

Время работы

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

Жизненный цикл универсального кода C#

Время компиляции

Ниже приведен код C#, эквивалентный приведенному выше примеру в Java:

public class GenericTest<T>
{
    private T _ref;

    public bool IsEqual<T1>(T1 obj) where T1 : IComparable<T>
    {
        return obj.CompareTo(this._ref) == 0 ? true : false;
    }
}

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

Метаданные класса скомпилированной библиотеки содержат информацию о универсальных шаблонах. Просмотр метаданных скомпилированной библиотеки с помощью [дизассемблера IL] (https:// docs.microsoft.com/en-us/dotnet/framework/tools/ildasm-exe-il-disassembler):

C# Assembly Metadata in IL Diassembler

IL-код (такой же, как байт-код Java) метода IsEqual содержит общую информацию — см. подчеркнутые разделы:

IL Code for C# Generic Type

Время работы

.Net Runtime (CLR) использует информацию об универсальном типе в скомпилированном коде для создания конкретных типов во время выполнения. Давайте разберемся на примере.

Следующий код создает три объекта GenericTest для трех разных типов.

GenericTest<int> intObj = new GenericTest<int>();
GenericTest<double> doubleObj = new GenericTest<double>();
GenericTest<string> strObj = new GenericTest<string>();

При выполнении этого кода среда выполнения .Net динамически создает три конкретных типа на основе исходного определения универсального типа GenericTest в коде IL:

  1. GenericTest: T заменено на int. Этот тип будет использоваться для создания всех новых объектов типа GenericTest
  2. .

2. GenericTest: T заменено на double. Этот тип будет использоваться для создания всех новых объектов типа GenericTest

.

3. GenericTest: T заменено на System.Object. Этот тип будет использоваться для создания всех новых объектов любого ссылочного типа, таких как GenericTest, GenericTest, GenericTest и т. д.

.Net Runtime (CLR) создает новый тип для каждого примитивного типа значения, что обеспечивает как безопасность типов, так и повышение производительности за счет исключения операций упаковки. Для ссылочного типа существует только тип, а механизм безопасности типа .Net Runtime обеспечивает безопасность типов.

Различия в поведении

Из-за характера реализации существует несколько различий между работой универсальных шаблонов в Java и C#:

* Поддержка примитивных типов

* Java не поддерживает примитивные типы в Generics, поскольку в этом случае стирание типов не работает.


  • C# поддерживает примитивные типы (или типы значений в C#) в Generics, что дает два преимущества:

    • Безопасность типов
    • Повышение производительности за счет устранения необходимости упаковки и распаковки. Это достигается за счет создания динамического конкретного типа среды выполнения .Net, как описано выше.

    * Из-за этого ограничения в Java существует ряд функциональных интерфейсов, таких как IntFunction, LongFunction и т. д. Если универсальные типы могут поддерживаться примитивными типами, может быть достаточно только одного интерфейса:

    <код>java функция открытого интерфейса<T,R> { R применить (значение T);

    * Существует открытый элемент JEP 218: Generics over Primitive Types для поддержки примитивных типов в Java generics.< /p>

* Производительность

* Type Erasure вставляет приведения везде, где это необходимо для обеспечения безопасности типов, но это увеличивает стоимость производительности, а не улучшает производительность, избегая приведения с помощью Generics. Например,


```java
public void test() {
    ArrayList<MyClass> al = new ArrayList<MyClass>();
    al.add(new MyClass());
    //Compiler would add cast
    MyClass m = al.get(0); //source
    //MyClass m = (MyClass)al.get(0) //compiled
    //this will be fine as al.get(0) anyway returns Object.
    Object o = al.get(0);
}
```

* Операции во время выполнения

* Если вам нужно выполнять проверки типов во время выполнения для T (например, экземпляр T IEnumerable), отражать общие типы или выполнять операции типа new T(), это либо невозможно в Java или вам придется использовать обходные пути. Давайте посмотрим на пример.


* We will write a function that will deserialize a JSON string into an object using Generic parameters.

  
* Following is the C# code:

  
  ```csharp
  public static T getObject<T>(string json)
  {
      return (T)JsonConvert.DeserializeObject(json, typeof(T));
  }
  // usage
  // MyClass m = getObject<MyClass>("json string");
  ```

  
* But same thing can't work in Java because *T.class* would not compile.

  
  ```java
  public static <T> T getObject(String json) {
    ObjectMapper m = new ObjectMapper();
    return (T)m.readValue(json, T.class);
  }
  ```


* To make the above code work, *getObject* method would have to take the Type as input parameter.

  
  ```java
  public static <T> T getObject(String json, Type t) {
    ObjectMapper m = new ObjectMapper();
    return (T)m.readValue(json, t.getClass());
  }
  //usage
  // MyClass m = getObject<MyClass>("json string", MyClass.class);
  ```

  

Обзор

Java и C# реализуют поддержку универсальных шаблонов совершенно по-разному. Метод стирания типов, используемый в Java, приводит к ограничениям на использование обобщений по сравнению с C#. Компилятор C#, а также среда выполнения (CLR) понимают универсальные шаблоны. Вот почему C# может обеспечить преимущества в производительности и лучшую поддержку операций во время выполнения.

Ссылки


Также опубликовано здесь


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE