Reputation: 651
I am trying to implement a code analyser which takes as input another Java file. For each Variable Declaration, I want to check if the type that the variable belongs to is generic or not. Is there a simple way to do that?
For example, I would like for it to:
isGenericType("HashSet") -> true
isGenericType("int") -> false
I can possibly create a registry containing all generic types, but the problem with that would be if I implement a custom generic type, then I would have to update the registry each time. Is there some simple solution to this?
Upvotes: 13
Views: 5341
Reputation: 12393
I think this is similar to what you are looking for:
It prints true
for java.util.HashSet
.
But false
for java.lang.Object
.
public class TestGenerics
{
public static boolean isGenericType(String s) throws ClassNotFoundException
{
Class c = Class.forName(s);
return c.getTypeParameters().length > 0;
}
public static void main(String[] args) throws ClassNotFoundException
{
System.out.println(isGenericType("java.util.HashSet"));
System.out.println(isGenericType("java.lang.Object"));
}
}
Upvotes: 6
Reputation: 4945
The other answers seem to be focused on using a regular expression or reflection, but what this seems to call for is a parser. So that's what I recommend you use.
As an example, Eclipse has their Java Development Tools (JDT) plug-in which provides you with all of the parse tools you'd need to do this right. All you'd have to do is ask the JDT to give you an Abstract Syntax Tree (AST) with bindings enabled. Whenever a variable is declared you'd get an ITypeBinding for the declared type of the variable, and another for the instantiated type (if the variable is instantiated in the declaration).
And ITypeBinding has methods that tell you whether it is generic, parameterized, etc.
You'll also get the type parameters, in case you need those.
There are other java parser options as well, but this is the one with which I am familiar.
=============================================
Specific results using the Eclipse JDT
Case 1: HashSet<String> h1 = new HashSet();
Case 2: HashSet<String> h2 = new HashSet<>();
Case 3: HashSet<String> h3 = new HashSet<String>();
As currently understood, the goal of this exercise is to recognize Case 1 as not generic (it is raw), and Cases 2 and 3 as generic (they have type parameters, though in Case 2 the type parameter is implied).
Looking solely at the nodes produced from new HashSet...
All three cases produce a ClassInstanceCreation instance.
Case 1 and Case 2 have 0 type arguments, while Case 3 has 1 type argument.
The ITypeBinding for Case 1 is marked raw, while for Cases 2 and 3 they are marked parameterized. This constitutes success, as we have a way of distinguishing between Case 1 on the one hand and Cases 2 and 3 on the other.
The ITypeBinding for Case 1 has 0 type arguments, while Cases 2 and 3 both have 1 type argument in the ITypeBinding. Note that Case 2 has 0 type arguments in the AST itself, but the binding has one type argument. This allows one to distinguish between an explicit type argument and the use of the diamond operator.
All of the information necessary to complete this task is readily available from the AST and bindings.
Now here's the bad news: the bindings are only available when the byte code is available, i.e. this isn't a simple text analysis exercise.
Upvotes: 3
Reputation: 72854
Even if HashSet
can be generic, the type HashSet
itself (without <T>
) is a raw type. Therefore, I would go with the approach of scanning the actual type of the declared variable, maybe applying a regex on the type to see if the angle brackets exist:
isGenericType --> matches the pattern [a-zA-Z_\$][\w\$]*<[a-zA-Z_\$][\w\$]*>
If you want to strictly account for a valid identifer, you can check its definition in the Java Language Specification:
Identifier:
IdentifierChars but not a Keyword or BooleanLiteral or NullLiteral
IdentifierChars:
JavaLetter {JavaLetterOrDigit}
JavaLetter:
any Unicode character that is a "Java letter"
JavaLetterOrDigit:
any Unicode character that is a "Java letter-or-digit"
A "Java letter" is a character for which the method
Character.isJavaIdentifierStart(int)
returns true.A "Java letter-or-digit" is a character for which the method
Character.isJavaIdentifierPart(int)
returns true.
Upvotes: 5
Reputation: 7519
This will test by object, class or class name.
Update: Several revisions of this code based on helpful comments have provided some interesting test cases. However, ultimately an appropriate solution to the problem depends on precisely how the question is defined. Refer to earlier revisions and comments.
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashMap;
public class GenericTest
{
public static void main(String[] args)
{
try
{
new GenericTest().testAll();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
System.exit(0);
}
public void testAll() throws ClassNotFoundException, InstantiationException, IllegalAccessException
{
Object a = new HashMap<String, Object>();
Object b = new HashMap();
int c = 0;
isGeneric(a);
System.out.println("\n");
isGeneric(b);
System.out.println("\n");
isGeneric(c);
System.out.println("\n");
isGeneric("java.util.HashMap");
System.out.println("\n");
isGeneric("java.lang.Integer");
System.out.println("\n");
isGeneric(new TestA());
System.out.println("\n");
isGeneric(new TestB());
System.out.println("\n");
isGeneric(new TestB<String>());
System.out.println("\n");
isGeneric(new TestC());
System.out.println("\n");
isGeneric(new TestD());
System.out.println("\n");
isGeneric(new TestE());
System.out.println("\n");
return;
}
public static void isGeneric(String className) throws ClassNotFoundException, InstantiationException, IllegalAccessException
{
GenericTest.isGeneric(Class.forName(className));
return;
}
public static boolean isGeneric(Object o)
{
return isGeneric(o.getClass());
}
public static boolean isGeneric(Class<?> c)
{
boolean hasTypeParameters = hasTypeParameters(c);
boolean hasGenericSuperclass = hasGenericSuperclass(c);
// boolean hasGenericSuperinterface = hasGenericSuperinterface(c);
// boolean isGeneric = hasTypeParameters || hasGenericSuperclass || hasGenericSuperinterface;
boolean isGeneric = hasTypeParameters || hasGenericSuperclass;
System.out.println(c.getName() + " isGeneric: " + isGeneric);
return isGeneric;
}
public static boolean hasTypeParameters(Class<?> c)
{
boolean flag = c.getTypeParameters().length > 0;
System.out.println(c.getName() + " hasTypeParameters: " + c.getTypeParameters().length);
return flag;
}
public static boolean hasGenericSuperclass(Class<?> c)
{
Class<?> testClass = c;
while (testClass != null)
{
Type t = testClass.getGenericSuperclass();
if (t instanceof ParameterizedType)
{
System.out.println(c.getName() + " hasGenericSuperclass: " + t.getClass().getName());
return true;
}
testClass = testClass.getSuperclass();
}
return false;
}
public static boolean hasGenericSuperinterface(Class<?> c)
{
for (Type t : c.getGenericInterfaces())
{
if (t instanceof ParameterizedType)
{
System.out.println(c.getName() + " hasGenericSuperinterface: " + t.getClass().getName());
return true;
}
}
return false;
}
public interface TestX<X> { }
public interface TestY extends TestX<String> { }
public class TestA implements TestY { }
public class TestB<V> extends TestA { }
public class TestC extends TestB<String> { }
public class TestD extends TestA { }
public class TestE extends TestC { }
}
The results of running the above code:
java.util.HashMap hasTypeParameters: 2
java.util.HashMap hasGenericSuperclass: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
java.util.HashMap isGeneric: true
java.util.HashMap hasTypeParameters: 2
java.util.HashMap hasGenericSuperclass: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
java.util.HashMap isGeneric: true
java.lang.Integer hasTypeParameters: 0
java.lang.Integer isGeneric: false
java.util.HashMap hasTypeParameters: 2
java.util.HashMap hasGenericSuperclass: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
java.util.HashMap isGeneric: true
java.lang.Integer hasTypeParameters: 0
java.lang.Integer isGeneric: false
GenericTest$TestA hasTypeParameters: 0
GenericTest$TestA isGeneric: false
GenericTest$TestB hasTypeParameters: 1
GenericTest$TestB isGeneric: true
GenericTest$TestB hasTypeParameters: 1
GenericTest$TestB isGeneric: true
GenericTest$TestC hasTypeParameters: 0
GenericTest$TestC hasGenericSuperclass: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
GenericTest$TestC isGeneric: true
GenericTest$TestD hasTypeParameters: 0
GenericTest$TestD isGeneric: false
GenericTest$TestE hasTypeParameters: 0
GenericTest$TestE hasGenericSuperclass: sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl
GenericTest$TestE isGeneric: true
Upvotes: 6