How to parameterize a generic method using Set?

I have a method with this signature:

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

When I call like this, for example, it works fine:

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

But when my T parameter is Set, it does not work:

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

Is there any way to make it work?

+5
source share
4 answers

You will need to do something really ugly using an uncontrolled cast:

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

It comes down to erasing the type. At runtime, Class<Set<String>> is the same as Class<Set<Integer>> , because we don’t have generics repeated, and therefore there is no way to know that you have a class for a “rowset” And class for "Set of integers."

Some time ago I asked a related question, which should also give you some pointers:

IMO, this confusion is due to the fact that generics were locked after the fact and were not confirmed. I think this is a language error when the compiler tells you that generic types do not match, but you have no easy way to even represent this particular type. For example, in your case, you will get a 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 wise to think: "Oh, I have to use Set<String>.class then", but this is not legal. This is an abstraction of a leak from the implementation of generics in the language, in particular that they are subject to type erasure. Semantically, Set<String>.class represents an instance of the rowset runtime class. But in reality, at run time, we cannot represent the execution class of a set of strings, because it is indistinguishable from a set containing objects of any other type.

Thus, we have runtime semantics that are inconsistent with compilation-time semantics, and knowing why Set<T>.class not legal requires knowing that generic elements are not updated at runtime. This mismatch is what leads to weird workarounds like these.

What binds the problem is that instance instances have also been merged with token types. Since you do not have access to the type of the universal parameter at run time, the work around should be an argument of type Class<T> . At first glance, this works fine, because you can pass things like String.class (which is of type Class<String> ), and the compiler is happy. But this method breaks up in your case: what if T itself represents a type with its own parameter of a general type? 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 basically they are Set.class at run time and therefore share the same same class instance. Thus, an IMO using a class as a runtime token does not work as a general solution.

Because of this flaw in the language, there are several libraries that make it very easy to get general type information. In addition, they also provide classes to better represent the “type” of something:

+3
source

From what I see, there are two possible solutions to this problem, in which both have their respective limitations.

The first solution is based on the fact that erasure of the java type is completed , which means that types for any parameterized types are erased regardless of the "depth". For example: a Map<String, Set<String> will be reduced to Map<String, Set> and then Map<Object, Object> , which means that although the type information is difficult to obtain, this is not technically necessary at runtime, given that any object can be inserted into the map (given that it conveys all the cool throws).

At the same time, we can create a relatively ugly (compared to the second solution) method of obtaining information such as runtime through an instance present on the map. Thus, no matter how many sets you insert and what the resulting “type” is after erasing, we can guarantee that its instance will be inserted back into the original map.

Demonstrated below:

 // Java 7 approach private <T> Map<String, byte[]> m(Map<String, T> data){ Class valueType = null; Iterator<T> valueIterator = data.values().iterator(); while(valueIterator.hasNext()){ T nextCandidate = valueIterator.next(); if(nextCandidate != null){ valueType = nextCandidate.getClass(); break; } } if(valueType == null){ // No instance present, fail return null; } // Create a new instance T obj = (T) valueType.newInstance(); // Exception handling not shown // Rest of code here return null; } 

as you can see, type information is extracted directly from the first non-zero value present in the map. In java 8 we can make better use of threads:

 // Java 8 approach private <T> Map<String, byte[]> m(Map<String, T> data){ // Note: use findFirst() for more consistent behaviour Optional<T> optInstance = data.values().stream().filter(Objects::nonNull).findAny(); if(!optInstance.isPresent()){ // No instance present, fail return null; } Class valueType = optInstance.get().getClass(); // Create a new instance T obj = (T) valueType.newInstance(); // Exception handling not shown // Rest of code here return null; } 

However, this solution has several limitations. As indicated, the card must contain at least one nonzero value for a successful operation. Secondly, this solution does not take into account subclasses of the declared type (? extends T) for specific elements, which can be problematic if you have elements of different classes (for example, TreeSet and HashSet within the same map).

The second problem can be solved easily by referring to type information on the basis of a key-value pair, and not on the basis of an "integral" map, although this is due to the "knowledge" of type information for all elements on the map. Alternatively, more complex solutions, such as developing the most specific general superclass for all nonzero values ​​in the map, can also be used, but for all purposes and goals this becomes more of a solution to a solution than a real one.


The second solution to this problem, in my opinion, is much cleaner, but it creates additional complexity for the caller. This approach follows a more functional approach and can be applied if only a limited number of type-dependent operations exist within the method. Following your proposed case of creating a typical type T, we can modify the method as follows:

 private <T> Map<String, byte[]> m(Map<String, T> data, Callable<T> creator){ // Create a new instance T obj = creator.call(); // Exception handling not shown // Rest of code here return null; } 

and is invoked as follows:

 Map<String, Set<String>> data = new HashMap<>(); // Instantiation method set to new HashSet (thanks to bayou.io for HashSet::new) m(data, HashSet::new); // Note: replace with anonymous inner class for java 7 

in this case, type information (which is present at the caller level) can be bypassed if the caller provides the type of functionality required. The example shows the basic creation of a HashSet for all values, but more complex rules for creating instances can be defined based on each element.

The disadvantage of this approach is that it provides complexity to the caller and can be very bad if it should be an external API function (although using private in your original method assumes otherwise). Java 7 and below also calls quite a lot of anonymous code on internal code to make the caller-side code more difficult to read. Also, if most of your methods require type information to be present, this decision becomes less feasible (since you will reprogram most of your method based on each type, defeating the point of using generics).


In general, I personally would prefer to use the second approach, if possible, only using the first approach, if it is considered impracticable. The essence of the solutions I get here is to not rely on type information when working with generics, or at least set the binding in such a way that you get the functions you need without ugly hacks. In the event that type-specific operations must be performed, provide the callers with the functionality to do this (via Callables, Runnables, or some FunctionalInterface your creation).

If type information is absolutely critical for some reason that is not clear, I suggest reading this article to completely eliminate type erasure, to be present directly from the method.

+1
source

The following caption works with the super keyword. (I tested with Java7)

 private <T> Map<String, byte[]> m(Map<String, T> data, Class<? super T> type) Map<String, Set<String>> abc = null; m(abc, Set.class); 

This is subtyping for generics.

+1
source

You need to do it like this:

 Map<String, Set> abc = null; //gives a compiler warning m(abc, Set.class) 

The problem is that if you want T be captured in Set<String> , there will be no way to express Class<T> , since there is no such thing as Set<String>.class , just Set.class .

0
source

All Articles