Generic methods returning dynamic object types

51,699

Solution 1

You could try type tokens:

public <T> T getValue(Class<T> cls) {
    if (valVal == null) return null;
    else {
        if (cls.isInstance(valVal)) return cls.cast(valVal);
        return null;
    }
}

Note, that this does not do any conversion (i.e., you cannot use this method to extract a Double, if valVal is an instance of Float or Integer).

You should get, btw., a compiler warning about your definition of getValVal. This is, because the cast cannot be checked at run-time (Java generics work by "erasure", which essentially means, that the generic type parameters are forgotten after compilation), so the generated code is more like:

public Object getValVal() {
    return valVal;
}

Solution 2

As you are discovering, there is a limit to what can be expressed using Java's type system, even with generics. Sometimes there are relationships between the types of certain values which you would like to assert using type declarations, but you can't (or perhaps you can, at the cost of excess complexity and long, verbose code). I think the sample code in this post (question and answers) is a good illustration of that.

In this case, the Java compiler could do more type checking if you stored the object/string representation inside the "transformer". (Perhaps you'll have to rethink what it is: maybe it's not just a "transformer".) Put a generic bound on your base Transformer class, and make that same bound the type of the "object".

As far as getting the value out of the cell, there's no way that compiler type checking will help you there, since the value can be of different types (and you don't know at compile time what type of object will be stored in a given cell).

I believe you could also do something similar to:

public <T> void setObject(Transformer<T> transformer, T object) {}

If the only way to set the transformer and object is through that method, compiler type checking on the arguments will prevent an incompatible transformer/object pair from going into a cell.

If I understand what you're doing, the type of Transformer which you use is determined solely by the type of object which the cell is holding, is that right? If so, rather than setting the transformer/object together, I would provide a setter for the object only, and do a hash lookup to find the appropriate transformer (using the object type as key). The hash lookup could be done every time the value is set, or when it is converted to a String. Either way would work.

This would naturally make it impossible for the wrong type of Transformer to be passed in.

Solution 3

I think you are a static-typed guy, but lemme try: have you thought about using a dynamic language like groovy for that part?

From your description it seems to me like types are more getting in the way than helping anything.

In groovy you can let the Cell.valVal be dynamic typed and get an easy transformation around:

class Cell {
  String val
  def valVal
}

def cell = new Cell(val:"10.0")
cell.valVal = cell.val as BigDecimal
BigDecimal valVal = cell.valVal
assert valVal.class == BigDecimal
assert valVal == 10.0

cell.val = "20"
cell.valVal = cell.val as Integer
Integer valVal2 = cell.valVal
assert valVal2.class == Integer
assert valVal2 == 20

Where as it's everything needed for the most common transformations. You can add yours too.

If needing to transform other blocks of code, note that java's syntax is valid groovy syntax, except for the do { ... } while() block

Share:
51,699

Related videos on Youtube

K.Barad
Author by

K.Barad

Casual programmer in my spare time, and a semi casual programmer at work: I believe strongly in the use of automation to save effort and regularly make micro tools for performing quick tasks. I use a variety of languages: C (ANSI), Java, Dos Batches, and also believe in using Excel spreadsheets as a form of langauge for data processing. I often combine several steps in different language where it is more effective. While only a a casual programmer I consider myself to be good at thinking like a programmer and am fast at planning data architectures, optimising for efficient running, and algorithm development. I believe the secret of good software design is not technique as much as mindset: I often think like a machine (and have about as bad a sense of humour!)

Updated on July 09, 2022

