Reputation: 3805
I try to write a class that take a parameter name and can return the corresponding parameter of a given object. Currently, my class look like this :
public class ParamValue<T> {
private String paramName;
@SuppressWarnings("unchecked")
public T getValue(Object obj) throws Exception {
Class<?> c = obj.getClass();
Field param = c.getDeclaredField(paramName);
boolean isAccessible = param.isAccessible();
param.setAccessible(true);
Object value = param.get(obj);
param.setAccessible(isAccessible);
return (T) value;
}
// get set ...
}
Now, imagine that we have an object with a simple Long parameter :
public class ExampleClass {
private Long value;
// get set ...
}
We can do this to get back the long value :
ExampleClass ec = new ExampleClass();
ec.setValue(12L);
ParamValue<Long> pvString = new ParamValue<>();
pvString.setParamName("value");
// print 12
System.out.println(pvString.getValue(ec));
Now, if I declare the "ParamValue" as a Point for example, it still works :
ExampleClass ec = new ExampleClass();
ec.setValue(12L);
ParamValue<Point> pvPoint = new ParamValue<>();
pvPoint.setParamName("value");
// print 12
System.out.println(pvPoint.getValue(ec));
But, as Point cannot be cast to Long, I expected some exception, like ClassCastException.
I know java compiler do some type erasure in compilation time, but I thought the compiler would automatically try to cast to Point, and fail, to the output of "pvPoint.getValue(ec)"
Can someone explain how this work ?
Thanks
Upvotes: 4
Views: 131
Reputation: 3805
I found a fancy thing, in case of someone come in there. If I extends the "ParamValue" class like thie :
public class ParamPoint extends ParamValue<Point> {
}
The type erasure apply only partially, no exception is throw, but it is still possible to test the type like this :
ParamValue<Point> pvPoint = new ParamPoint();
ParameterizedType superclassType = (ParameterizedType) pvPoint.getClass().getGenericSuperclass();
Type paramType = superclassType.getActualTypeArguments()[0];
// print true
System.out.println(paramType.equals(Point.class));
Hope this help someone.
Upvotes: 0
Reputation: 2371
ClassCastException is a RuntimeException, which means that it will be thrown only at runtime and not at compilation time. Since your value reference Object value = param.get(obj);
is of type Object, your cast to type should be allowed since all classes extend Object. Your getValue method accepts any Object as a parameter and therefore at compilation time, any class will be accepted, regardless of the parametrized type you declared.
If you wanted type safety, you could declare the parameter of the method as <? extends T>
and then only T and classes which extend it would be allowed for the method call.
You could also modify the method like this:
public <T> T getValue(Class<T> returnType, Object obj) throws Exception {
Class<?> c = obj.getClass();
Field param = c.getDeclaredField(paramName);
boolean isAccessible = param.isAccessible();
param.setAccessible(true);
Object value = param.get(obj);
param.setAccessible(isAccessible);
return returnType.cast(value);
}
In this case there is no need to parametrize your class. You can even modify the return statement from the above method to something like this:
if (returnType.isInstance(value)) {
return returnType.cast(value);
} else {
// could be replaced with a return null
throw new ClassCastException("Exception message");
}
Upvotes: 2
Reputation: 5095
You are suppressing the compiler warning that you are doing an unchecked cast, so the compiler will not catch it. And at runtime, the type info is not available.
If you would like your code to actually throw a ClassCastException, you could add a field storing the actual target class. That will also make it possible to remove the @SuppressWarnings
annotation. The resulting class will look like this:
class ParamValue<T> {
private final Class<T> clazz;
private final String paramName;
ParamValue(Class<T> clazz, String paramName)
{
this.clazz = clazz;
this.paramName = paramName;
}
public T getValue(Object obj) throws Exception {
Class<?> c = obj.getClass();
Field param = c.getDeclaredField(paramName);
boolean isAccessible = param.isAccessible();
param.setAccessible(true);
Object value = param.get(obj);
param.setAccessible(isAccessible);
return clazz.cast(value);
}
}
and can be invoked like this:
ExampleClass ec = new ExampleClass();
ec.setValue(12L);
ParamValue<Point> pvPoint = new ParamValue<>(Point.class, "value");
// print 12
System.out.println(pvPoint.getValue(ec));
Upvotes: 1
Reputation: 8241
The cast (T)
doesn't do anything at runtime. It only calms the compiler.
The method PrintStream.println
has multiple overloads, one of which does accept an Object
parameter. So no casts are executed in your code.
You'd see a ClassCastException
in these cases:
Store the value in a variable of the wanted type:
final Point value = pvPoint.getValue(ec); // CCE
System.out.println(value);
Provide a Class<T>
instance to the ParamValue
for runtime checks:
public class ParamValue<T> {
private final Class<T> clazz;
public ParamValue(Class<T> clazz) {
this.clazz = clazz;
}
public T getValue(Object obj) throws Exception {
Class<?> c = obj.getClass();
Field param = c.getDeclaredField(paramName);
boolean isAccessible = param.isAccessible();
param.setAccessible(true);
Object value = param.get(obj);
param.setAccessible(isAccessible);
return clazz.cast(value); // instead of (T) value
}
// get set ...
}
And then:
ParamValue<Point> pvPoint = new ParamValue<>(Point.class);
pvPoint.setParamName("value");
System.out.println(pvPoint.getValue(ec)); // CCE
Upvotes: 1