Можно ли узнать конкретный тип generic в Java в runtime
Что проверяют
Понимаете ли вы стирание типов и, что ценнее, знаете ли, что стирается не всё. Ответ «нельзя, type erasure» правильный, но короткий, за ним ждут продолжение.
Как отвечать
У конкретного объекта узнать нельзя. Компилятор заменяет параметр типа его верхней границей, а без границы на Object, поэтому new ArrayList<String>() в рантайме это просто ArrayList, и getClass() вернёт одно и то же для списка строк и списка чисел.
Но стирание касается экземпляров, а не объявлений. Сигнатуры полей, методов и суперкласса сохраняются в класс-файле и доступны рефлексии:
Field f = Holder.class.getDeclaredField("names"); // List<String> names
var t = (ParameterizedType) f.getGenericType();
System.out.println(t.getActualTypeArguments()[0]); // class java.lang.StringНа этом стоит приём с анонимным наследником: new TypeReference<List<String>>() {} записывает аргумент в сигнатуру суперкласса, откуда его достаёт getGenericSuperclass. Так устроены TypeReference в Jackson, TypeToken в Guava, ParameterizedTypeReference в Spring. Есть и путь проще: передать Class<T> параметром конструктора и сохранить в поле.
Где спотыкаются
Пробуют obj instanceof List<String>. Такой код не компилируется, допустим только List<?>, а он про аргумент ничего не скажет.
Утверждают, что информации о типах в рантайме не остаётся вовсе. Поле private List<String> names; свой аргумент хранит, и половина фреймворков сериализации живёт именно на этом.
Обходят ограничение непроверенным приведением и удивляются месту падения. ClassCastException возникает не там, где в коллекцию положили чужой объект, а там, где его прочитали и компилятор вставил проверку. По той же причине запрещён new T[n].