Сначала может показаться, что JVM создает новую версию класса где и заменяет
Т на нужный тип. Но на самом деле, JVM частично 'подправляет' уже имеющийся параметризованный класс. В том числе заменяет
Т на версию нужного значения каждый раз, когда это требуется. Т.е класс существует один, а не несколько версий с полями разных типов.
В вашем же случае сначала JVM подправляет
T obj на
Object obj (который преобразуется нисходящим преобразованием в String) а потом снова подправляет
String obj на
Object obj (который потом так-же преобразуется в Integer неявно)
g.set("String");
f.set(123);
T obj на самом деле заменяется на
Object obj. Этот процесс называется стиранием, который происходит на стадии компиляции. А затем неявно происходит нисходящее преобразование.
class Test<T> {
T val; // на самом деле T заменяется на Object
Test(T o) { val = o; }
T get() { return val; }
}
class SimpleClass {
void info() { System.out.println("я метод info() из класса SimpleClass"); }
}
Test<SimpleClass> v = new Test<>(new SimpleClass());
SimpleClass v2 = v.get(); // при инициализации v2 на самом деле происходит неявное нисходящее преобразование к нужному типу
//что происходит на самом деле:
//SimpleClass v2 = (SimpleClass) v.get();
v2.info();
Стирание генерализированных типов происходит на стадии компиляции. В самом простом случае ничем не ограниченный тип стирается до Object`а, обобщенный класс NameClass до NameClass.
Другими словами информация о дженериках существует только на этапе компиляции и недоступна в runtime.