Understanding generics: Incompatible types when a class has a generic type and implements one of its parameterized superclasses

I am using a simple application with MVP architecture.

Here are my interfaces MvpView

and MvpPresenter

(nothing interesting in MvpModel

, so I'll skip it):

/// MvpView.java
public interface MvpView {
}

/// MvpPresenter.java
public interface MvpPresenter<V extends MvpView> {
    void attachView(V view);

    void detachView();
}

      

I now have a basic implementation MvpView

which is Activity

:

// BaseActivity.java
public abstract class BaseActivity<V extends MvpView, P extends MvpPresenter<V>> 
        extends AppCompatActivity implements MvpView {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPresenter().attachView(this);
    }

    public abstract P getPresenter();

    // other logic
}

      

As for me, everything looks correct, but there is a compilation error at the line:

    getPresenter().attachView(this);

      

incompatible types

If I add append to V

, then the project compiles and everything works fine:

    getPresenter().attachView((V) this);

      

1. The question is , why do I need this cast or why am I experiencing this incompatible non-cast type error?

(1 already answered by Eran )

2. How can I link V

to BaseActivity

in this example or how best to implement this MVP approach?

This is weird, as it is for me, because mine BaseActivity

expands MvpView

as it is defined by this shared parameter V extends MvpView

:!

+3


source to share


2 answers


getPresenter()

is an instance of the type P

that extends MvpPresenter<V>

. Therefore, it getPresenter.attachView()

expects a type argument V

.

Now we know what V

needs to implement MvpView

, and we also know that BaseActivity

sells MvpView

, but the implementation is not necessarily the same.

For example, you can create a specific subclass SubBaseActivity

and create it with:



SubBaseActivity<MvpViewImpl, MvpPresenterImpl<MvpViewImpl>>
    activity = new SubBaseActivity<> (); // let ignore the fact that you are not suppose
                                         // to instantiate Android activities this way

      

Now getPresenter()

returns a MvpPresenterImpl

and getPresenter().attachView()

expects a type argument MvpViewImpl

. But this

not type MvpViewImpl

.

When you make an unsafe listing from BaseActivity<V,P>

to V

, you are saying that a compiler BaseActivity<V,P>

can be added to V

. However, the reason this works at runtime is because the compiler erases the type parameters V

and P

. Since the type boundary V

is equal MvpView

, the casting does not V

become cast to MvpView

, which it implements BaseActivity

.

+1


source


The problem is what you think it is V

referencing BaseActivity<V,?>

, because you are probably only using it in practice.

Unfortunately, there is currently no way to reference the class you are declaring within the scope of the type arguments to enforce such a constraint.

The simplest fix is ​​to use an unchecked cast (masking the warning if you like):

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    @SupressWarning("unchecked")
    final V myView = (V) this;
    getPresenter().attachView(myView);
}

      

You don't need to use a local variable myView

, but declaring it explicitly allows you to disable only this cast, not any other "unchecked" warning in this method.

In any case, you should make sure that any extensible class will set its type parameter V

for itself, so as not to break the contract:



public class ConcreteActivity<V extends MvpView, P extends MvpPresenter<V>> extends BaseActivity<ConcreteActivity<V,P>, P> {
...
} 

      

At compile time, you cannot ensure that your test code must use reflection at compile time to ensure that every extending class meets that constraint.

You can go a little further to avoid the immediate throw warning, but for that you need to add a field typed V

to point to this

, set in the BasicActivity constructor, passed as a parameter by the extending class constructor ....

public abstract class BaseActivity<V extends MvpView, P extends MvpPresenter<V>> 
        extends AppCompatActivity implements MvpView {

    private final V myView;

    protected BaseActivity(final V myView) {
       if (myView != this) { throw new IllegalArgumentException("you must pass this object"); } 
       this.myView = myView;
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getPresenter().attachView(myView);
    }

    public abstract P getPresenter();

    // other logic
}


public class ConcreteActivity<V extends MvpView, P extends MvpPresenter<V> extends BaseActivity<BaseActivity<V, P>, P> {

    public ConcreteActivity() {
       super(this);
    }
    ...
}

      

Note that we double check what myView

is actually this

in the constructor in order not to work earlier if the extension class does not match; you can leave this if your test code guarantees it will always be the case.

Although it is preferable to avoid any warning ... I would say that in this situation the first alternative is perfectly acceptable, since it requires less code and is more memory efficient.

0


source







All Articles