akhiljain
akhiljain

Reputation: 651

Check if type is generic

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

Answers (4)

eugenioy
eugenioy

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

Erick G. Hagstrom
Erick G. Hagstrom

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

M A
M A

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

Parker
Parker

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

Related Questions