javagenericsreflectioninterfaceraw-types

Using Raw types to dynamically apply type parameters for a reflected subclass


IMPORTANT:

the code I currently have Is working per my expectations. It does what I want it to do. My Question is about wether the WAY in which I have made it work is wrong. The reason I am asking this is because I've seen plenty of stack overflow results about raw types and how they should basically NEVER be used.

What I'm doing and Why I used raw types

Currently I am dynamically creating a concrete subclass of a generic interface where the interface takes in parameters when the class is constructed. When I make an instance of this class and use its returned object to call various methods, I use raw types because it works for what I'm trying to do. Here is an example in my functioning code where the raw types are used. This code is in top down order i.e. between code blocks there is no code.

Loading properties file

Properties prop = new Properties();
    try {
    prop.load(ObjectFactory.class.getResourceAsStream("config.properties"));

This is the File parser that implements FileParserImplementation and takes in the data and puts it into an array. This code gets the Class type and then makes an instance of that type dynamically.

Class<? extends FileParserImplementation> parser = null;
parser = Class.forName(prop.getProperty("FileParserImplementation")).asSubclass(FileParserImplementation.class);
FileParserImplementation ParserInstance = (FileParserImplementation) parser.getDeclaredConstructors()[0].newInstance();

These two classes and their instances are the two seperate DataParsers implementing DataParserImplementation. These take in the array of Strings that the FileParser gives and creates objects/manipulates the data into whatever is needed. It puts out a Collection of this data. The Fileparser dependency is passed in through constructor injection. This can be configured through the properties file at runtime.

Class<? extends DataParserImplementation> dataset1 = Class.forName(prop.getProperty("DataParserImplementation_1")).asSubclass(DataParserImplementation.class);
Class<? extends DataParserImplementation> dataset2 = Class.forName(prop.getProperty("DataParserImplementation_2")).asSubclass(DataParserImplementation.class);
DataParserImplementation Dataset1Instance = (DataParserImplementation) dataset1.getDeclaredConstructors()[0].newInstance(ParserInstance);
DataParserImplementation Dataset2Instance = (DataParserImplementation) dataset2.getDeclaredConstructors()[0].newInstance(ParserInstance);

This is the Crossreferencer class that implements CrossReferencerImplementation. It takes in the two datasets and Cross references them In whatever way is desired by the actual concrete reflected class. This also can be configured at runtime. It outputs a Map in this main. The map serves as the final collection for the data (I might change that later).

Class<? extends CrossReferenceImplementation> crossreferencer = Class.forName(prop.getProperty("CrossReferenceImplementation")).asSubclass(CrossReferenceImplementation.class);
CrossReferenceImplementation crossReferencerInstance = 
(CrossReferenceImplementation) crossreferencer.getDeclaredConstructors()[0].newInstance();

Getting the Map result from calling a method on our reflected instance. Then the contents of this map are printed out. currently it seems the map parameters are gotten as well because the Objects that are inside the map are properly using their toString methods when reflectiveFinalMap.get(key).toString() is called. This leads me to believe it works as I intend.

Map reflectiveFinalMap = (Map) 
crossReferencerInstance.CrossReference(Dataset1Instance.Parse(), Dataset2Instance.Parse());
for (Object key:reflectiveFinalMap.keySet()) {
            System.out.println(key + " { " + 
reflectiveFinalMap.get(key).toString() + " }");
        }
    return reflectiveFinalMap;
}
//catch block goes here

Notice that each time I reflectively create an instance of a class that implements one of my interfaces, I use the interface as the raw type. My Hope is that the reflection then sees the parameterized type of this raw type when it creates the concrete subclass, because thats where the parameter types are actually specified. The point is to let any class that implements those interfaces be generic to the point where they can take in just about anything and return just about anything.

Things I tried to not use raw types.

I've tried to actually obtain the parameterized type of CrossReferenceImplementation in the reflected crossreferencer Class that I get right now by calling

Class arrayparametertype = (Class)((ParameterizedType)crossreferencer.getClass().getGenericSuperclass()).getActualTypeArguments()[0];

