Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java how to parametrize a generic method with a Set?

Tags:

java

generics

I have a method with such signature:

private <T> Map<String, byte[]> m(Map<String, T> data, Class<T> type)

When I invoke like this for example it is working fine:

Map<String, String> abc= null;
m(abc, String.class);

But when my parameter T is a Set it doesn't work:

Map<String, Set<String>> abc= null;
m(abc, Set.class);

Is there a way to make it work?

like image 713
Janek Avatar asked Jul 28 '15 18:07

Janek


1 Answers

You're going to have to do something really ugly, using an unchecked cast like this:

m(abc, (Class<Set<String>>) (Class<?>) Set.class);

This comes down to type-erasure. At runtime Class<Set<String>> is the same as Class<Set<Integer>>, because we don't have reified generics, and so there is no way to know that what you have is a class for a "Set of strings" vs. a class for a "Set of integers".

I asked a related question some time ago that should also give you some pointers:

  • Return a class instance with its generic type

IMO this confusion is due to the fact the generics were bolted on after the fact, and aren't reified. I think it's a failing of the language when the compiler tells you that the generic types don't match, but you don't have an easy way of even representing that particular type. For example, in your case you end up with the compile-time error:

        m(abc, Set.class);
        ^
  required: Map<String,T>,Class<T>
  found: Map<String,Set<String>>,Class<Set>
  reason: inferred type does not conform to equality constraint(s)
    inferred: Set
    equality constraints(s): Set,Set<String>
  where T is a type-variable:
    T extends Object declared in method <T>m(Map<String,T>,Class<T>)

Now it would be perfectly reasonable for you to think "Oh, I should use Set<String>.class then", but that is not legal. This is abstraction leakage from the implementation of generics in the language, specifically that they are subject to type-erasure. Semantically, Set<String>.class represents the runtime class instance of a set of strings. But actually at runtime we cannot represent the runtime class of a set of strings, because it is indistinguishable from a set that contains objects of any other type.

So we have a runtime semantic that is at odds with compile-time semantic, and knowing why Set<T>.class isn't legal requires knowing that generics are not reified at runtime. This mismatch is what leads to weird workarounds like these.

What compounds the problem is that class instances also ended up being conflated with type-tokens. Since you do not have access to the type of the generic parameter at runtime, the work around has been to pass in an argument of type Class<T>. On the surface this works great because you can pass in things like String.class (which is of type Class<String>) and the compiler is happy. But this method breaks down in your case: what if T itself represents a type with its own generic-type parameter? Now using classes as type-tokens is not useful because there is no way to distinguish between Class<Set<String>> and Class<Set<Integer>> because fundamentally, they are both Set.class at runtime and so share the same class instance. So IMO, using a class as a runtime type-token doesn't work as a general solution.

Due to this shortcoming in the language, there are some libraries that make it very easy to retrieve the generic type-information. In addition they also provide classes are better at representing the "type" of something:

  • TypeTools
  • Reflection Explained: Google Guava
like image 122
Vivin Paliath Avatar answered Oct 11 '22 04:10

Vivin Paliath