Reputation: 20889
I have a project, where i'm working with a lot of generics but also require typesafety. Since the stacktrace is at about 200 calls, when the current error appears, im afraid that i can not deliver the complete sourcecode that is involved. I'll try to scratch it down to the problems i have spotted so far and deliver a brief example.
First, there are classes, holding generic values. Each generic implements Comparable. That might be a Long, a Boolean - or even custom Classes implementing this interface.
Each of such a class has a DatabaseID, wich is a enum-entry that contains a default value of a certain type. MOST the time this DatabaseId's Default Value is of the same type as the Generic-Class of the Holdingclass.
Something like this:
public enum DatabaseId{
A_LONG_VALUE(0L),
A_BOOLEAN_VALUE(false),
A_INTERVAL("");
}
Interval is such a custom class implementing the Comparable interface. But also Interval is an exception from the above mentioned rule. The Generic-Holding Class for "intervals" has a generic Type T of "Interval.class", while the DatabaseValue that persists the Interval stores it as a string. like [5,250]
or [5]
for discrete values.
Now, at some point, i need to "load" the value that is stored behind a Database id and inject it into the holding's class CurrentValue Attribute.
Since this is done on a "very" parent class of the whole hierarchy this is done in a generic way also:
public void initialize() {
Database db = Database.getInstance();
try {
this.setValue((T) db.get(this.getDatabaseId()));
} catch (Exception ex) {
Log.e(LOG_TAG, "Invalid cast while initializing Value Holder.", ex);
this.setValue(null);
}
}
For the mentioned Example, where a ValueHoler is holding Interval.class
and the DatabaseId id delivering an String
, this will obviously fail. At this point, i'm expecting an Exception, because i'm trying to cast an Object
(db.get delivers Object
) to the generic Type T
(which is Interval
in this example). The real type of the Database Entry however is String
.
But: No Exception is thrown, so the setValue(null)
is never executed. The Application contionues until a point, where ValueHolder.getCurrentvalue()
is called and used to compare it with another value of the same Type i expect currentValue
to be. (Interval
in this case)
this.getCurrentValue().containsInterval(this.getRequiredValue());
At this point, JAVA notices that "CurrentValue" is a String, and throws an Exception:
java.lang.ClassCastException: java.lang.String cannot be cast to my.namespace.Interval
The Actual problem
I can easily catch this szenario, like this:
public void initialize() {
Database db = Database.getInstance();
try {
if (this.getValueHolderValueType() == Interval.class){
//Interval has an constructor for the string represenatation of it
this.setValue((T) new Interval((String)db.get(getDatabaseId()));
}else{
this.setValue((T) db.get(this.getDatabaseId()));
}
} catch (Exception ex) {
Log.e(LOG_TAG, "Invalid cast while initializing valueHolder",ex);
this.setValue(null);
}
}
but if another developer comes along and implements a new Class extending Comparable he'll might be not be aware of the "manual" casting, and ran into the same troubles.
While writing this, I found a solution to notify the developer, like this:
-doing non-generic-casts first
-comparing both types
-raise notification if Generic cast is not possible.
if (this.getValueHolderValueType() == Interval.class){
this.setValue((T) new Interval((String)db.get(getDatabaseId())));
}else if (this.getValueHolderValueType() != this.getDatabaseId().getType()){
Log.e(LOG_TAG, "Invalid cast while initializing ValueHolder. DatabaseId delivers '"+this.getDatabaseId().getType()+"', but valueHolder expects '"+this.getValueHolderValueType()+"'. Maybe you are missing a non-generic-cast right here?");
}else{
this.setValue((T) db.get(this.getDatabaseId()));
}
So the only question that remains unanswered:
WHY is this.setValue((T) db.get(this.getDatabaseId()));
NOT raising an exception, when T is of type Interval
, DatabaseID's value is of type String
and db.get is returning an Object
?
WHY is the Exception thrown in the moment, when i try to Access the "wrong class" rather than when i'm doing the cast?
Upvotes: 4
Views: 7676
Reputation: 2173
Because Java generics are implemented at the compiler level, which turns everything into Object
(or other bound) and inserts the appropriate cast in the code that uses the object, rather than the code that simply passes it around.
Edit: To address your problem, you may wish to have an abstract method in your holder that returns the correct type it can store, and compare that with the type of the value returned from the database (either an ==
comparison or one of other kind), and throw your exception (or do something else) if the comparison fails.
Edit: For instance:
Object candidate = db.get(this.getDatabaseId());
boolean canBeStored = this.getStorageType().isInstance(candidate);
...
Edit: I see you already had similar logic in place. In order to make it work more OO, I'd suggest:
Object original = db.get(this.getDatabaseId());
T modified = this.adaptObjectToContainer(original);
...
protected T adaptObjectToContainer(Object original) {
if(this.getValueHolderValueType().isInstance(original)) {
return (T)original;
}
throw new ClassCastException(...);
}
// subclasses may do relevant conversions, throw an exception, etc
protected Interval adaptObjectToContainer(Object original) {
...
if(original instanceof String) {
return new Interval(original);
}
...
}
Ideally, to minimise code, there could be a utility class for each storage type which would be the responsible for the conversion.
Upvotes: 4
Reputation: 12388
As it was pointed in the previous answer, setValue(Object) succeeds because of type erasure.
If it makes sense for your application, you may try the following approach, where a reference to the expected class is mantained:
class Foo<T> {
private final Class<T> klass;
private T value;
Foo(Class<T> klass) {
this.klass=klass;
}
void setValue(Object obj) {
value=klass.cast(obj);
}
}
Then
Foo<Integer> foo = new Foo<Integer>(Integer.class);
foo.setValue("bar");//throws exception
Upvotes: 1