Reputation: 1016
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.
Upvotes: 28
Views: 58918
Reputation: 30445
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.
Upvotes: 3
Reputation: 31053
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;
}
Upvotes: 23
Reputation: 14529
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
Upvotes: 1