Calling java.lang.reflect.Type.toString()
provides very nice representation of generic types:
@Test
public void typeToStringWithTypeToken() {
assertEquals("java.util.List<java.lang.String>", new TypeToken<List<String>>() {}.getType().toString());
}
What I need is the reverse of Type.toString()
method, i.e. a method that can create Type
s from 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");
}
Which can pass 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 couldn't find a library or a SO question that addresses this problem.
The output of java.lang.reflect.Type.toString()
is implementation specific.
The behaviour you are seeing is part of Gson's ParameterizedTypeImpl
Due to type erasure, classes in Java don’t have type parameters at runtime.
The Type
from gson doesn’t represent an actual loaded Class
.
See Get Type of ParameterizedType from generic for related information.
Well... You can do it by yourself using for example antlr4 using 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 parse 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 working example here.
NOTE
CLASS
lexer rule may be a bit inaccurate. Also this parser only applies to non-primitive classes and parametrized types, but you of course extend it to support wildcard types as well.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With