Class .forName equivalent to create ParameterizedType from String

Calling java.lang.reflect.Type.toString() provides a very good representation of the generic types:

 @Test public void typeToStringWithTypeToken() { assertEquals("java.util.List<java.lang.String>", new TypeToken<List<String>>() {}.getType().toString()); } 

I need the inverse Type.toString() method, i.e. method that can create Type from the given string representation:

 public static Type parseTypeString(String typeString) throws ClassNotFoundException { if (typeString.indexOf('<') == -1) { return Class.forName(typeString); } // ??? how to implement rest throw new IllegalStateException("not implemented"); } 

What can pass the following tests with specialized types:

 @Test public void justSimpleClassName() throws Exception { assertEquals(Integer.class, parseTypeString("java.lang.Integer")); } @Test public void listOfInteger() throws Exception { assertEquals(new TypeToken<List<Integer>>() {}.getType(), parseTypeString("java.util.List<java.lang.Integer>")); } @Test public void complexType() throws Exception { assertEquals(new TypeToken<Map<List<Integer>, Set<String>>>() {}.getType(), parseTypeString("java.util.Map<java.util.List<java.lang.Integer>, java.util.Set<java.lang.String>>")); } 

I could not find a library or SO question that solves this problem.

+5
source share
2 answers

Well ... you can do it yourself using antlr4 using the following grammar

 type returns[ClassBuilder value] : cls=CLASS { $value = ClassBuilder.parse($cls.text); } | cls=CLASS { $value = ClassBuilder.parse($cls.text); } LT head=type { $value.add($head.value); } (COMMA tail=type { $value.add($tail.value); })* GT ; GT : '>' ; LT : '<' ; COMMA : ',' ; CLASS : ('a'..'z'|'A'..'Z') ('a'..'z'|'A'..'Z'|'0'..'9'|'$'|'.'|'_')* ; 

Where ClassBuilder looks like

 public class ClassBuilder { private final Class<?> clazz; private final List<ClassBuilder> parameters = new ArrayList<>(); public ClassBuilder(final Class<?> clazz) { this.clazz = clazz; } public static ClassBuilder parse(String clazz) { try { return new ClassBuilder(Class.forName(clazz)); } catch (ClassNotFoundException e) { // do better handling here throw new IllegalStateException(e); } } public void add(ClassBuilder builder) { parameters.add(builder); } public Type build() { // class is not parametrized if (parameters.isEmpty()) { return clazz; } return ParameterizedTypeImpl.make( clazz, parameters.stream() .map(ClassBuilder::build) .toArray(Type[]::new), null); } } 

And then finally analyze the string

 CharStream stream = new ANTLRInputStream( "java.util.Map<java.util.List<java.lang.Integer>, java.util.Set<java.lang.String>>" ); TokenStream tokenStream = new CommonTokenStream(new ParametrizedTypeLexer(stream)); ParametrizedTypeParser parser = new ParametrizedTypeParser(tokenStream); assertEquals( new TypeToken<Map<List<Integer>, Set<String>>>() {}.getType(), parser.type().value.build() ); 

You can see a working example here .

Note

CLASS The lexer rule may be a little inaccurate. In addition, this parser applies only to non-primitive classes and parameterized types, but you, of course, extend it to support wildcard types.

+2
source

The result of java.lang.reflect.Type.toString() is implementation specific.
The behavior you see is part of the Gson ParameterizedTypeImpl
Due to type erasure, classes in Java do not have type parameters at run time.
Type from gson is not the actual loaded Class .

See Get the ParameterizedType Type from the General for relevant information.

+1
source

All Articles