Example, Java is using erased generics. Once the code is compiled, the generics information is no longer in the bytecode. List<String> becomes List<>. This is called type erasure.
C# is using reified generics where this information is preserved. List<String> is still List<String> after compilation
Incidentally if you do what they're proposing for PHP in Java (where you define a non-generic subclass of a generic type), the actual generic type parameters actually are in the bytecode, and depending on the static type you use to reference it, may or may not be enforced...
public class StringList extends java.util.ArrayList<String> {
public static void main(String[] args) throws Exception {
StringList asStringList = new StringList();
java.util.ArrayList<Integer> asArrayList = (java.util.ArrayList<Integer>)(Object)asStringList;
System.out.println("It knows it's an ArrayList<String>: " + java.util.Arrays.toString(((java.lang.reflect.ParameterizedType)asArrayList.getClass().getGenericSuperclass()).getActualTypeArguments()));
System.out.println("But you can save and store Integers in it:");
asArrayList.add(42);
System.out.println(asArrayList.get(0));
System.out.println(asArrayList.get(0).getClass());
System.out.println("Unless it's static type is StringArrayList:");
System.out.println(asStringList.get(0));
}
}
That prints out: It knows it's an ArrayList<String>: [class java.lang.String]
But you can save and store Integers in it:
42
class java.lang.Integer
Unless it's static type is StringArrayList:
Exception in thread "main" java.lang.ClassCastException: class java.lang.Integer cannot be cast to class java.lang.String (java.lang.Integer and java.lang.String are in module java.base of loader 'bootstrap')
at StringList.main(StringList.java:11)
And as a consequence, C# can pack the value types directly in the generic data structure, instead of holding references to heap-allocated objects.
This is very important both for cache locality and for minimizing garbage collector pressure.