Чем отличаются дженерики в 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
Отражение поддерживается как 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:
В следующем фрагменте перечислены различия между исходным кодом и скомпилированной версией — см. комментарии в коде:
//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
Время работы
На уровне 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):
IL-код (такой же, как байт-код Java) метода IsEqual содержит общую информацию — см. подчеркнутые разделы:
Время работы
.Net Runtime (CLR) использует информацию об универсальном типе в скомпилированном коде для создания конкретных типов во время выполнения. Давайте разберемся на примере.
Следующий код создает три объекта GenericTest
GenericTest<int> intObj = new GenericTest<int>();
GenericTest<double> doubleObj = new GenericTest<double>();
GenericTest<string> strObj = new GenericTest<string>();
При выполнении этого кода среда выполнения .Net динамически создает три конкретных типа на основе исходного определения универсального типа GenericTest
- GenericTest
: T заменено на int. Этот тип будет использоваться для создания всех новых объектов типа GenericTest .
2. GenericTest
3. GenericTest: T заменено на System.Object. Этот тип будет использоваться для создания всех новых объектов любого ссылочного типа, таких как 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# может обеспечить преимущества в производительности и лучшую поддержку операций во время выполнения.
Ссылки
- Внутреннее устройство C# Generics
- Если вы хотите перейти к подробностям на уровне памяти, прочитайте .Net Generics изнутри.
- Очень подробное сравнение функций — Сравнение универсальных шаблонов Java и C# — веб-журнал Джонатана Прайора< /а>
- Core Java SE 9 для нетерпеливых
Также опубликовано здесь
Оригинал