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){
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.