Comments

  • K.Barad
    K.Barad almost 2 years

    Possibly a question which has been asked before, but as usual the second you mention the word generic you get a thousand answers explaining type erasure. I went through that phase long ago and know now a lot about generics and their use, but this situation is a slightly more subtle one.

    I have a container representing a cell of data in an spreadsheet, which actually stores the data in two formats: as a string for display, but also in another format, dependent on the data (stored as object). The cell also holds a transformer which converts between the type, and also does validity checks for type (e.g. an IntegerTransformer checks if the string is a valid integer, and if it is returns an Integer to store and vice versa).

    The cell itself is not typed as I want to be able to change the format (e.g. change the secondary format to float instead of integer, or to raw string) without having to rebuild the cell object with a new type. a previous attempt did use generic types but unable to change the type once defined the coding got very bulky with a lot of reflection.

    The question is: how do I get the data out of my Cell in a typed way? I experimented and found that using a generic type could be done with a method even though no constraint was defined

    public class Cell {
        private String stringVal;
        private Object valVal;
        private Transformer<?> trans;
        private Class<?> valClass;
    
        public String getStringVal(){
            return stringVal;
        }
    
        public boolean setStringVal(){
            //this not only set the value, but checks it with the transformer that it meets constraints and updates valVal too
        }
    
        public <T> T getValVal(){
            return (T) valVal;
            //This works, but I don't understand why
        }
    }
    

    The bit that puts me off is: that is ? it can't be casting anything, there is no input of type T which constrains it to match anything, actually it doesn't really say anything anywhere. Having a return type of Object does nothing but give casting complications everywhere.

    In my test I set a Double value, it stored the Double (as an object), and when i did Double testdou = testCell.getValVal(); it instantly worked, without even an unchecked cast warning. however, when i did String teststr = testCell.getValVal() I got a ClassCastException. Unsurprising really.

    There are two views I see on this:

    One: using an undefined Cast to seems to be nothing more than a bodge way to put the cast inside the method rather than outside after it returns. It is very neat from a user point of view, but the user of the method has to worry about using the right calls: all this is doing is hiding complex warnings and checks until runtime, but seems to work.

    The second view is: I don't like this code: it isn't clean, it isn't the sort of code quality I normaly pride myself in writing. Code should be correct, not just working. Errors should be caught and handled, and anticipated, interfaces should be foolproof even if the only expecter user is myself, and I always prefer a flexible generic and reusable technique to an awkward one off. The problem is: is there any normal way to do this? Is this a sneaky way to achieve the typeless, all accepting ArrayList which returns whatever you want without casting? or is there something I'm missing here. Something tells me I shouldn't trust this code!

    perhaps more of a philosophical question than I intended but I guess that's what I'm asking.

    edit: further testing.

    I tried the following two interesting snippets:

    public <T> T getTypedElem() {
        T output = (T) this.typedElem;
        System.out.println(output.getClass());
        return output;
    }
    
    public <T> T getTypedElem() {
        T output = null;
        try {
            output = (T) this.typedElem;
            System.out.println(output.getClass());
        } catch (ClassCastException e) {
            System.out.println("class cast caught");
            return null;
        }
        return output;
    }
    

    When assigning a double to typedElem and trying to put it into a String I get an exception NOT on the cast to , but on the return, and the second snippet does not protect. The output from the getClass is java.lang.Double, suggesting that is being dynamically inferred from typedElem, but that compiler level type checks are just forced out of the way.

    As a note for the debate: there is also a function for getting the valClass, meaning it's possible to do an assignability check at runtime.

    Edit2: result

    After thinking about the options I've gone with two solutions: one the lightweight solution, but annotated the function as @depreciated, and second the solution where you pass it the class you want to try to cast it as. this way it's a choice depending on the situation.

    • Alex D
      Alex D over 11 years
      One more comment on this post (besides everything I wrote below): I think in this case the extra compiler checking you are trying to achieve won't actually catch many bugs. If you make your code as concise as possible (although you forego some type checking), you'll probably wind up with a more reliable program in the end.
    • K.Barad
      K.Barad over 11 years
      Library writing is always that line between functionality and simplicity. I am thinking about either taking it as it is (you shouldn't read data without knowing what it is, and the transformers let you check anyway) or Dirk's proposal (just give it an input and take the type off that).
    • Juh_
      Juh_ over 9 years
      Good question but way too long!
  • dcow
    dcow over 11 years
    As much as I like this answer, it cheating to say, "Use a different language where you don't have this problem."..
  • K.Barad
    K.Barad over 11 years
    not going outside of standard Java. Four reasons are compatiblity and portability, availabilty of libraries on other machines, collaboration with others, and I have a mental dislike of huge libraries/language extensions which are more complex to use than it is to make it yourself.
  • Alex D
    Alex D over 11 years
    @DavidCowden -- it's not cheating. Groovy, Scala, Clojure, and JRuby all run on the JVM and can interoperate with the OP's Java codebase. More than that, the software industry as a whole needs to get the message that we have much better languages than Java/C#/C++ now. Java was fine back in 1995, but we can do much, much better now, and people need to keep hearing that.
  • K.Barad
    K.Barad over 11 years
    I know there are limitations and I'm close to the edge. That's why I'm trying to get a feel for which side of the edge I'm on. I don't think I'm too upset if I can't get an active assertion that this is a safe cast, I know better than to expect this is standard usage and generics have generally been a great tool and very useful in the past for me. the Transformer is an interface, and not so simple as the example of course. A DateTransformer concept would be an implementation that expects a certain date format, you could have a PercentageTransformer, MonthTransformer, or custom types.
  • Alex D
    Alex D over 11 years
    @K.Barad -- the language which WillP is showing you here is compatible with Java, and is portable to anywhere where Java goes (because it runs on the JVM). It can call into Java code, so availability of libraries is not a problem. As for collaboration with others, many people are using these newer languages already, and more will follow. If you follow WillP's advice and learn a newer, Java-compatible language, you will not only solve the problem you have with generics -- you may find it revolutionizes your whole way of programming.
  • Alex D
    Alex D over 11 years
    If it's an interface, you can still parameterize the interface, and give it getters/setters which use the generic bound of the interface itself, right? ie interface Transformer<T> { T get(); void set(T object); }
  • dcow
    dcow over 11 years
    @AlexD Sure Clojure and Groovy, etc. all run on the JVM and allow you to program under different paradigms all together. Great. It doesn't mean you answered K.Barad's question though..
  • K.Barad
    K.Barad over 11 years
    The Transformer (and some of the other functions left out of the example) is part of a library I'm rebuilding (since the previous typed version was clearly too cumbersome). In particular my transformer interface is double typed, and my cell locks it down as <String, ?>. I have quite a few functions I'm building up as a concept of DualTyped data which exists in two forms, with the Transformers forming the exchange and checks. The Cell, however, isn't so constrained. As far as I can get it there is no way to make a general type variable which is dynamic but shared, correct me if I'm wrong?
  • K.Barad
    K.Barad over 11 years
    sorry, comment took more than 1 box. I think we are commenting in parallel :P. The transformer is typed, but that is another file. The issue is that type parameter is only applicable inside a transformer, and more specifically it is fixed the instant you instantiate it. But a cell may have it's value and transformer removed and replaced with a different transformer and recalculated. This is why I'm trying to take complexity out of the cell and the spreadsheet to make it something that only applied to the data.
  • Will
    Will over 11 years
    We are trying to show a different kind of solution, different point of view :-). What we are questioning is: do we really need to workaround generics? Is there any reason to all this? I've used groovy alongside java for two years in a production environment, and both play great together. Groovy is just a jar inside your app. Netbeans, Eclipse and Intellij all offer great support for groovy. Also, JRuby and Clojure are great alternatives.
  • Alex D
    Alex D over 11 years
    I also think that putting the value inside the transformer doesn't really make sense -- it was just a way to get stricter type checking out of the compiler. I just added another idea for you...
  • K.Barad
    K.Barad over 11 years
    I never really got past ANSI C to be honest, call me old fashioned. Thing is this is not something that is going to be run on a computer with any flexibility, I know some of the places where I'll use this library I can't even control what version of Java I get (other than that it's Java 1.6). It is also going to be have to be sustainable by multiple people and I want it to be a single jar, as small as possible without repackaging. I know there are other branches of Java out there, but I do think that saying "use a different language" is cheating :P
  • K.Barad
    K.Barad over 11 years
    I know how erasure works yes, although the generated code is a little more complex than that. I've pondered a method like that already, but isInstance is not a very good check, I am more after Class.isAssignableFrom(cls). It's not as elegant as I'd like it but it is one of the ways I can get the assertion in place. I could always have both options, with a disclaimer. It would be easier to use the simple option for simple situations with a predictable datatype, and then the stronger option for the truly dynamic case. It could be made easier to not ask for the class, but just an example
  • Will
    Will over 11 years
    To me, Java's greatest heritage will be the JVM. As a language it's limited. As a platform it rocks. Instead of the "using a different language is cheating" thinking, think "use the right tool for the right job". Stop trying to open the tuna can with the screwdriver; it will work, but will make a mess in your kitchen :-). Well, in the end, it's your call :-)
  • K.Barad
    K.Barad over 11 years
    a little more complex than that. Actualyl you could have multiple Transformers for the same type. I could have a transformer for UINT16Transformer which only accepts if it represents an integer which is expressable an an unsigned 16 bit int, another class for UINT8Transformer, but actually store the information in a normal Integer, just this cell is constrained. Yes, i will be constraining it so that the cell can only be updated through getters and setters which check with the Transformer. What's harder is if there are mutable objects, but that's not a new issue.
  • K.Barad
    K.Barad over 11 years
    Granted the electric tin opener is the easiest way, but at the end of the day your swiss army knife in your pocket is the only thing you know you will always have with you. The flipside to your argument is "find better ways to use the tools you have rather than getting specialist tools for everything". Not that there aren't good points both ways, but certainly in this case the decision is not mine and I'm limited to not even getting to chose the updated set of standard libraries: I have to use what i'm given.
  • K.Barad
    K.Barad over 11 years
    The more I think about it the more this seems like the safest solution. I'm sure I can find a way to deal with the extra complexity. The more convincing point is that this is the method used for a lot of the other collections libraries, suggesting there isn't a cleaner or nicer method. If you want to be able to enforce the type you need to get it from somewhere.
  • Will
    Will over 11 years
    The flipside doesn't apply: groovy runs on top of java and is as general purpose as java. Well, no point in keeping the discussion if you can't do in anything but java. I'll just leave the idea for future projects :-).
  • JAB
    JAB almost 11 years
    From my experience, even though the JVM doesn't do a check at the point of source-explicit cast, it will still do a check when you try assigning the result of the generified method to a variable of the expected type (and I actually find that to be a bit better in most cases than having the exception being thrown within the getValue() method, as in cases like that it's normally an issue with the code at the point of method call rather than anything in the getValue() method itself, so having the topmost item in the stacktrace being the line where the method is called makes more sense to me).
  • JAB
    JAB almost 11 years
    (Well, assuming the type parameter is not itself a generified type, of course. Things get a lot trickier when you're doing casts with generified types. I also thought there might be issues with assigning objects to a variable when the actual object is an instance of a subclass of the variable's type, but when I double-checked it seems that it's perfectly fine to cast a variable to a superclass or implemented interface, as getClass() and instanceof still work with the actual class of the value.)