Generics are very widely used in both Java and C# programming, especially in frameworks and libraries. Generics primarily provide type safety and improved performance by avoiding the need to cast variables. Java and C# Generics look very similar at the syntactical level but they work differently. The difference in behavior is because of how the support for Generics is implemented in both these languages. We'll explore this in this post. Generics Implementation Generics support in a language is required at both and . Let's use an example to understand this better. Compile time Run time A library named declares a Generic type as shown below. This library is built and published which is then used in other programs. common-lib public class GenericTest<T> { private T _ref; } An application named uses the 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? } } and are different artifacts. When is compiled, the compiler needs to know that is a Generic type so it should be treated differently. Therefore when is compiled, the compiled output should have information about the Generic type. This will allow the compiler to ensure type safety when compiling the . Both Java and C# guarantee compile-time type safety so this is important. common-lib demo-app demo-app GenericTest<T> common-lib demo-app Reflection is supported by both Java and C#. Reflection APIs allow accessing type information at run time. Reflection also supports creating new objects, call methods on an object, etc. at run time. To support all these operations on Generic types, some support for Generics should be present at the run time also. Java Generic Code Lifecycle Compile Time Java uses the concept of to support Generics in Java. Through Type Erasure, the Java compiler converts all Generic type references to Non-Generic types at the time of compilation. The type Erasure approach was used to provide backward compatibility so that Non-Generic types can be passed to newer code using generics. Let's understand with an example. Type Erasure The following is a simple generic class. public class GenericTest<T> { private T _ref; public <T1 extends Comparable<T>> boolean isEqual(T1 obj){ return obj.compareTo(this._ref) == 0 ? true : false; } } When this class is compiled, the Generic type parameters are removed and replaced with Non-Generic equivalents. Following is the generated byte code - shown by : Bytecode Viewer The following snippet lists the difference between source code and the compiled version - see comments in the code: //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) . One of the side effects of Type Erasure is that and are the same after Type Erasure by the compiler, so it is not possible to have both in the same package. Compiled Java code does not have any trace of Generic types. Everything is mapped to a raw Java type GenericTest<T> GenericTest Run Time At the JVM level, there are no Generic types. As explained in the earlier section, the Java compiler removes all traces of Generic types so the JVM doesn't have to do anything different to handle Generic types. C# Generic Code Lifecycle Compile Time Following is the equivalent C# code of the example used above in 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; } } When the above code is compiled, the C# compiler retains the generic type information, which is used by the .Net Runtime to support generics. The class metadata of compiled library maintains Generics information. A peek into compiled library metadata using [IL Disassembler] ( ): https://docs.microsoft.com/en-us/dotnet/framework/tools/ildasm-exe-il-disassembler The IL Code (same as byte code of Java) of method has Generics information - see underlined sections: IsEqual Run Time .Net Runtime (CLR) uses the Generic type information in the compiled code to create concrete types at runtime. Let's understand with an example. The following code creates three objects of for three different types. GenericTest<T> GenericTest<int> intObj = new GenericTest<int>(); GenericTest<double> doubleObj = new GenericTest<double>(); GenericTest<string> strObj = new GenericTest<string>(); When this code is run, the .Net Runtime would dynamically create three concrete types based on the original Generic type definition in the IL Code: GenericTest<T> : T replaced with . This type will be used to create all new objects of type GenericTest<int> GenericTest<int> int : T replaced with . This type will be used to create all new objects of type GenericTest<double> GenericTest<double> double : T replaced with . This type will be used to create all new objects of any reference type like GenericTest<String>, GenericTest<FileStream>, GenericTest<SomeClass>, etc. GenericTest<Object> System.Object .Net Runtime (CLR) creates a new type for each primitive \ value type, which gives both type safety and performance benefit by avoiding boxing operations. For reference type, there is only type and the .Net Runtime type safety mechanism ensures type safety. Differences in Behavior Due to the nature of implementation, there are a few differences between how Generics work in Java and C#: Primitive Types Support Java does not support primitive types in Generics because cannot work in that case. Type Erasure C# supports primitive types (or Value Types in C#) in Generics which gives two benefits: Type safety Performance benefits by removing boxing and unboxing needs. This is achieved by .Net Runtime's dynamic concrete type creation as explained above. Because of this limitation in Java, there are a number of Functional Interfaces like IntFunction, LongFunction, etc. If primitive types can be supported by Generics, only one interface can be enough: public interface Function<T,R> { R apply(T value); } There is an open item to support primitive types in Java generics. JEP 218: Generics over Primitive Types Performance Type Erasure inserts casts wherever required to ensure type safety, but this will add to performance cost rather than improving performance by avoiding casting with Generics. E.g., 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); } Run time operations If you have to do run-time type checks on T (like is T instance of IEnumerable), reflect on Generic types, or do kind of operations, it is either not possible in Java or you have to use workarounds. Let's see an example. new T() We will write a function that will deserialize a JSON string into an object using Generic parameters. Following is the C# code: 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 would not compile. T.class public static <T> T getObject(String json) { ObjectMapper m = new ObjectMapper(); return (T)m.readValue(json, T.class); } To make the above code work, method would have to take the Type as input parameter. getObject 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); Summary Java and C# implement Generics support very differently. Type Erasure method used in Java results in limitations on Generics usage compared to C#. C# compiler as well as the Runtime (CLR) understands Generics. That’s why C# is able to provide performance benefits and better support for run-time operations. References C# Generics Internals If you want to go to memory-level details, read .Net Generics under the hood Very detailed comparison of features - Comparing Java and C# Generics - Jonathan Pryor's web log Core Java SE 9 for the Impatient Also published here.