And then I tried to pass in that arrayparameter when creating an instance of crossreferencer like this:

CrossReferenceImplementation crossReferencer = (CrossReferenceImplementation<<arrayparametertype>>) crossreferencer.getDeclaredConstructors()[0].newInstance();

That didn't work since variable parameter types apparently aren't a thing. I tried to manually specify the specific parameter of the concrete reflected class(I DON'T want this anyway because it breaks the whole point of reflection here, decoupling the Classes from each other by being able to use anythng that implements the appropriate interface). This caused this warning to appear and the code to not actually run the methods it was supposed to:

//how the parameters were specified. Messy and breaks the reflection.
CrossReferenceImplementation<Map<String, SalesRep>,Map<String, SalesRep>,Map<String, SalesRep>> crossReferencer = (CrossReferenceImplementation) crossreferencer.getDeclaredConstructors()[0].newInstance();

//where the warning occured
Map reflectiveFinalMap = (Map) crossReferencer.CrossReference(Dataset1.Parse(), Dataset2.Parse());

The Warning: "Dataset1 has raw type so result of Parse is erased". Note that SalesRep here is the object in which the data is held as fields of that object. This object gets manipulated and put into various collections. It too is accessed via reflection in the many methods of DataParserImplementations

A similar error message and problem occured when specifying the parameter type of the Map (AGAIN I DON'T want this because it makes the reflection pointless I want the map return result to be generic and be specified by the implementing class).

//where the parameterized type was specified
Map reflectiveFinalMap = (Map<String,SalesRep>) crossReferencer.CrossReference(Dataset1.Parse(), Dataset2.Parse());

When specifying the actual parameterized type of the map result the error message was: "crossReferencer has raw type so result of CrossReference is erased".

Running the code did indeed confirm for me that .CrossReference method's results were erased while everything else ran fine.

What internet searches I tried before asking here

So I used the raw types for both operations As can be seen in the main code and everything worked fine. But I have seen so much "Don't use raw types". And this is why I ask: Is this an appropriate use of raw types? Should I do it a different way that DOESN'T break the reflection? It breaks the reflection because manually specifying the type parameter not only makes my code not run, it also means ONLY that concrete class can be used. I reflected so that I could use anything that implements the generic interface. I don't want to only be able to use specific concrete instances. I've tried searching stack overflow for whats in my title and other similar things. I think this might be related to type erasure but I'm honestly not sure of that. Nothing else really addressed this problem because nothing talked about generics, parameterized types and reflection all at once (the crux of my problem). I have been told generics and reflection don't play well together but this code works anyways and works the way I want it to. It works well. I just want to make sure I'm not doing something TERRIBLY wrong.

The Goal.

To gain an Understanding of my current usage of raw types so I know I'm doing it the right way. By 'Right' I mean the opposite of what I define as the 'Wrong' Way below. An example of what 'Understanding' I seek is:

To understand why puesdo code along the lines of:

ConcreteClass forname(myPropertiesFileObject.get(ConcreteClassname)) as subClass of (MyGenericInterface);
MyRAWGenericInterfaceType ConcreteClassInstance = (MyRAWGenericInterfaceType) ConcreteClass.newInstance( Insert generic Type constructor arguments here);
RAWCollectionType someCollection = RAWCollectionType concreteClassInstance.CallingAMethod(Insert generic Type method arguments here);

Uses Raw types where RAW is contained in the Interface or collection type name. This is as opposed to doing it in some way that doesn't use raw types but doesn't break the point of the reflection, to decouple the interactions between these classes. Specifying the parameters with hard code would 'break the reflection' in this case. Additionally I'd like to understand Why specifying parameters (even if I know thats not what I'm going to do) for these RAW types in the pusedocode above causes the errors listed above in the question, Namely why is the result of CallingAMethod erased when supplying the actual parameters to the RAWCollectionType that the method returns? The root problem is that when I supply type parameters to RAWCollectionType when I declare it, it refuses to be updated by what CallingAMethod returns and I Don't Understand Why. It takes the return value, but if the body of the method CallingAMethod has the returned value passed in as an argument, updated inside the method and then returned, the return that I receive doesn't have the updates. CallingAMethod in this example would be like if I hada list like:

[1,2,3]

and inside the method I had something like:

foreach(thing in list){
    thing += 1
}

and then I returned the list, the return I'd get when specifying parameters would be [1,2,3] and when using raw types it would be [2,3,4] like I desire. I'm asking this because I've heard bad things about using raw types.

Additionally I want to make sure that my use of raw types is not horribly wrong and that it works because it's SUPPOSED to work. Maybe I've just gotten good at the whole reflection and generics thing and found a valid use for raw types, or I could be doing something so horrible it warrants my arrest. Thats what i intend to find out. To clarify, by wrong I mean:

bad design (should use a different way to call my methods reflectively and also use reflective classes that use generic interfaces)

inefficient design(time complexity wise, code line wise or maintainability wise)

there is a better way, you shouldn't even be doing this in the first place

If any of those reasons or something I missed popped out when you read this code then TELL ME. Otherwise please explain then why my use of raw types is Valid and isn't a violation of this question:[link]What is a raw type and why shouldn't we use it?


Solution

  • Java have type erasure, so your Map<A,B> in runtime is just a Map, same for CrossReferenceImplementation<Map<String, SalesRep>,Map<String, SalesRep>,Map<String, SalesRep>> is just a CrossReferenceImplementation.
    This also means that you can cast any map to Map and just put any objects you want in it, so you can have Map<String, Long> that is actually storing objects of Map<Cookie, Fish> type, and this is why you need to be careful with raw types and reflections.

    You can't really use reflection and generics normally - you will always have some unchecked code then, but you can limit it to minimum and make it kind of type-safe anyways.

    Like you can create own method to get field: (this is a bit of pseudocode, I will skip all possible exceptions, etc)

    public class FieldAccessor<O, T> { 
        final Field field; // + private constructor
        public T get(O object) { return (T) field.get(object); } // unsafe, bu we validated this before constructing this accessor
        public static <O, T> FieldAccessor<O, T> create(Class<? super O> definingClass, Class<? super T> fieldClass, String fieldName) {
            Field field = definingClass.getDeclaredField(fieldName);
            if (field.getType() != fieldClass) {
                throw some exception;
            }
            return new FieldAccessor<>(field);
        }
    

    Then you have all the needed validation before you need to use that field, and it will already return valid type. So you can get some value of valid type and add it to normal generic Map instance.

    FieldAccessor<X, A> keyAccessor = FieldAccessor.create(X.class, A.class, "someProperty");
    FieldAccessor<Y, B> valueAccessor = FieldAccessor.create(Y.class, B.class, "someOtherProperty");
    Map<A, B> myMap = new HashMap<>();
    mapMap.put(keyAccessor.get(myXValue), valueAccessor.get(myYValue));
    

    This way you have type safe code that still works on reflections - it might still fail at runtime if you will provide invalid types, but at least you always know where it will fail - as here FieldAccessor is already checking all the types in runtime to ensure that you will not do something stupid like add Integer to Map<String, Long> as this might be hard to debug later. (unless someone will use this accessor as raw type, as .get isn't validated - but you can add that by passing definingClass to constructor and checking object instance in get methods)

    You can do similar stuff for methods and fields that use generic types (like field of Map<X, Y> type, this FieldAccessor would only allow you to check if it is some kind of Map) - but it would be much harder as API for generics is still a bit "empty" - there is no build in way to create own instances of generic types or to check if they are assignable. (libraries like gson does that so they can deserialize maps and other generic types, they have own implementation of java generic type representation interfaces, like that ParameterizedType and implemented own method to check if given types are assignable)

    Just when you are using reflections you need to always remember and understand that you are the one responsible for validating types, as compiler can't help you here, so that unsafe and raw typed code is fine as long as you have logic that validates if this code will never do something really unsafe (like that passing wrong type to generic method, like Integer to map of Long).
    Just don't throw raw types and reflections in the middle of some normal code, add some abstraction to it, so it will be easier to maintain such code and project.

    I hope this somewhat answers your question.