Burke
Burke

Reputation: 35

Java: Reflection, Generic Types, and Unchecked Casts

My class will be given an Object class. I am then using reflection to iterate over the declared fields of that class and registering a ChangeListener on each field with the Property base class.

The original 'createChangeListener' method looked like this:

private void createChangeListener(Property property) {
    property.addListener(new ChangeListener() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {                        
            Foo.this.propertyChanged(observable);
        }
    });
}

However, this was producing an unwanted warning:

warning: [unchecked] unchecked call to addListener(ChangeListener<? super T>) as a member of the raw type ObservableValue
    property.addListener(new ChangeListener() {
        where T is a type-variable:
    T extends Object declared in interface ObservableValue

Not to be dissuaded, I provided a generic type for my Property parameter and ChangeListener:

private void createChangeListener(Property<Object> property) {
    property.addListener(new ChangeListener<Object>() {
        @Override
        public void changed(ObservableValue observable, Object oldValue, Object newValue) {                        
            Foo.this.propertyChanged(observable);
        }
    });
}

...Only now to be notified that I have simply shifted my problem to the source of the reflection. The code, below, is now modified to cast to Property<Object> from its original Property w/o a generic type:

if (Property.class.isAssignableFrom(field.getType())) {
    createChangeListener((Property<Object>)(field.get(model)));
}

This previously warningless code is now producing the head-tilting:

warning: [unchecked] unchecked cast
    createChangeListener((Property<Object>)(field.get(model)));
required: Property<Object>
found:    Object

Questions:

Upvotes: 3

Views: 491

Answers (2)

Has QUIT--Anony-Mousse
Has QUIT--Anony-Mousse

Reputation: 77475

You need to ramp up your generics skills.

Instead of trying to solve it by casts, try solving it by writing generic code yourself:

private <T> void createChangeListener(Property<T> property) {
  property.addListener(new ChangeListener<T>() {
    @Override
    public void changed(ObservableValue<? extends T> observable, T oldValue, T newValue) {                        
        Foo.this.propertyChanged(observable);
    }
  });
}

Here, the compiler can help you with type checking. It knows that oldValue and newValue will have a certain type T, and can check that you do not make incorrect assumptions.

Now since addListener(ChangeListener<? super T>) will also accept listeners for super types (<? super T>!), the following should be fine, too:

private void createChangeListener(Property<?> property) {
  property.addListener(new ChangeListener<Object>() {
    @Override
    public void changed(ObservableValue<?> observable, Object oldValue, Object newValue) {                        
        Foo.this.propertyChanged(observable);
    }
  });
}

The compiler should be able to verify that this code is type safe.

Make sure you know the differences between <?>, <Object>, <T>, <? super T>, <? extends T> and try to learn how to use them in your own code. Always prefer the broader version (extends and super can make all the difference! Check the Java Collections for examples).

In above example, the super means that you are allowed to attach an Object listener to a Property<String>, because it is a supertype of String.

Upvotes: 1

Tagir Valeev
Tagir Valeev

Reputation: 100279

Casting your field value to Property<Object> you say explicitly that Property generic parameter is Object, which may be wrong and compiler cannot help you. For example, the real type of the field is Property<String> you may do the following:

Property<Object> p = (Property<Object>)(field.get(model)); // compiler warning, runtime ok
p.setValue(new Object()); // runtime ok, but now you're probably stored incompatible value

// somewhere much later in completely different piece of code
String value = this.propertyField.getValue(); // sudden ClassCastException

So compiler warns you that casting to Property<Object> you are making an assumption the compiler cannot prove and you may have actually different type which may result in putting the object into incorrect state which may break your program somewhere much later.

In your particular case you don't want to say "I know that generic argument type is an Object". What do you want to say is "I don't care about generic argument type". For this case there's a special construct <?>. You can use it here:

void createChangeListener(Property<?> property) {
    property.addListener(new ChangeListener<Object>() {
        @Override
        public void changed(ObservableValue<?> observable, 
                            Object oldValue, Object newValue) {                        
            Foo.this.propertyChanged(observable);
        }
    });
}

And you can safely cast to Property<?> without any warning. Now however you cannot call setValue at all, but seems that you don't need anyways.

Upvotes: 2

Related Questions